diff --git a/.gitignore b/.gitignore index 2629f97d1..3ffacac45 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,8 @@ lib/generated_plugin_registrant.dart test/services/coins/bitcoin/bitcoin_wallet_test_parameters.dart test/services/coins/firo/firo_wallet_test_parameters.dart test/services/coins/dogecoin/dogecoin_wallet_test_parameters.dart +test/services/coins/namecoin/namecoin_wallet_test_parameters.dart +test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart /integration_test/private.dart # Exceptions to above rules. diff --git a/assets/images/bitcoincash.png b/assets/images/bitcoincash.png new file mode 100644 index 000000000..18552e02e Binary files /dev/null and b/assets/images/bitcoincash.png differ diff --git a/assets/svg/coin_icons/Bitcoincash.svg b/assets/svg/coin_icons/Bitcoincash.svg new file mode 100644 index 000000000..4e700f9e0 --- /dev/null +++ b/assets/svg/coin_icons/Bitcoincash.svg @@ -0,0 +1 @@ +bitcoin-cash-bch \ No newline at end of file diff --git a/assets/svg/coin_icons/Namecoin.svg b/assets/svg/coin_icons/Namecoin.svg new file mode 100644 index 000000000..2cda6aaf0 --- /dev/null +++ b/assets/svg/coin_icons/Namecoin.svg @@ -0,0 +1 @@ + \ No newline at end of file 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..0cb47b7ff 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 @@ -113,8 +113,10 @@ class _AddEditNodeViewState extends ConsumerState { break; case Coin.bitcoin: + case Coin.bitcoincash: case Coin.dogecoin: case Coin.firo: + case Coin.namecoin: case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: @@ -527,6 +529,8 @@ class _NodeFormState extends ConsumerState { case Coin.bitcoin: case Coin.dogecoin: case Coin.firo: + case Coin.namecoin: + case Coin.bitcoincash: case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index e1ea02abe..da3bdfed0 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -2250,7 +2250,7 @@ class BitcoinWallet extends CoinServiceAPI { batches[batchNumber] = {}; } final scripthash = _convertToScriptHash(allAddresses[i], _network); - final id = const Uuid().v1(); + final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); requestIdToAddressMap[id] = allAddresses[i]; batches[batchNumber]!.addAll({ id: [scripthash] diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart new file mode 100644 index 000000000..aa83ea4a9 --- /dev/null +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -0,0 +1,3111 @@ +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'; +import 'package:bitbox/bitbox.dart' as Bitbox; + +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 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 0x80: // bch mainnet wif + coinType = "145"; // bch mainnet + break; + case 0xef: // bch testnet wif + coinType = "1"; // bch testnet + 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 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 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; + case Coin.bitcoincashTestnet: + return bitcoincashtestnet; + default: + throw Exception("Bitcoincash network type not set!"); + } + } + + List outputsList = []; + + @override + Coin get coin => _coin; + + @override + Future> get allOwnAddresses => + _allOwnAddresses ??= _fetchAllOwnAddresses(); + Future>? _allOwnAddresses; + + Future? _utxoData; + Future get utxoData => _utxoData ??= _fetchUtxoData(); + + @override + Future> get unspentOutputs async => + (await utxoData).unspentOutputArray; + + @override + Future get availableBalance async { + final data = await utxoData; + return Format.satoshisToAmount( + data.satoshiBalance - data.satoshiBalanceUnconfirmed); + } + + @override + Future get pendingBalance async { + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + } + + @override + Future get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(); + + @override + Future get totalBalance async { + if (!isActive) { + final totalBalance = DB.instance + .get(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 get currentReceivingAddress => + _currentReceivingAddressP2PKH ??= + _getCurrentAddressForChain(0, DerivePathType.bip44); + + Future? _currentReceivingAddressP2PKH; + + @override + Future exit() async { + _hasCalledExit = true; + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } + + bool _hasCalledExit = false; + + @override + bool get hasCalledExit => _hasCalledExit; + + @override + Future get fees => _feeObject ??= _getFees(); + Future? _feeObject; + + @override + Future get maxFee async { + final fee = (await fees).fast; + final satsFee = + Format.satoshisToAmount(fee) * Decimal.fromInt(Constants.satsPerCoin); + return satsFee.floor().toBigInt().toInt(); + } + + @override + Future> get mnemonic => _getMnemonicList(); + + Future 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 get storedChainHeight async { + final storedHeight = DB.instance + .get(boxName: walletId, key: "storedChainHeight") as int?; + return storedHeight ?? 0; + } + + Future updateStoredChainHeight({required int newHeight}) async { + DB.instance.put( + 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 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.bitcoincash: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.bitcoincashTestnet: + 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 bch 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 _recoverWalletFromBIP32SeedPhrase({ + required String mnemonic, + int maxUnusedAddressGap = 20, + int maxNumberOfIndexesToCheck = 1000, + }) async { + longMutex = true; + + Map> p2pkhReceiveDerivations = {}; + Map> p2pkhChangeDerivations = {}; + + final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); + + List p2pkhReceiveAddressArray = []; + int p2pkhReceiveIndex = -1; + + List 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 txCountCallArgs = {}; + final Map 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 args = {}; + final Map 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( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: p2pkhReceiveAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: p2pkhChangeAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: p2pkhReceiveIndex); + await DB.instance.put( + boxName: walletId, key: 'changeIndexP2PKH', value: p2pkhChangeIndex); + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put(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 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 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> allTxs = + await _fetchHistory(allOwnAddresses); + final txData = await transactionData; + for (Map 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 getAllTxsToWatch( + TransactionData txData, + ) async { + if (_hasCalledExit) return; + List unconfirmedTxnsToNotifyPending = []; + List 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 refresh() async { + final bchaddr = Bitbox.Address.toCashAddress(await currentReceivingAddress); + print("bchaddr: $bchaddr ${await currentReceivingAddress}"); + + 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> prepareSend({ + required String address, + required int satoshiAmount, + Map? 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; + } 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 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 send({ + required String toAddress, + required int amount, + Map 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 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 initializeNew() async { + Logging.instance + .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); + + if ((DB.instance.get(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(boxName: walletId, key: "id", value: _walletId), + DB.instance + .put(boxName: walletId, key: "isFavorite", value: false), + ]); + } + + @override + Future initializeExisting() async { + Logging.instance.log("Opening existing ${coin.prettyName} wallet.", + level: LogLevel.Info); + + if ((DB.instance.get(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(boxName: walletId, key: "latest_tx_model") + as TransactionData?; + if (data != null) { + _transactionData = Future(() => data); + } + } + + @override + Future get transactionData => + _transactionData ??= _fetchTransactionData(); + Future? _transactionData; + + @override + bool validateAddress(String address) { + try { + // 0 for bitcoincash: address scheme, 1 for legacy address + final format = Bitbox.Address.detectFormat(address); + print("format $format"); + return true; + } catch (e, s) { + return false; + } + } + + @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 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> _getMnemonicList() async { + final mnemonicString = + await _secureStore.read(key: '${_walletId}_mnemonic'); + if (mnemonicString == null) { + return []; + } + final List data = mnemonicString.split(' '); + return data; + } + + Future 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> _fetchAllOwnAddresses() async { + final List allAddresses = []; + + final receivingAddressesP2PKH = DB.instance.get( + boxName: walletId, key: 'receivingAddressesP2PKH') as List; + final changeAddressesP2PKH = + DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') + as List; + + // 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 _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 _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; + case Coin.bitcoincashTestnet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a BitcoinWallet using a non bitcoin coin type: ${coin.name}"); + } + } + + // 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(boxName: walletId, key: "receivingIndexP2PKH", value: 0); + await DB.instance + .put(boxName: walletId, key: "changeIndexP2PKH", value: 0); + await DB.instance.put( + 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( + boxName: walletId, + key: 'addressBookEntries', + value: {}); + + // 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 _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 _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(boxName: walletId, key: indexKey)) + 1; + await DB.instance + .put(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 _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(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(boxName: walletId, key: chainArray, value: [address]); + } else { + // Make a deep copy of the existing list + final List newArray = []; + addressArray + .forEach((dynamic _address) => newArray.add(_address as String)); + newArray.add(address); // Add the address passed into the method + await DB.instance + .put(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 _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(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> _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.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 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.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": , + /// "wif": , + /// }, + /// addressB : { + /// "pubKey": , + /// "wif": , + /// }, + /// } + Future addDerivations({ + required int chain, + required DerivePathType derivePathType, + required Map 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.from(jsonDecode(derivationsString ?? "{}") as Map); + + // add derivation + derivations.addAll(derivationsToAdd); + + // save derivations + final newReceiveDerivationsString = jsonEncode(derivations); + await _secureStore.write(key: key, value: newReceiveDerivationsString); + } + + Future _fetchUtxoData() async { + final List allAddresses = await _fetchAllOwnAddresses(); + + try { + final fetchedUtxoList = >>[]; + + final Map>> 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); + print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); + 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> 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 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"] = {}; + 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 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 allOutputs = dataModel.unspentOutputArray; + Logging.instance + .log('Outputs fetched: $allOutputs', level: LogLevel.Info); + await _sortOutputs(allOutputs); + await DB.instance.put( + boxName: walletId, key: 'latest_utxo_model', value: dataModel); + await DB.instance.put( + 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(boxName: walletId, key: 'latest_utxo_model'); + + if (latestTxModel == null) { + final emptyModel = { + "total_user_currency": "0.00", + "total_sats": 0, + "total_btc": "0", + "outputArray": [] + }; + 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 _sortOutputs(List utxos) async { + final blockedHashArray = + DB.instance.get(boxName: walletId, key: 'blocked_tx_hashes') + as List?; + final List lst = []; + if (blockedHashArray != null) { + for (var hash in blockedHashArray) { + lst.add(hash as String); + } + } + final labels = + DB.instance.get(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 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> _getBatchTxCount({ + required Map addresses, + }) async { + try { + final Map> args = {}; + print("Address $addresses"); + for (final entry in addresses.entries) { + args[entry.key] = [_convertToScriptHash(entry.value, _network)]; + } + + print("Args ${jsonEncode(args)}"); + + final response = await electrumXClient.getBatchHistory(args: args); + print("Response ${jsonEncode(response)}"); + final Map result = {}; + for (final entry in response.entries) { + result[entry.key] = entry.value.length; + } + print("result ${jsonEncode(result)}"); + return result; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown in _getBatchTxCount(address: $addresses: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future _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(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 _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(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 _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 checkCurrentReceivingAddressesForTransactions() async { + if (Platform.environment["FLUTTER_TEST"] == "true") { + try { + return _checkCurrentReceivingAddressesForTransactions(); + } catch (_) { + rethrow; + } + } + } + + Future _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 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 = []; + 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>> _fetchHistory( + List allAddresses) async { + try { + List> allTxHashes = []; + + final Map>> batches = {}; + final Map 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 = Logger.isTestEnv ? "$i" : 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> allTransactions, String txid) { + for (int i = 0; i < allTransactions.length; i++) { + if (allTransactions[i]["txid"] == txid) { + return true; + } + } + return false; + } + + Future _fetchTransactionData() async { + final List allAddresses = await _fetchAllOwnAddresses(); + + final changeAddressesP2PKH = + DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') + as List; + + final List> allTxHashes = + await _fetchHistory(allAddresses); + + final cachedTransactions = + DB.instance.get(boxName: walletId, key: 'latest_tx_model') + as TransactionData?; + int latestTxnBlockHeight = + DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") + as int? ?? + 0; + + final unconfirmedCachedTransactions = + cachedTransactions?.getAllTransactions() ?? {}; + unconfirmedCachedTransactions + .removeWhere((key, value) => value.confirmedStatus); + + print("CACHED_TRANSACTIONS_IS $cachedTransactions"); + 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> allTransactions = []; + + for (final txHash in allTxHashes) { + Logging.instance.log("bch: $txHash", level: LogLevel.Info); + 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> midSortedArray = []; + + for (final txObject in allTransactions) { + List sendersArray = []; + List 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 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"] = []; + 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 result = {"dateTimeChunks": []}; + final dateArray = []; + + 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"] = >[]; + } + 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( + boxName: walletId, + key: 'storedTxnDataHeight', + value: latestTxnBlockHeight); + await DB.instance.put( + 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? utxos}) async { + Logging.instance + .log("Starting coinSelection ----------", level: LogLevel.Info); + final List availableOutputs = utxos ?? outputsList; + final List 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 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 recipientsArray = [_recipientAddress]; + List 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)) { + feeForOneOutput = (vSizeForOneOutput + 1); + } + + final int amount = satoshiAmountToSend - feeForOneOutput; + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: [amount], + ); + Map 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)) { + feeForOneOutput = (vSizeForOneOutput + 1); + } + if (feeForTwoOutputs < ((vSizeForTwoOutPuts + 1))) { + feeForTwoOutputs = ((vSizeForTwoOutPuts + 1)); + } + + 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 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 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 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 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> fetchBuildTxData( + List utxosToUse, + ) async { + // return data + Map results = {}; + Map> addressTxid = {}; + + // addresses to check + List 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] = []; + } + (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> buildTransaction({ + required List utxosToUse, + required Map utxoSigningData, + required List recipients, + required List satoshiAmounts, + }) async { + final builder = Bitbox.Bitbox.transactionBuilder(); + + // retrieve address' utxos from the rest api + List _utxos = + []; // await Bitbox.Address.utxo(address) as List; + utxosToUse.forEach((element) { + _utxos.add(Bitbox.Utxo( + element.txid, + element.vout, + Bitbox.BitcoinCash.fromSatoshi(element.value), + element.value, + 0, + MINIMUM_CONFIRMATIONS + 1)); + }); + Logger.print("bch utxos: ${_utxos}"); + + // placeholder for input signatures + final signatures = []; + + // placeholder for total input balance + int totalBalance = 0; + + // iterate through the list of address _utxos and use them as inputs for the + // withdrawal transaction + _utxos.forEach((Bitbox.Utxo utxo) { + // add the utxo as an input for the transaction + builder.addInput(utxo.txid, utxo.vout); + final ec = utxoSigningData[utxo.txid]["keyPair"] as ECPair; + + final bitboxEC = Bitbox.ECPair.fromWIF(ec.toWIF()); + + // add a signature to the list to be used later + signatures.add({ + "vin": signatures.length, + "key_pair": bitboxEC, + "original_amount": utxo.satoshis + }); + + totalBalance += utxo.satoshis; + }); + + // calculate the fee based on number of inputs and one expected output + final fee = + Bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length); + + // calculate how much balance will be left over to spend after the fee + final sendAmount = totalBalance - fee; + + // add the output based on the address provided in the testing data + for (int i = 0; i < recipients.length; i++) { + String recipient = recipients[i]; + int satoshiAmount = satoshiAmounts[i]; + builder.addOutput(recipient, satoshiAmount); + } + + // sign all inputs + signatures.forEach((signature) { + builder.sign( + signature["vin"] as int, + signature["key_pair"] as Bitbox.ECPair, + signature["original_amount"] as int); + }); + + // build the transaction + final tx = builder.build(); + final txHex = tx.toHex(); + final vSize = tx.virtualSize(); + Logger.print("bch raw hex: $txHex"); + + return {"hex": txHex, "vSize": vSize}; + } + + @override + Future 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 _rescanRestore() async { + Logging.instance.log("starting rescan restore", level: LogLevel.Info); + + // restore from backup + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2PKH_BACKUP'); + final tempChangeAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'changeAddressesP2PKH_BACKUP'); + final tempReceivingIndexP2PKH = DB.instance + .get(boxName: walletId, key: 'receivingIndexP2PKH_BACKUP'); + final tempChangeIndexP2PKH = DB.instance + .get(boxName: walletId, key: 'changeIndexP2PKH_BACKUP'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: tempReceivingAddressesP2PKH); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: tempChangeAddressesP2PKH); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: tempReceivingIndexP2PKH); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2PKH', + value: tempChangeIndexP2PKH); + await DB.instance.delete( + key: 'receivingAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'changeAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'receivingIndexP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete(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(boxName: walletId, key: 'latest_utxo_model_BACKUP'); + await DB.instance.put( + boxName: walletId, key: 'latest_utxo_model', value: utxoData); + await DB.instance + .delete(key: 'latest_utxo_model_BACKUP', boxName: walletId); + + Logging.instance.log("rescan restore complete", level: LogLevel.Info); + } + + Future _rescanBackup() async { + Logging.instance.log("starting rescan backup", level: LogLevel.Info); + + // backup current and clear data + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2PKH_BACKUP', + value: tempReceivingAddressesP2PKH); + await DB.instance + .delete(key: 'receivingAddressesP2PKH', boxName: walletId); + + final tempChangeAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'changeAddressesP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2PKH_BACKUP', + value: tempChangeAddressesP2PKH); + await DB.instance + .delete(key: 'changeAddressesP2PKH', boxName: walletId); + + final tempReceivingIndexP2PKH = + DB.instance.get(boxName: walletId, key: 'receivingIndexP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2PKH_BACKUP', + value: tempReceivingIndexP2PKH); + await DB.instance + .delete(key: 'receivingIndexP2PKH', boxName: walletId); + + final tempChangeIndexP2PKH = + DB.instance.get(boxName: walletId, key: 'changeIndexP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2PKH_BACKUP', + value: tempChangeIndexP2PKH); + await DB.instance + .delete(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(boxName: walletId, key: 'latest_utxo_model'); + await DB.instance.put( + boxName: walletId, key: 'latest_utxo_model_BACKUP', value: utxoData); + await DB.instance + .delete(key: 'latest_utxo_model', boxName: walletId); + + Logging.instance.log("rescan backup complete", level: LogLevel.Info); + } + + @override + set isFavorite(bool markFavorite) { + DB.instance.put( + boxName: walletId, key: "isFavorite", value: markFavorite); + } + + @override + bool get isFavorite { + try { + return DB.instance.get(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 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 bch? + 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; + } + + @override + Future generateNewAddress() async { + try { + await _incrementAddressIndexForChain( + 0, DerivePathType.bip44); // First increment the receiving index + final newReceivingIndex = DB.instance.get( + boxName: walletId, + key: 'receivingIndexP2PKH') as int; // Check the new receiving index + final newReceivingAddress = await _generateAddressForChain( + 0, + newReceivingIndex, + DerivePathType + .bip44); // Use new index to derive a new receiving address + await _addToAddressesArrayForChain( + newReceivingAddress, + 0, + DerivePathType + .bip44); // Add that new receiving address to the array of receiving addresses + _currentReceivingAddressP2PKH = Future(() => + newReceivingAddress); // Set the new receiving address that the service + + return true; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from generateNewAddress(): $e\n$s", + level: LogLevel.Error); + return false; + } + } +} + +// Bitcoincash Network +final bitcoincash = NetworkType( + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'bc', + bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), + pubKeyHash: 0x00, + scriptHash: 0x05, + wif: 0x80); + +final bitcoincashtestnet = NetworkType( + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'tb', + bip32: Bip32Type(public: 0x043587cf, private: 0x04358394), + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef); diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index bc0e4be28..89231f200 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -8,6 +8,8 @@ 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/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -97,6 +99,26 @@ abstract class CoinServiceAPI { tracker: tracker, ); + case Coin.bitcoincash: + return BitcoinCashWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + client: client, + cachedClient: cachedClient, + tracker: tracker, + ); + + case Coin.bitcoincashTestnet: + return BitcoinCashWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + client: client, + cachedClient: cachedClient, + tracker: tracker, + ); + case Coin.dogecoin: return DogecoinWallet( walletId: walletId, @@ -123,6 +145,16 @@ abstract class CoinServiceAPI { // tracker: tracker, ); + case Coin.namecoin: + return NamecoinWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + tracker: tracker, + cachedClient: cachedClient, + client: client, + ); + case Coin.dogecoinTestNet: return DogecoinWallet( walletId: walletId, diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart new file mode 100644 index 000000000..379fda4e2 --- /dev/null +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -0,0 +1,3809 @@ +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 = 2; +// Find real dust limit +const int DUST_LIMIT = 1000000; + +const String GENESIS_HASH_MAINNET = + "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"; + +enum DerivePathType { bip44, bip49, bip84 } + +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 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 0xb4: // nmc mainnet wif + coinType = "7"; // nmc mainnet + break; + default: + throw Exception("Invalid Namecoin network type used!"); + } + switch (derivePathType) { + case DerivePathType.bip44: + return root.derivePath("m/44'/$coinType'/0'/$chain/$index"); + case DerivePathType.bip49: + return root.derivePath("m/49'/$coinType'/0'/$chain/$index"); + case DerivePathType.bip84: + return root.derivePath("m/84'/$coinType'/0'/$chain/$index"); + default: + throw Exception("DerivePathType must not be null."); + } +} + +/// wrapper for compute() +bip32.BIP32 getBip32NodeFromRootWrapper( + Tuple4 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 args) { + return getBip32Root(args.item1, args.item2); +} + +class NamecoinWallet 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.namecoin: + return namecoin; + default: + throw Exception("Invalid network type!"); + } + } + + List outputsList = []; + + @override + set isFavorite(bool markFavorite) { + DB.instance.put( + boxName: walletId, key: "isFavorite", value: markFavorite); + } + + @override + bool get isFavorite { + try { + return DB.instance.get(boxName: walletId, key: "isFavorite") + as bool; + } catch (e, s) { + Logging.instance + .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + @override + Coin get coin => _coin; + + @override + Future> get allOwnAddresses => + _allOwnAddresses ??= _fetchAllOwnAddresses(); + Future>? _allOwnAddresses; + + Future? _utxoData; + Future get utxoData => _utxoData ??= _fetchUtxoData(); + + @override + Future> get unspentOutputs async => + (await utxoData).unspentOutputArray; + + @override + Future get availableBalance async { + final data = await utxoData; + return Format.satoshisToAmount( + data.satoshiBalance - data.satoshiBalanceUnconfirmed); + } + + @override + Future get pendingBalance async { + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + } + + @override + Future get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(); + + @override + Future get totalBalance async { + if (!isActive) { + final totalBalance = DB.instance + .get(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 get currentReceivingAddress => _currentReceivingAddress ??= + _getCurrentAddressForChain(0, DerivePathType.bip84); + Future? _currentReceivingAddress; + + Future get currentLegacyReceivingAddress => + _currentReceivingAddressP2PKH ??= + _getCurrentAddressForChain(0, DerivePathType.bip44); + Future? _currentReceivingAddressP2PKH; + + Future get currentReceivingAddressP2SH => + _currentReceivingAddressP2SH ??= + _getCurrentAddressForChain(0, DerivePathType.bip49); + Future? _currentReceivingAddressP2SH; + + @override + Future exit() async { + _hasCalledExit = true; + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } + + bool _hasCalledExit = false; + + @override + bool get hasCalledExit => _hasCalledExit; + + @override + Future get fees => _feeObject ??= _getFees(); + Future? _feeObject; + + @override + Future get maxFee async { + final fee = (await fees).fast as String; + final satsFee = Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin); + return satsFee.floor().toBigInt().toInt(); + } + + @override + Future> get mnemonic => _getMnemonicList(); + + Future 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; + } + } + + int get storedChainHeight { + final storedHeight = DB.instance + .get(boxName: walletId, key: "storedChainHeight") as int?; + return storedHeight ?? 0; + } + + Future updateStoredChainHeight({required int newHeight}) async { + await DB.instance.put( + 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; + } + if (decodeBase58[0] == _network.scriptHash) { + // P2SH + return DerivePathType.bip49; + } + throw ArgumentError('Invalid version or Network mismatch'); + } else { + try { + decodeBech32 = segwit.decode(address, namecoin.bech32!); + } 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'); + } + // P2WPKH + return DerivePathType.bip84; + } + } + + bool longMutex = false; + + @override + Future 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.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 NamecoinWallet using a non namecoin coin type: ${coin.name}"); + } + // if (_networkType == BasicNetworkType.main) { + // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + // throw Exception("genesis hash does not match main net!"); + // } + // } else if (_networkType == BasicNetworkType.test) { + // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + // throw Exception("genesis hash does not match test net!"); + // } + // } + } + // 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> _checkGaps( + int maxNumberOfIndexesToCheck, + int maxUnusedAddressGap, + int txCountBatchSize, + bip32.BIP32 root, + DerivePathType type, + int account) async { + List addressArray = []; + int returningIndex = -1; + Map> derivations = {}; + int gapCounter = 0; + for (int index = 0; + index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap; + index += txCountBatchSize) { + List iterationsAddressArray = []; + Logging.instance.log( + "index: $index, \t GapCounter $account ${type.name}: $gapCounter", + level: LogLevel.Info); + + final _id = "k_$index"; + Map txCountCallArgs = {}; + final Map receivingNodes = {}; + + for (int j = 0; j < txCountBatchSize; j++) { + final node = await compute( + getBip32NodeFromRootWrapper, + Tuple4( + account, + index + j, + root, + type, + ), + ); + String? address; + switch (type) { + case DerivePathType.bip44: + address = P2PKH( + data: PaymentData(pubkey: node.publicKey), + network: _network) + .data + .address!; + break; + case DerivePathType.bip49: + address = P2SH( + data: PaymentData( + redeem: P2WPKH( + data: PaymentData(pubkey: node.publicKey), + network: _network, + overridePrefix: namecoin.bech32!) + .data), + network: _network) + .data + .address!; + break; + case DerivePathType.bip84: + address = P2WPKH( + network: _network, + data: PaymentData(pubkey: node.publicKey), + overridePrefix: namecoin.bech32!) + .data + .address!; + break; + default: + throw Exception("No Path type $type exists"); + } + receivingNodes.addAll({ + "${_id}_$j": { + "node": node, + "address": address, + } + }); + txCountCallArgs.addAll({ + "${_id}_$j": address, + }); + } + + // get address tx counts + final counts = await _getBatchTxCount(addresses: txCountCallArgs); + + // check and add appropriate addresses + for (int k = 0; k < txCountBatchSize; k++) { + int count = counts["${_id}_$k"]!; + if (count > 0) { + final node = receivingNodes["${_id}_$k"]; + // add address to array + addressArray.add(node["address"] as String); + iterationsAddressArray.add(node["address"] as String); + // set current index + returningIndex = index + k; + // reset counter + gapCounter = 0; + // add info to derivations + derivations[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 (count == 0) { + gapCounter++; + } + } + // cache all the transactions while waiting for the current function to finish. + unawaited(getTransactionCacheEarly(iterationsAddressArray)); + } + return { + "addressArray": addressArray, + "index": returningIndex, + "derivations": derivations + }; + } + + Future getTransactionCacheEarly(List allAddresses) async { + try { + final List> allTxHashes = + await _fetchHistory(allAddresses); + for (final txHash in allTxHashes) { + try { + unawaited(cachedElectrumXClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + )); + } catch (e) { + continue; + } + } + } catch (e) { + // + } + } + + Future _recoverWalletFromBIP32SeedPhrase({ + required String mnemonic, + int maxUnusedAddressGap = 20, + int maxNumberOfIndexesToCheck = 1000, + }) async { + longMutex = true; + + Map> p2pkhReceiveDerivations = {}; + Map> p2shReceiveDerivations = {}; + Map> p2wpkhReceiveDerivations = {}; + Map> p2pkhChangeDerivations = {}; + Map> p2shChangeDerivations = {}; + Map> p2wpkhChangeDerivations = {}; + + final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); + + List p2pkhReceiveAddressArray = []; + List p2shReceiveAddressArray = []; + List p2wpkhReceiveAddressArray = []; + int p2pkhReceiveIndex = -1; + int p2shReceiveIndex = -1; + int p2wpkhReceiveIndex = -1; + + List p2pkhChangeAddressArray = []; + List p2shChangeAddressArray = []; + List p2wpkhChangeAddressArray = []; + int p2pkhChangeIndex = -1; + int p2shChangeIndex = -1; + int p2wpkhChangeIndex = -1; + + // actual size is 36 due to p2pkh, p2sh, and p2wpkh so 12x3 + const txCountBatchSize = 12; + + try { + // receiving addresses + Logging.instance + .log("checking receiving addresses...", level: LogLevel.Info); + final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); + + final resultReceive49 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 0); + + final resultReceive84 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 0); + + Logging.instance + .log("checking change addresses...", level: LogLevel.Info); + // change addresses + final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); + + final resultChange49 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 1); + + final resultChange84 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 1); + + await Future.wait([ + resultReceive44, + resultReceive49, + resultReceive84, + resultChange44, + resultChange49, + resultChange84 + ]); + + p2pkhReceiveAddressArray = + (await resultReceive44)['addressArray'] as List; + p2pkhReceiveIndex = (await resultReceive44)['index'] as int; + p2pkhReceiveDerivations = (await resultReceive44)['derivations'] + as Map>; + + p2shReceiveAddressArray = + (await resultReceive49)['addressArray'] as List; + p2shReceiveIndex = (await resultReceive49)['index'] as int; + p2shReceiveDerivations = (await resultReceive49)['derivations'] + as Map>; + + p2wpkhReceiveAddressArray = + (await resultReceive84)['addressArray'] as List; + p2wpkhReceiveIndex = (await resultReceive84)['index'] as int; + p2wpkhReceiveDerivations = (await resultReceive84)['derivations'] + as Map>; + + p2pkhChangeAddressArray = + (await resultChange44)['addressArray'] as List; + p2pkhChangeIndex = (await resultChange44)['index'] as int; + p2pkhChangeDerivations = (await resultChange44)['derivations'] + as Map>; + + p2shChangeAddressArray = + (await resultChange49)['addressArray'] as List; + p2shChangeIndex = (await resultChange49)['index'] as int; + p2shChangeDerivations = (await resultChange49)['derivations'] + as Map>; + + p2wpkhChangeAddressArray = + (await resultChange84)['addressArray'] as List; + p2wpkhChangeIndex = (await resultChange84)['index'] as int; + p2wpkhChangeDerivations = (await resultChange84)['derivations'] + as Map>; + + // save the derivations (if any) + if (p2pkhReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhReceiveDerivations); + } + if (p2shReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip49, + derivationsToAdd: p2shReceiveDerivations); + } + if (p2wpkhReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip84, + derivationsToAdd: p2wpkhReceiveDerivations); + } + if (p2pkhChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhChangeDerivations); + } + if (p2shChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip49, + derivationsToAdd: p2shChangeDerivations); + } + if (p2wpkhChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip84, + derivationsToAdd: p2wpkhChangeDerivations); + } + + // 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 (p2shReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip49); + p2shReceiveAddressArray.add(address); + p2shReceiveIndex = 0; + } + if (p2wpkhReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip84); + p2wpkhReceiveAddressArray.add(address); + p2wpkhReceiveIndex = 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; + } + if (p2shChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip49); + p2shChangeAddressArray.add(address); + p2shChangeIndex = 0; + } + if (p2wpkhChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip84); + p2wpkhChangeAddressArray.add(address); + p2wpkhChangeIndex = 0; + } + + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2WPKH', + value: p2wpkhReceiveAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2WPKH', + value: p2wpkhChangeAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: p2pkhReceiveAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: p2pkhChangeAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2SH', + value: p2shReceiveAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2SH', + value: p2shChangeAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2WPKH', + value: p2wpkhReceiveIndex); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2WPKH', + value: p2wpkhChangeIndex); + await DB.instance.put( + boxName: walletId, key: 'changeIndexP2PKH', value: p2pkhChangeIndex); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: p2pkhReceiveIndex); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2SH', + value: p2shReceiveIndex); + await DB.instance.put( + boxName: walletId, key: 'changeIndexP2SH', value: p2shChangeIndex); + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put(boxName: walletId, key: "isFavorite", value: false); + + longMutex = false; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $e\n$s", + level: LogLevel.Error); + + longMutex = false; + rethrow; + } + } + + Future refreshIfThereIsNewData() async { + if (longMutex) return false; + if (_hasCalledExit) return false; + Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); + + try { + bool needsRefresh = false; + Set txnsToCheck = {}; + + for (final String txid in txTracker.pendings) { + if (!txTracker.wasNotifiedConfirmed(txid)) { + txnsToCheck.add(txid); + } + } + + for (String txid in txnsToCheck) { + final txn = await electrumXClient.getTransaction(txHash: txid); + int confirmations = txn["confirmations"] as int? ?? 0; + bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; + if (!isUnconfirmed) { + // unconfirmedTxs = {}; + needsRefresh = true; + break; + } + } + if (!needsRefresh) { + var allOwnAddresses = await _fetchAllOwnAddresses(); + List> allTxs = + await _fetchHistory(allOwnAddresses); + final txData = await transactionData; + for (Map 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.Error); + rethrow; + } + } + + Future getAllTxsToWatch( + TransactionData txData, + ) async { + if (_hasCalledExit) return; + List unconfirmedTxnsToNotifyPending = []; + List unconfirmedTxnsToNotifyConfirmed = []; + + for (final chunk in txData.txChunks) { + for (final tx in chunk.transactions) { + if (tx.confirmedStatus) { + // 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) { + if (tx.txType == "Received") { + unawaited(NotificationApi.showNotification( + title: "Incoming 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); + } else if (tx.txType == "Sent") { + unawaited(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") { + unawaited(NotificationApi.showNotification( + title: "Incoming transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } else if (tx.txType == "Sent") { + unawaited(NotificationApi.showNotification( + title: "Outgoing transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } + } + } + + 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 + bool get isRefreshing => refreshMutex; + + bool refreshMutex = false; + + //TODO Show percentages properly/more consistently + /// Refreshes display data for the wallet + @override + Future 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 + unawaited(updateStoredChainHeight(newHeight: currentHeight)); + } + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); + final changeAddressForTransactions = + _checkChangeAddressForTransactions(DerivePathType.bip84); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); + final currentReceivingAddressesForTransactions = + _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)); + + final allTxsToWatch = getAllTxsToWatch(await newTxData); + await Future.wait([ + newTxData, + changeAddressForTransactions, + currentReceivingAddressesForTransactions, + newUtxoData, + 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); + // 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> prepareSend({ + required String address, + required int satoshiAmount, + Map? 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 txData = + await coinSelection(satoshiAmount, rate, address, isSendAll); + + Logging.instance.log("prepare send: $txData", level: LogLevel.Info); + try { + if (txData is int) { + switch (txData) { + 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 $txData"); + } + } else { + final hex = txData["hex"]; + + if (hex is String) { + final fee = txData["fee"] as int; + final vSize = txData["vSize"] as int; + + Logging.instance + .log("prepared txHex: $hex", level: LogLevel.Info); + Logging.instance.log("prepared fee: $fee", level: LogLevel.Info); + Logging.instance + .log("prepared 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 txData as Map; + } else { + throw Exception("prepared hex is not a String!!!"); + } + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } 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 confirmSend({required Map txData}) async { + try { + Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); + + final hex = txData["hex"] as String; + + final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex); + 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 send({ + required String toAddress, + required int amount, + Map 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 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 initializeNew() async { + Logging.instance + .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); + + if ((DB.instance.get(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(boxName: walletId, key: "id", value: walletId), + DB.instance + .put(boxName: walletId, key: "isFavorite", value: false), + ]); + } + + @override + Future initializeExisting() async { + Logging.instance.log("Opening existing ${coin.prettyName} wallet.", + level: LogLevel.Info); + + if ((DB.instance.get(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(boxName: walletId, key: "latest_tx_model") + as TransactionData?; + if (data != null) { + _transactionData = Future(() => data); + } + } + + @override + Future get transactionData => + _transactionData ??= _fetchTransactionData(); + Future? _transactionData; + + @override + bool validateAddress(String address) { + return Address.validateAddress(address, _network, namecoin.bech32!); + } + + @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; + + NamecoinWallet({ + 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 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) { + unawaited(refresh()); + } + } + + Future> _getMnemonicList() async { + final mnemonicString = + await _secureStore.read(key: '${_walletId}_mnemonic'); + if (mnemonicString == null) { + return []; + } + final List data = mnemonicString.split(' '); + return data; + } + + Future 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> _fetchAllOwnAddresses() async { + final List allAddresses = []; + final receivingAddresses = DB.instance.get( + boxName: walletId, key: 'receivingAddressesP2WPKH') as List; + final changeAddresses = DB.instance.get( + boxName: walletId, key: 'changeAddressesP2WPKH') as List; + final receivingAddressesP2PKH = DB.instance.get( + boxName: walletId, key: 'receivingAddressesP2PKH') as List; + final changeAddressesP2PKH = + DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') + as List; + final receivingAddressesP2SH = DB.instance.get( + boxName: walletId, key: 'receivingAddressesP2SH') as List; + final changeAddressesP2SH = + DB.instance.get(boxName: walletId, key: 'changeAddressesP2SH') + as List; + + for (var i = 0; i < receivingAddresses.length; i++) { + if (!allAddresses.contains(receivingAddresses[i])) { + allAddresses.add(receivingAddresses[i] as String); + } + } + for (var i = 0; i < changeAddresses.length; i++) { + if (!allAddresses.contains(changeAddresses[i])) { + allAddresses.add(changeAddresses[i] as String); + } + } + 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); + } + } + for (var i = 0; i < receivingAddressesP2SH.length; i++) { + if (!allAddresses.contains(receivingAddressesP2SH[i])) { + allAddresses.add(receivingAddressesP2SH[i] as String); + } + } + for (var i = 0; i < changeAddressesP2SH.length; i++) { + if (!allAddresses.contains(changeAddressesP2SH[i])) { + allAddresses.add(changeAddressesP2SH[i] as String); + } + } + return allAddresses; + } + + Future _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 _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.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 NamecoinWallet using a non namecoin 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(boxName: walletId, key: "receivingIndexP2WPKH", value: 0); + await DB.instance + .put(boxName: walletId, key: "changeIndexP2WPKH", value: 0); + await DB.instance + .put(boxName: walletId, key: "receivingIndexP2PKH", value: 0); + await DB.instance + .put(boxName: walletId, key: "changeIndexP2PKH", value: 0); + await DB.instance + .put(boxName: walletId, key: "receivingIndexP2SH", value: 0); + await DB.instance + .put(boxName: walletId, key: "changeIndexP2SH", value: 0); + await DB.instance.put( + 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( + boxName: walletId, + key: 'addressBookEntries', + value: {}); + + // Generate and add addresses to relevant arrays + await Future.wait([ + // P2WPKH + _generateAddressForChain(0, 0, DerivePathType.bip84).then( + (initialReceivingAddressP2WPKH) { + _addToAddressesArrayForChain( + initialReceivingAddressP2WPKH, 0, DerivePathType.bip84); + _currentReceivingAddress = + Future(() => initialReceivingAddressP2WPKH); + }, + ), + _generateAddressForChain(1, 0, DerivePathType.bip84).then( + (initialChangeAddressP2WPKH) => _addToAddressesArrayForChain( + initialChangeAddressP2WPKH, + 1, + DerivePathType.bip84, + ), + ), + + // P2PKH + _generateAddressForChain(0, 0, DerivePathType.bip44).then( + (initialReceivingAddressP2PKH) { + _addToAddressesArrayForChain( + initialReceivingAddressP2PKH, 0, DerivePathType.bip44); + _currentReceivingAddressP2PKH = + Future(() => initialReceivingAddressP2PKH); + }, + ), + _generateAddressForChain(1, 0, DerivePathType.bip44).then( + (initialChangeAddressP2PKH) => _addToAddressesArrayForChain( + initialChangeAddressP2PKH, + 1, + DerivePathType.bip44, + ), + ), + + // P2SH + _generateAddressForChain(0, 0, DerivePathType.bip49).then( + (initialReceivingAddressP2SH) { + _addToAddressesArrayForChain( + initialReceivingAddressP2SH, 0, DerivePathType.bip49); + _currentReceivingAddressP2SH = + Future(() => initialReceivingAddressP2SH); + }, + ), + _generateAddressForChain(1, 0, DerivePathType.bip49).then( + (initialChangeAddressP2SH) => _addToAddressesArrayForChain( + initialChangeAddressP2SH, + 1, + DerivePathType.bip49, + ), + ), + ]); + + // // P2PKH + // _generateAddressForChain(0, 0, DerivePathType.bip44).then( + // (initialReceivingAddressP2PKH) { + // _addToAddressesArrayForChain( + // initialReceivingAddressP2PKH, 0, DerivePathType.bip44); + // this._currentReceivingAddressP2PKH = + // Future(() => initialReceivingAddressP2PKH); + // }, + // ); + // _generateAddressForChain(1, 0, DerivePathType.bip44) + // .then((initialChangeAddressP2PKH) => _addToAddressesArrayForChain( + // initialChangeAddressP2PKH, + // 1, + // DerivePathType.bip44, + // )); + // + // // P2SH + // _generateAddressForChain(0, 0, DerivePathType.bip49).then( + // (initialReceivingAddressP2SH) { + // _addToAddressesArrayForChain( + // initialReceivingAddressP2SH, 0, DerivePathType.bip49); + // this._currentReceivingAddressP2SH = + // Future(() => initialReceivingAddressP2SH); + // }, + // ); + // _generateAddressForChain(1, 0, DerivePathType.bip49) + // .then((initialChangeAddressP2SH) => _addToAddressesArrayForChain( + // initialChangeAddressP2SH, + // 1, + // DerivePathType.bip49, + // )); + + Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); + } + + /// Generates a new internal or external chain address for the wallet using a BIP84, BIP44, or BIP49 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 _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; + case DerivePathType.bip49: + address = P2SH( + data: PaymentData( + redeem: P2WPKH( + data: data, + network: _network, + overridePrefix: namecoin.bech32!) + .data), + network: _network) + .data + .address!; + break; + case DerivePathType.bip84: + address = P2WPKH( + network: _network, data: data, overridePrefix: namecoin.bech32!) + .data + .address!; + break; + } + + // 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 _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; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + + final newIndex = + (DB.instance.get(boxName: walletId, key: indexKey)) + 1; + await DB.instance + .put(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 _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; + case DerivePathType.bip49: + chainArray += "P2SH"; + break; + case DerivePathType.bip84: + chainArray += "P2WPKH"; + break; + } + + final addressArray = + DB.instance.get(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(boxName: walletId, key: chainArray, value: [address]); + } else { + // Make a deep copy of the existing list + final List newArray = []; + addressArray + .forEach((dynamic _address) => newArray.add(_address as String)); + newArray.add(address); // Add the address passed into the method + await DB.instance + .put(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 _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; + case DerivePathType.bip49: + arrayKey += "P2SH"; + break; + case DerivePathType.bip84: + arrayKey += "P2WPKH"; + break; + } + final internalChainArray = + DB.instance.get(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; + case DerivePathType.bip49: + key = "${walletId}_${chainId}DerivationsP2SH"; + break; + case DerivePathType.bip84: + key = "${walletId}_${chainId}DerivationsP2WPKH"; + break; + } + return key; + } + + Future> _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.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 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.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": , + /// "wif": , + /// }, + /// addressB : { + /// "pubKey": , + /// "wif": , + /// }, + /// } + Future addDerivations({ + required int chain, + required DerivePathType derivePathType, + required Map 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.from(jsonDecode(derivationsString ?? "{}") as Map); + + // add derivation + derivations.addAll(derivationsToAdd); + + // save derivations + final newReceiveDerivationsString = jsonEncode(derivations); + await _secureStore.write(key: key, value: newReceiveDerivationsString); + } + + Future _fetchUtxoData() async { + final List allAddresses = await _fetchAllOwnAddresses(); + + try { + final fetchedUtxoList = >>[]; + + final Map>> batches = {}; + const batchSizeMax = 100; + int batchNumber = 0; + for (int i = 0; i < allAddresses.length; i++) { + if (batches[batchNumber] == null) { + batches[batchNumber] = {}; + } + final scripthash = _convertToScriptHash(allAddresses[i], _network); + + print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); + 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> 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 utxo = {}; + final int confirmations = txn["confirmations"] as int? ?? 0; + final bool confirmed = confirmations >= MINIMUM_CONFIRMATIONS; + if (!confirmed) { + satoshiBalancePending += value; + } + + utxo["txid"] = txn["txid"]; + utxo["vout"] = fetchedUtxoList[i][j]["tx_pos"]; + utxo["value"] = value; + + utxo["status"] = {}; + 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 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 allOutputs = dataModel.unspentOutputArray; + Logging.instance + .log('Outputs fetched: $allOutputs', level: LogLevel.Info); + await _sortOutputs(allOutputs); + await DB.instance.put( + boxName: walletId, key: 'latest_utxo_model', value: dataModel); + await DB.instance.put( + 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(boxName: walletId, key: 'latest_utxo_model') + as models.UtxoData?; + + if (latestTxModel == null) { + final emptyModel = { + "total_user_currency": "0.00", + "total_sats": 0, + "total_btc": "0", + "outputArray": [] + }; + return UtxoData.fromJson(emptyModel); + } else { + Logging.instance + .log("Old output model located", level: LogLevel.Warning); + return latestTxModel; + } + } + } + + /// 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 _sortOutputs(List utxos) async { + final blockedHashArray = + DB.instance.get(boxName: walletId, key: 'blocked_tx_hashes') + as List?; + final List lst = []; + if (blockedHashArray != null) { + for (var hash in blockedHashArray) { + lst.add(hash as String); + } + } + final labels = + DB.instance.get(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 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> _getBatchTxCount({ + required Map addresses, + }) async { + try { + final Map> args = {}; + for (final entry in addresses.entries) { + args[entry.key] = [_convertToScriptHash(entry.value, _network)]; + } + final response = await electrumXClient.getBatchHistory(args: args); + + final Map 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 _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; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + final newReceivingIndex = + DB.instance.get(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; + case DerivePathType.bip49: + _currentReceivingAddressP2SH = Future(() => newReceivingAddress); + break; + case DerivePathType.bip84: + _currentReceivingAddress = Future(() => newReceivingAddress); + break; + } + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future _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; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + final newChangeIndex = + DB.instance.get(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); + } + } 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 _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.Error); + rethrow; + } + } + + /// public wrapper because dart can't test private... + Future checkCurrentReceivingAddressesForTransactions() async { + if (Platform.environment["FLUTTER_TEST"] == "true") { + try { + return _checkCurrentReceivingAddressesForTransactions(); + } catch (_) { + rethrow; + } + } + } + + Future _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 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 namecoin address + String _convertToScriptHash(String namecoinAddress, NetworkType network) { + try { + final output = Address.addressToOutputScript( + namecoinAddress, network, namecoin.bech32!); + final hash = sha256.convert(output.toList(growable: false)).toString(); + + final chars = hash.split(""); + final reversedPairs = []; + 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>> _fetchHistory( + List allAddresses) async { + try { + List> allTxHashes = []; + + final Map>> batches = {}; + final Map requestIdToAddressMap = {}; + const batchSizeMax = 100; + 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 = Logger.isTestEnv ? "$i" : 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> allTransactions, String txid) { + for (int i = 0; i < allTransactions.length; i++) { + if (allTransactions[i]["txid"] == txid) { + return true; + } + } + return false; + } + + Future>> fastFetch(List allTxHashes) async { + List> allTransactions = []; + + const futureLimit = 30; + List>> transactionFutures = []; + int currentFutureCount = 0; + for (final txHash in allTxHashes) { + Future> transactionFuture = + cachedElectrumXClient.getTransaction( + txHash: txHash, + verbose: true, + coin: coin, + ); + transactionFutures.add(transactionFuture); + currentFutureCount++; + if (currentFutureCount > futureLimit) { + currentFutureCount = 0; + await Future.wait(transactionFutures); + for (final fTx in transactionFutures) { + final tx = await fTx; + + allTransactions.add(tx); + } + } + } + if (currentFutureCount != 0) { + currentFutureCount = 0; + await Future.wait(transactionFutures); + for (final fTx in transactionFutures) { + final tx = await fTx; + + allTransactions.add(tx); + } + } + return allTransactions; + } + + Future _fetchTransactionData() async { + final List allAddresses = await _fetchAllOwnAddresses(); + + final changeAddresses = DB.instance.get( + boxName: walletId, key: 'changeAddressesP2WPKH') as List; + final changeAddressesP2PKH = + DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') + as List; + final changeAddressesP2SH = + DB.instance.get(boxName: walletId, key: 'changeAddressesP2SH') + as List; + + for (var i = 0; i < changeAddressesP2PKH.length; i++) { + changeAddresses.add(changeAddressesP2PKH[i] as String); + } + for (var i = 0; i < changeAddressesP2SH.length; i++) { + changeAddresses.add(changeAddressesP2SH[i] as String); + } + + final List> allTxHashes = + await _fetchHistory(allAddresses); + + final cachedTransactions = + DB.instance.get(boxName: walletId, key: 'latest_tx_model') + as TransactionData?; + int latestTxnBlockHeight = + DB.instance.get(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); + } + } + } + } + + Set hashes = {}; + for (var element in allTxHashes) { + hashes.add(element['tx_hash'] as String); + } + await fastFetch(hashes.toList()); + List> 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> midSortedArray = []; + + Set vHashes = {}; + for (final txObject in allTransactions) { + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"]![i] as Map; + final prevTxid = input["txid"] as String; + vHashes.add(prevTxid); + } + } + await fastFetch(vHashes.toList()); + + for (final txObject in allTransactions) { + List sendersArray = []; + List 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 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 (changeAddresses.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"] = []; + 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 result = {"dateTimeChunks": []}; + final dateArray = []; + + 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"] = >[]; + } + 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( + boxName: walletId, + key: 'storedTxnDataHeight', + value: latestTxnBlockHeight); + await DB.instance.put( + 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? utxos, + }) async { + Logging.instance + .log("Starting coinSelection ----------", level: LogLevel.Info); + final List availableOutputs = utxos ?? outputsList; + final List 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 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); + + // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray + List recipientsArray = [_recipientAddress]; + List 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, + ); + + final int roughEstimate = + roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + if (feeForOneOutput < roughEstimate) { + feeForOneOutput = roughEstimate; + } + + final int amount = satoshiAmountToSend - feeForOneOutput; + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: [amount], + ); + Map 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.bip84), + ], + satoshiAmounts: [ + satoshiAmountToSend, + satoshisBeingUsed - satoshiAmountToSend - 1 + ], // dust limit is the minimum amount a change output should be + ))["vSize"] as int; + + // Assume 1 output, only for recipient and no change + final feeForOneOutput = estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ); + // Assume 2 outputs, one for recipient and one for change + final feeForTwoOutputs = estimateTxFee( + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ); + + Logging.instance + .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info); + Logging.instance + .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info); + + if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { + if (satoshisBeingUsed - satoshiAmountToSend > + feeForOneOutput + DUST_LIMIT) { + // Here, we know that theoretically, we may be able to include another output(change) but we first need to + // factor in the value of this output in satoshis. + int changeOutputSize = + satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs; + // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and + // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new + // change address. + if (changeOutputSize > DUST_LIMIT && + satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == + feeForTwoOutputs) { + // generate new change address if current change address has been used + await _checkChangeAddressForTransactions(DerivePathType.bip84); + final String newChangeAddress = + await _getCurrentAddressForChain(1, DerivePathType.bip84); + + 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 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 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 DUST_LIMIT sats + // which makes it uneconomical to add to the transaction. Here, we pass data directly to instruct + // the wallet to begin crafting the transaction that the user requested. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map 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 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> fetchBuildTxData( + List utxosToUse, + ) async { + // return data + Map results = {}; + Map> addressTxid = {}; + + // addresses to check + List addressesP2PKH = []; + List addressesP2SH = []; + List addressesP2WPKH = []; + Logging.instance.log("utxos: $utxosToUse", level: LogLevel.Info); + + 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, + ); + Logging.instance.log("tx: ${json.encode(tx)}", + level: LogLevel.Info, printFullLength: true); + + 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] = []; + } + (addressTxid[address] as List).add(txid); + switch (addressType(address: address)) { + case DerivePathType.bip44: + addressesP2PKH.add(address); + break; + case DerivePathType.bip49: + addressesP2SH.add(address); + break; + case DerivePathType.bip84: + addressesP2WPKH.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, + ), + }; + } + } + } + } + } + + // p2sh / bip49 + final p2shLength = addressesP2SH.length; + if (p2shLength > 0) { + final receiveDerivations = await _fetchDerivations( + chain: 0, + derivePathType: DerivePathType.bip49, + ); + final changeDerivations = await _fetchDerivations( + chain: 1, + derivePathType: DerivePathType.bip49, + ); + for (int i = 0; i < p2shLength; i++) { + // receives + final receiveDerivation = receiveDerivations[addressesP2SH[i]]; + // if a match exists it will not be null + if (receiveDerivation != null) { + final p2wpkh = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network, + overridePrefix: namecoin.bech32!) + .data; + + final redeemScript = p2wpkh.output; + + final data = + P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data; + + for (String tx in addressTxid[addressesP2SH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + receiveDerivation["wif"] as String, + network: _network, + ), + "redeemScript": redeemScript, + }; + } + } else { + // if its not a receive, check change + final changeDerivation = changeDerivations[addressesP2SH[i]]; + // if a match exists it will not be null + if (changeDerivation != null) { + final p2wpkh = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network, + overridePrefix: namecoin.bech32!) + .data; + + final redeemScript = p2wpkh.output; + + final data = + P2SH(data: PaymentData(redeem: p2wpkh), network: _network) + .data; + + for (String tx in addressTxid[addressesP2SH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + changeDerivation["wif"] as String, + network: _network, + ), + "redeemScript": redeemScript, + }; + } + } + } + } + } + + // p2wpkh / bip84 + final p2wpkhLength = addressesP2WPKH.length; + if (p2wpkhLength > 0) { + final receiveDerivations = await _fetchDerivations( + chain: 0, + derivePathType: DerivePathType.bip84, + ); + final changeDerivations = await _fetchDerivations( + chain: 1, + derivePathType: DerivePathType.bip84, + ); + + for (int i = 0; i < p2wpkhLength; i++) { + // receives + final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; + // if a match exists it will not be null + if (receiveDerivation != null) { + final data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network, + overridePrefix: namecoin.bech32!) + .data; + + for (String tx in addressTxid[addressesP2WPKH[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[addressesP2WPKH[i]]; + // if a match exists it will not be null + if (changeDerivation != null) { + final data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network, + overridePrefix: namecoin.bech32!) + .data; + + for (String tx in addressTxid[addressesP2WPKH[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> buildTransaction({ + required List utxosToUse, + required Map utxoSigningData, + required List recipients, + required List satoshiAmounts, + }) async { + Logging.instance + .log("Starting buildTransaction ----------", level: LogLevel.Info); + + final txb = TransactionBuilder(network: _network); + txb.setVersion(2); + + // 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, namecoin.bech32!); + } + + // Add transaction output + for (var i = 0; i < recipients.length; i++) { + txb.addOutput(recipients[i], satoshiAmounts[i], namecoin.bech32!); + } + + 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?, + overridePrefix: namecoin.bech32!); + } + } catch (e, s) { + Logging.instance.log("Caught exception while signing transaction: $e\n$s", + level: LogLevel.Error); + rethrow; + } + + final builtTx = txb.build(namecoin.bech32!); + final vSize = builtTx.virtualSize(); + + return {"hex": builtTx.toHex(), "vSize": vSize}; + } + + @override + Future fullRescan( + int maxUnusedAddressGap, + int maxNumberOfIndexesToCheck, + ) async { + Logging.instance.log("Starting full rescan!", level: LogLevel.Info); + longMutex = true; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + // clear cache + await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); + + // back up data + await _rescanBackup(); + + 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 _rescanRestore() async { + Logging.instance.log("starting rescan restore", level: LogLevel.Info); + + // restore from backup + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2PKH_BACKUP'); + final tempChangeAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'changeAddressesP2PKH_BACKUP'); + final tempReceivingIndexP2PKH = DB.instance + .get(boxName: walletId, key: 'receivingIndexP2PKH_BACKUP'); + final tempChangeIndexP2PKH = DB.instance + .get(boxName: walletId, key: 'changeIndexP2PKH_BACKUP'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: tempReceivingAddressesP2PKH); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: tempChangeAddressesP2PKH); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: tempReceivingIndexP2PKH); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2PKH', + value: tempChangeIndexP2PKH); + await DB.instance.delete( + key: 'receivingAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'changeAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'receivingIndexP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'changeIndexP2PKH_BACKUP', boxName: walletId); + + // p2Sh + final tempReceivingAddressesP2SH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2SH_BACKUP'); + final tempChangeAddressesP2SH = DB.instance + .get(boxName: walletId, key: 'changeAddressesP2SH_BACKUP'); + final tempReceivingIndexP2SH = DB.instance + .get(boxName: walletId, key: 'receivingIndexP2SH_BACKUP'); + final tempChangeIndexP2SH = DB.instance + .get(boxName: walletId, key: 'changeIndexP2SH_BACKUP'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2SH', + value: tempReceivingAddressesP2SH); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2SH', + value: tempChangeAddressesP2SH); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2SH', + value: tempReceivingIndexP2SH); + await DB.instance.put( + boxName: walletId, key: 'changeIndexP2SH', value: tempChangeIndexP2SH); + await DB.instance.delete( + key: 'receivingAddressesP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'changeAddressesP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'receivingIndexP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'changeIndexP2SH_BACKUP', boxName: walletId); + + // p2wpkh + final tempReceivingAddressesP2WPKH = DB.instance.get( + boxName: walletId, key: 'receivingAddressesP2WPKH_BACKUP'); + final tempChangeAddressesP2WPKH = DB.instance + .get(boxName: walletId, key: 'changeAddressesP2WPKH_BACKUP'); + final tempReceivingIndexP2WPKH = DB.instance + .get(boxName: walletId, key: 'receivingIndexP2WPKH_BACKUP'); + final tempChangeIndexP2WPKH = DB.instance + .get(boxName: walletId, key: 'changeIndexP2WPKH_BACKUP'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2WPKH', + value: tempReceivingAddressesP2WPKH); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2WPKH', + value: tempChangeAddressesP2WPKH); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2WPKH', + value: tempReceivingIndexP2WPKH); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2WPKH', + value: tempChangeIndexP2WPKH); + await DB.instance.delete( + key: 'receivingAddressesP2WPKH_BACKUP', boxName: walletId); + await DB.instance.delete( + key: 'changeAddressesP2WPKH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'receivingIndexP2WPKH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'changeIndexP2WPKH_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"); + + // P2SH derivations + final p2shReceiveDerivationsString = await _secureStore.read( + key: "${walletId}_receiveDerivationsP2SH_BACKUP"); + final p2shChangeDerivationsString = await _secureStore.read( + key: "${walletId}_changeDerivationsP2SH_BACKUP"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2SH", + value: p2shReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2SH", + value: p2shChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH_BACKUP"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH_BACKUP"); + + // P2WPKH derivations + final p2wpkhReceiveDerivationsString = await _secureStore.read( + key: "${walletId}_receiveDerivationsP2WPKH_BACKUP"); + final p2wpkhChangeDerivationsString = await _secureStore.read( + key: "${walletId}_changeDerivationsP2WPKH_BACKUP"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2WPKH", + value: p2wpkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2WPKH", + value: p2wpkhChangeDerivationsString); + + await _secureStore.delete( + key: "${walletId}_receiveDerivationsP2WPKH_BACKUP"); + await _secureStore.delete( + key: "${walletId}_changeDerivationsP2WPKH_BACKUP"); + + // UTXOs + final utxoData = DB.instance + .get(boxName: walletId, key: 'latest_utxo_model_BACKUP'); + await DB.instance.put( + boxName: walletId, key: 'latest_utxo_model', value: utxoData); + await DB.instance + .delete(key: 'latest_utxo_model_BACKUP', boxName: walletId); + + Logging.instance.log("rescan restore complete", level: LogLevel.Info); + } + + Future _rescanBackup() async { + Logging.instance.log("starting rescan backup", level: LogLevel.Info); + + // backup current and clear data + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2PKH_BACKUP', + value: tempReceivingAddressesP2PKH); + await DB.instance + .delete(key: 'receivingAddressesP2PKH', boxName: walletId); + + final tempChangeAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'changeAddressesP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2PKH_BACKUP', + value: tempChangeAddressesP2PKH); + await DB.instance + .delete(key: 'changeAddressesP2PKH', boxName: walletId); + + final tempReceivingIndexP2PKH = + DB.instance.get(boxName: walletId, key: 'receivingIndexP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2PKH_BACKUP', + value: tempReceivingIndexP2PKH); + await DB.instance + .delete(key: 'receivingIndexP2PKH', boxName: walletId); + + final tempChangeIndexP2PKH = + DB.instance.get(boxName: walletId, key: 'changeIndexP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2PKH_BACKUP', + value: tempChangeIndexP2PKH); + await DB.instance + .delete(key: 'changeIndexP2PKH', boxName: walletId); + + // p2sh + final tempReceivingAddressesP2SH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2SH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2SH_BACKUP', + value: tempReceivingAddressesP2SH); + await DB.instance + .delete(key: 'receivingAddressesP2SH', boxName: walletId); + + final tempChangeAddressesP2SH = + DB.instance.get(boxName: walletId, key: 'changeAddressesP2SH'); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2SH_BACKUP', + value: tempChangeAddressesP2SH); + await DB.instance + .delete(key: 'changeAddressesP2SH', boxName: walletId); + + final tempReceivingIndexP2SH = + DB.instance.get(boxName: walletId, key: 'receivingIndexP2SH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2SH_BACKUP', + value: tempReceivingIndexP2SH); + await DB.instance + .delete(key: 'receivingIndexP2SH', boxName: walletId); + + final tempChangeIndexP2SH = + DB.instance.get(boxName: walletId, key: 'changeIndexP2SH'); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2SH_BACKUP', + value: tempChangeIndexP2SH); + await DB.instance + .delete(key: 'changeIndexP2SH', boxName: walletId); + + // p2wpkh + final tempReceivingAddressesP2WPKH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2WPKH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2WPKH_BACKUP', + value: tempReceivingAddressesP2WPKH); + await DB.instance + .delete(key: 'receivingAddressesP2WPKH', boxName: walletId); + + final tempChangeAddressesP2WPKH = DB.instance + .get(boxName: walletId, key: 'changeAddressesP2WPKH'); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2WPKH_BACKUP', + value: tempChangeAddressesP2WPKH); + await DB.instance + .delete(key: 'changeAddressesP2WPKH', boxName: walletId); + + final tempReceivingIndexP2WPKH = DB.instance + .get(boxName: walletId, key: 'receivingIndexP2WPKH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2WPKH_BACKUP', + value: tempReceivingIndexP2WPKH); + await DB.instance + .delete(key: 'receivingIndexP2WPKH', boxName: walletId); + + final tempChangeIndexP2WPKH = + DB.instance.get(boxName: walletId, key: 'changeIndexP2WPKH'); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2WPKH_BACKUP', + value: tempChangeIndexP2WPKH); + await DB.instance + .delete(key: 'changeIndexP2WPKH', 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"); + + // P2SH derivations + final p2shReceiveDerivationsString = + await _secureStore.read(key: "${walletId}_receiveDerivationsP2SH"); + final p2shChangeDerivationsString = + await _secureStore.read(key: "${walletId}_changeDerivationsP2SH"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2SH_BACKUP", + value: p2shReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2SH_BACKUP", + value: p2shChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH"); + + // P2WPKH derivations + final p2wpkhReceiveDerivationsString = + await _secureStore.read(key: "${walletId}_receiveDerivationsP2WPKH"); + final p2wpkhChangeDerivationsString = + await _secureStore.read(key: "${walletId}_changeDerivationsP2WPKH"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2WPKH_BACKUP", + value: p2wpkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2WPKH_BACKUP", + value: p2wpkhChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2WPKH"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2WPKH"); + + // UTXOs + final utxoData = + DB.instance.get(boxName: walletId, key: 'latest_utxo_model'); + await DB.instance.put( + boxName: walletId, key: 'latest_utxo_model_BACKUP', value: utxoData); + await DB.instance + .delete(key: 'latest_utxo_model', boxName: walletId); + + Logging.instance.log("rescan backup complete", level: LogLevel.Info); + } + + bool isActive = false; + + @override + void Function(bool)? get onIsActiveWalletChanged => + (isActive) => this.isActive = isActive; + + @override + Future 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: Check if this is the correct formula for namecoin + int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (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; + } + + @override + Future generateNewAddress() async { + try { + await _incrementAddressIndexForChain( + 0, DerivePathType.bip84); // First increment the receiving index + final newReceivingIndex = DB.instance.get( + boxName: walletId, + key: 'receivingIndexP2WPKH') as int; // Check the new receiving index + final newReceivingAddress = await _generateAddressForChain( + 0, + newReceivingIndex, + DerivePathType + .bip84); // Use new index to derive a new receiving address + await _addToAddressesArrayForChain( + newReceivingAddress, + 0, + DerivePathType + .bip84); // Add that new receiving address to the array of receiving addresses + _currentReceivingAddress = Future(() => + newReceivingAddress); // Set the new receiving address that the service + + return true; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from generateNewAddress(): $e\n$s", + level: LogLevel.Error); + return false; + } + } +} + +// Namecoin Network +final namecoin = NetworkType( + messagePrefix: '\x18Namecoin Signed Message:\n', + bech32: 'nc', + bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), + pubKeyHash: 0x34, //From 52 + scriptHash: 0x0d, //13 + wif: 0xb4); //from 180 diff --git a/lib/services/price.dart b/lib/services/price.dart index 6a7160576..c79be324f 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -79,7 +79,7 @@ class PriceAPI { Map> result = {}; try { final uri = Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,epic-cash,zcoin,dogecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"); + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"); // final uri = Uri.parse( // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero%2Cbitcoin%2Cepic-cash%2Czcoin%2Cdogecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 805dc64db..dcdee319b 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -5,6 +5,8 @@ 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/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -40,6 +42,8 @@ class AddressUtils { switch (coin) { case Coin.bitcoin: return Address.validateAddress(address, bitcoin); + case Coin.bitcoincash: + return Address.validateAddress(address, bitcoincash); case Coin.dogecoin: return Address.validateAddress(address, dogecoin); case Coin.epicCash: @@ -49,8 +53,12 @@ class AddressUtils { case Coin.monero: return RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || RegExp("[a-zA-Z0-9]{106}").hasMatch(address); + case Coin.namecoin: + return Address.validateAddress(address, namecoin); case Coin.bitcoinTestNet: return Address.validateAddress(address, testnet); + case Coin.bitcoincashTestnet: + return Address.validateAddress(address, bitcoincashtestnet); case Coin.firoTestNet: return Address.validateAddress(address, firoTestNetwork); case Coin.dogecoinTestNet: diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 72ceac3ca..87e3b44ed 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -1,3 +1,4 @@ +import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; abstract class Assets { @@ -105,13 +106,16 @@ class _SVG { String get txExchangeFailed => "assets/svg/tx-exchange-icon-failed.svg"; String get bitcoin => "assets/svg/coin_icons/Bitcoin.svg"; + String get bitcoincash => "assets/svg/coin_icons/Bitcoincash.svg"; String get dogecoin => "assets/svg/coin_icons/Dogecoin.svg"; String get epicCash => "assets/svg/coin_icons/EpicCash.svg"; String get firo => "assets/svg/coin_icons/Firo.svg"; String get monero => "assets/svg/coin_icons/Monero.svg"; + String get namecoin => "assets/svg/coin_icons/Namecoin.svg"; // TODO provide proper assets String get bitcoinTestnet => "assets/svg/coin_icons/Bitcoin.svg"; + String get bitcoincashTestnet => "assets/svg/coin_icons/Bitcoincash.svg"; String get firoTestnet => "assets/svg/coin_icons/Firo.svg"; String get dogecoinTestnet => "assets/svg/coin_icons/Dogecoin.svg"; @@ -119,6 +123,8 @@ class _SVG { switch (coin) { case Coin.bitcoin: return bitcoin; + case Coin.bitcoincash: + return bitcoincash; case Coin.dogecoin: return dogecoin; case Coin.epicCash: @@ -127,8 +133,12 @@ class _SVG { return firo; case Coin.monero: return monero; + case Coin.namecoin: + return namecoin; case Coin.bitcoinTestNet: return bitcoinTestnet; + case Coin.bitcoincashTestnet: + return bitcoinTestnet; case Coin.firoTestNet: return firoTestnet; case Coin.dogecoinTestNet: @@ -148,22 +158,30 @@ 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/bitcoincash.png"; + String get namecoin => "assets/images/namecoin.png"; String imageFor({required Coin coin}) { switch (coin) { case Coin.bitcoin: case Coin.bitcoinTestNet: return bitcoin; + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + return bitcoincash; case Coin.dogecoin: case Coin.dogecoinTestNet: return dogecoin; case Coin.epicCash: return epicCash; case Coin.firo: + return firo; case Coin.firoTestNet: return firo; case Coin.monero: return monero; + case Coin.namecoin: + return namecoin; } } } diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index a9b3d5db5..78afe5f7f 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -22,5 +22,9 @@ 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://blockchair.com/bitcoin-cash/transaction/$txid"); + case Coin.namecoin: + return Uri.parse("https://chainz.cryptoid.info/nmc/tx.dws?$txid.htm"); } } diff --git a/lib/utilities/cfcolors.dart b/lib/utilities/cfcolors.dart index 741ca1230..fefc9522a 100644 --- a/lib/utilities/cfcolors.dart +++ b/lib/utilities/cfcolors.dart @@ -6,16 +6,21 @@ class _CoinThemeColor { const _CoinThemeColor(); Color get bitcoin => const Color(0xFFFCC17B); + Color get bitcoincash => const Color(0xFFFCC17B); Color get firo => const Color(0xFFFF897A); Color get dogecoin => const Color(0xFFFFE079); Color get epicCash => const Color(0xFFC5C7CB); Color get monero => const Color(0xFFFF9E6B); + Color get namecoin => const Color(0xFFFCC17B); Color forCoin(Coin coin) { switch (coin) { case Coin.bitcoin: case Coin.bitcoinTestNet: return bitcoin; + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + return bitcoincash; case Coin.dogecoin: case Coin.dogecoinTestNet: return dogecoin; @@ -26,6 +31,8 @@ class _CoinThemeColor { return firo; case Coin.monero: return monero; + case Coin.namecoin: + return namecoin; } } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 3b82cde40..0025c8cf8 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -39,12 +39,14 @@ abstract class Constants { final List values = []; switch (coin) { case Coin.bitcoin: + case Coin.bitcoincash: case Coin.dogecoin: case Coin.firo: case Coin.bitcoinTestNet: case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: + case Coin.namecoin: values.addAll([24, 21, 18, 15, 12]); break; @@ -62,6 +64,10 @@ abstract class Constants { case Coin.bitcoinTestNet: return 600; + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + return 600; + case Coin.dogecoin: case Coin.dogecoinTestNet: return 60; @@ -75,6 +81,9 @@ abstract class Constants { case Coin.monero: return 120; + + case Coin.namecoin: + return 600; } } diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index d2e042439..666a16032 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; abstract class DefaultNodes { @@ -13,6 +14,8 @@ abstract class DefaultNodes { firo, monero, epicCash, + bitcoincash, + namecoin, bitcoinTestnet, dogecoinTestnet, firoTestnet, @@ -30,6 +33,18 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get bitcoincash => NodeModel( + host: "electrum1.cipig.net", + port: 20055, + name: defaultName, + id: _nodeId(Coin.bitcoincash), + useSSL: true, + enabled: true, + coinName: Coin.bitcoincash.name, + isFailover: true, + isDown: false, + ); + static NodeModel get dogecoin => NodeModel( host: "dogecoin.stackwallet.com", port: 50022, @@ -80,6 +95,18 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get namecoin => NodeModel( + host: "46.229.238.187", + port: 57002, + name: defaultName, + id: _nodeId(Coin.namecoin), + useSSL: true, + enabled: true, + coinName: Coin.namecoin.name, + isFailover: true, + isDown: false, + ); + static NodeModel get bitcoinTestnet => NodeModel( host: "electrumx-testnet.cypherstack.com", port: 51002, @@ -116,11 +143,26 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get bitcoincashTestnet => NodeModel( + host: "testnet.hsmiths.com", + port: 53012, + name: defaultName, + id: _nodeId(Coin.bitcoincash), + useSSL: true, + enabled: true, + coinName: Coin.bitcoincash.name, + isFailover: true, + isDown: false, + ); + static NodeModel getNodeFor(Coin coin) { switch (coin) { case Coin.bitcoin: return bitcoin; + case Coin.bitcoincash: + return bitcoincash; + case Coin.dogecoin: return dogecoin; @@ -133,9 +175,15 @@ abstract class DefaultNodes { case Coin.monero: return monero; + case Coin.namecoin: + return namecoin; + case Coin.bitcoinTestNet: return bitcoinTestnet; + case Coin.bitcoincashTestnet: + return bitcoincashTestnet; + case Coin.firoTestNet: return firoTestnet; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 9e489dde4..c39761f54 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -5,19 +5,26 @@ 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; +import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' + as nmc; enum Coin { bitcoin, + bitcoincash, dogecoin, epicCash, firo, monero, + namecoin, /// /// /// bitcoinTestNet, + bitcoincashTestnet, dogecoinTestNet, firoTestNet, } @@ -30,6 +37,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "Bitcoin"; + case Coin.bitcoincash: + return "Bitcoin Cash"; case Coin.dogecoin: return "Dogecoin"; case Coin.epicCash: @@ -38,8 +47,12 @@ extension CoinExt on Coin { return "Firo"; case Coin.monero: return "Monero"; + case Coin.namecoin: + return "Namecoin"; case Coin.bitcoinTestNet: return "tBitcoin"; + case Coin.bitcoincashTestnet: + return "tBitcoincash"; case Coin.firoTestNet: return "tFiro"; case Coin.dogecoinTestNet: @@ -51,6 +64,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "BTC"; + case Coin.bitcoincash: + return "BCH"; case Coin.dogecoin: return "DOGE"; case Coin.epicCash: @@ -59,8 +74,12 @@ extension CoinExt on Coin { return "FIRO"; case Coin.monero: return "XMR"; + case Coin.namecoin: + return "NMC"; case Coin.bitcoinTestNet: return "tBTC"; + case Coin.bitcoincashTestnet: + return "tBCH"; case Coin.firoTestNet: return "tFIRO"; case Coin.dogecoinTestNet: @@ -72,6 +91,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "bitcoin"; + case Coin.bitcoincash: + return "bitcoincash"; case Coin.dogecoin: return "dogecoin"; case Coin.epicCash: @@ -81,8 +102,12 @@ extension CoinExt on Coin { return "firo"; case Coin.monero: return "monero"; + case Coin.namecoin: + return "namecoin"; case Coin.bitcoinTestNet: return "bitcoin"; + case Coin.bitcoincashTestnet: + return "bitcoincash"; case Coin.firoTestNet: return "firo"; case Coin.dogecoinTestNet: @@ -93,9 +118,12 @@ extension CoinExt on Coin { bool get isElectrumXCoin { switch (this) { case Coin.bitcoin: + case Coin.bitcoincash: case Coin.dogecoin: case Coin.firo: + case Coin.namecoin: case Coin.bitcoinTestNet: + case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: return true; @@ -112,6 +140,10 @@ extension CoinExt on Coin { case Coin.bitcoinTestNet: return btc.MINIMUM_CONFIRMATIONS; + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + return bch.MINIMUM_CONFIRMATIONS; + case Coin.firo: case Coin.firoTestNet: return firo.MINIMUM_CONFIRMATIONS; @@ -125,6 +157,8 @@ extension CoinExt on Coin { case Coin.monero: return xmr.MINIMUM_CONFIRMATIONS; + case Coin.namecoin: + return nmc.MINIMUM_CONFIRMATIONS; } } } @@ -134,6 +168,10 @@ Coin coinFromPrettyName(String name) { case "Bitcoin": case "bitcoin": return Coin.bitcoin; + case "Bitcoincash": + case "bitcoincash": + case "Bitcoin Cash": + return Coin.bitcoincash; case "Dogecoin": case "dogecoin": return Coin.dogecoin; @@ -146,10 +184,18 @@ Coin coinFromPrettyName(String name) { case "Monero": case "monero": return Coin.monero; + case "Namecoin": + case "namecoin": + return Coin.namecoin; case "Bitcoin Testnet": case "tBitcoin": case "bitcoinTestNet": return Coin.bitcoinTestNet; + + case "Bitcoincash Testnet": + case "tBitcoincash": + case "Bitcoin Cash Testnet": + return Coin.bitcoincashTestnet; case "Firo Testnet": case "tFiro": case "firoTestNet": @@ -168,6 +214,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { switch (ticker.toLowerCase()) { case "btc": return Coin.bitcoin; + case "bch": + return Coin.bitcoincash; case "doge": return Coin.dogecoin; case "epic": @@ -176,8 +224,12 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.firo; case "xmr": return Coin.monero; + case "nmc": + return Coin.namecoin; case "tbtc": return Coin.bitcoinTestNet; + case "tbch": + return Coin.bitcoincashTestnet; case "tfiro": return Coin.firoTestNet; case "tdoge": diff --git a/pubspec.yaml b/pubspec.yaml index 0150e5728..89ac8230e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,7 +46,7 @@ dependencies: bitcoindart: git: url: https://github.com/cypherstack/bitcoindart.git - ref: a35968c2d2d900e77baa9f8b28c89b722c074039 + ref: 65eb920719c8f7895c5402a07497647e7fc4b346 stack_wallet_backup: git: @@ -69,8 +69,15 @@ dependencies: git: url: https://github.com/cypherstack/stack-bip39.git ref: 3bef5acc21340f3cc78df0ad1dce5868a3ed68a5 + bitbox: + git: + url: https://github.com/Quppy/bitbox-flutter.git + ref: ea65073efbaf395a5557e8cd7bd72f195cd7eb11 bip32: ^2.0.0 - bech32: ^0.2.1 + bech32: + git: + url: https://github.com/cypherstack/bech32.git + ref: 22279d4bb24ed541b431acd269a1bc50af0f36a0 bs58check: ^1.0.2 # Storage plugins @@ -181,6 +188,8 @@ flutter: - assets/images/doge.png - assets/images/bitcoin.png - assets/images/epic-cash.png + - assets/images/bitcoincash.png + - assets/images/namecoin.png - assets/svg/plus.svg - assets/svg/gear.svg - assets/svg/bell.svg @@ -261,10 +270,12 @@ flutter: - assets/svg/tx-icon-anonymize-failed.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg + - assets/svg/coin_icons/Bitcoincash.svg - assets/svg/coin_icons/Dogecoin.svg - assets/svg/coin_icons/EpicCash.svg - assets/svg/coin_icons/Firo.svg - assets/svg/coin_icons/Monero.svg + - assets/svg/coin_icons/Namecoin.svg # lottie animations - assets/lottie/test.json - assets/lottie/test2.json diff --git a/test/services/coins/bitcoincash/bitcoincash_history_sample_data.dart b/test/services/coins/bitcoincash/bitcoincash_history_sample_data.dart new file mode 100644 index 000000000..42312585e --- /dev/null +++ b/test/services/coins/bitcoincash/bitcoincash_history_sample_data.dart @@ -0,0 +1,120 @@ +final Map> historyBatchArgs0 = { + "k_0_0": ["4061323fc54ad0fd2fb6d3fd3af583068d7a733f562242a71e00ea7a82fb482b"], + "k_0_1": ["04818da846fe5e03ac993d2e0c1ccc3848ff6073c3aba6a572df4efc5432ae8b"], + "k_0_2": ["a0345933dd4146905a279f9aa35c867599fec2c52993a8f5da3a477acd0ebcfc"], + "k_0_3": ["607bc74daf946bfd9d593606f4393e44555a3dd0b529ddd08a0422be7955912e"], + "k_0_4": ["449dfb82e6f09f7e190f21fe63aaad5ccb854ba1f44f0a6622f6d71fff19fc63"], + "k_0_5": ["3643e3fe26e0b08dcbc89c47efce3b3264f361160341e3c2a6c73681dde12d39"], + "k_0_6": ["6daca5039b35adcbe62441b68eaaa48e9b0a806ab5a34314bd394b9b5c9289e5"], + "k_0_7": ["113f3d214f202795fdc3dccc6942395812270e787abb88fe4ddfa14f33d62d6f"], + "k_0_8": ["5dea575b85959647509d2ab3c92cda3776a4deba444486a7925ae3b71306e7e3"], + "k_0_9": ["5e2e6d3b43dfa29fabf66879d9ba67e4bb2f9f7ed10cfbb75e0b445eb4b84287"], + "k_0_10": [ + "1bfe42869b6b1e5efa1e1b47f382615e3d27e3e66e9cc8ae46b71ece067b4d37" + ], + "k_0_11": ["e0b38e944c5343e67c807a334fcf4b6563a6311447c99a105a0cf2cc3594ad11"] +}; + +final Map> historyBatchArgs1 = { + "k_0_0": ["50550ac9d45b7484b41e32751326127f3e121354e3bceead3e5fd020c94c4fe1"], + "k_0_1": ["f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34"], + "k_0_2": ["f729a8b3d47b265bf78ee78216174f3f5ef44aedfebf2d3224f1afadcfd6b52b"], + "k_0_3": ["82f5da8c4d26af2898dbb947c6afb83b5ad92e609345f1b89819293dd7714c75"], + "k_0_4": ["b4d6bf5639a8cd368772c26da95173940510618023e8952eb8db70aeb1d59cd2"], + "k_0_5": ["12e0f3cb2bf44b80f3c34cfd3fadc2a39de2f4776bc2be5b7100126db1238983"], + "k_0_6": ["ed5351a1e390d6635fa1ccf594998eb82fa627caf93541f3d5f1021b90e75ec7"], + "k_0_7": ["97917c094ec3afcd1b41338e7c06774b2f76c7a430e486c0080a86a141f39723"], + "k_0_8": ["58f96c6274cd3b74d362a30778497cef65f0c657ce94bb8b274b802e47876e3c"], + "k_0_9": ["99fb86f164906c621a42ee2b224972b3ea8ce10dbc1bccecbbdb1a7582e2954a"], + "k_0_10": [ + "555b8d6a03d2b93c381d2cda19fac11034bf5128ccbcbe5ff46b87f17969b4cb" + ], + "k_0_11": ["9d0163f011c1259568c188c4770606b25c823f8b76bbd262c1c7f3095ed24620"] +}; + +final Map>> historyBatchResponse = { + "k_0_0": [], + "s_0_0": [{}, {}], + "w_0_0": [], + "k_0_1": [{}], + "s_0_1": [], + "w_0_1": [{}, {}, {}], + "k_0_2": [], + "s_0_2": [], + "w_0_2": [], + "k_0_3": [], + "s_0_3": [], + "w_0_3": [], + "k_0_4": [], + "s_0_4": [], + "w_0_4": [], + "k_0_5": [], + "s_0_5": [], + "w_0_5": [], + "k_0_6": [], + "s_0_6": [], + "w_0_6": [], + "k_0_7": [], + "s_0_7": [], + "w_0_7": [], + "k_0_8": [], + "s_0_8": [], + "w_0_8": [], + "k_0_9": [], + "s_0_9": [], + "w_0_9": [], + "k_0_10": [], + "s_0_10": [], + "w_0_10": [], + "k_0_11": [], + "s_0_11": [], + "w_0_11": [] +}; + +final Map>> emptyHistoryBatchResponse = { + "k_0_0": [], + "s_0_0": [], + "w_0_0": [], + "k_0_1": [], + "s_0_1": [], + "w_0_1": [], + "k_0_2": [], + "s_0_2": [], + "w_0_2": [], + "k_0_3": [], + "s_0_3": [], + "w_0_3": [], + "k_0_4": [], + "s_0_4": [], + "w_0_4": [], + "k_0_5": [], + "s_0_5": [], + "w_0_5": [], + "k_0_6": [], + "s_0_6": [], + "w_0_6": [], + "k_0_7": [], + "s_0_7": [], + "w_0_7": [], + "k_0_8": [], + "s_0_8": [], + "w_0_8": [], + "k_0_9": [], + "s_0_9": [], + "w_0_9": [], + "k_0_10": [], + "s_0_10": [], + "w_0_10": [], + "k_0_11": [], + "s_0_11": [], + "w_0_11": [] +}; + +final List activeScriptHashes = [ + "11663d093cb17dfbed4a96d148b22d3e094b31d23c639c2814beb79f2ab0ca75", + "06593b2d896751e8dda288bb6587b6bb6a1dee71d82a85457f5654f781e37b12", + "a328ae88ebce63c0010709ae900c199df2b585cdebce53a6291886dfdcc28c63", + "26f92666caebb9a17b14f5b573b385348cdc80065472b8961091f3226d2f650f", + "2f18558e5d3015cb6578aee1c3e4b645725fa4e1d26ce22cb31c9949f3b4957c", + "bf5a6c56814e80eed11e1e459801515f8c2b83da812568aa9dc26e6356f6965b", +]; diff --git a/test/services/coins/bitcoincash/bitcoincash_transaction_data_samples.dart b/test/services/coins/bitcoincash/bitcoincash_transaction_data_samples.dart new file mode 100644 index 000000000..148818b37 --- /dev/null +++ b/test/services/coins/bitcoincash/bitcoincash_transaction_data_samples.dart @@ -0,0 +1,375 @@ +import 'package:stackwallet/models/paymint/transactions_model.dart'; + +final transactionData = TransactionData.fromMap({ + "61fedb3cb994917d2852191785ab59cb0d177d55d860bf10fd671f6a0a83247c": tx1, + "9765cc2efa42ffdfb5ec86e6ae043de4cf2158a00ed19a182c4244bc548334ba": tx2, + "070b45d901243b5856a0cccce8c5f5f548c19aaa00cb0059b37a6a9a3632288a": tx3, + "84aecde036ebe013aa3bd2fcb4741db504c7c040d34f7c33732c967646991855": tx4, +}); + +final tx1 = Transaction( + txid: "61fedb3cb994917d2852191785ab59cb0d177d55d860bf10fd671f6a0a83247c", + confirmedStatus: true, + confirmations: 187, + txType: "Received", + amount: 7000000, + fees: 742, + height: 756720, + address: "12QZH44735UHWAXFgb4hfdq756GjzXtZG7", + timestamp: 1662544771, + worthNow: "0.00", + worthAtBlockTimestamp: "0.00", + inputSize: 1, + outputSize: 2, + inputs: [ + Input( + txid: "f716d010786225004b41e35dd5eebfb11a4e5ea116e1a48235e5d3a591650732", + vout: 0, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "12QZH44735UHWAXFgb4hfdq756GjzXtZG7", + value: 7000000, + ), + Output( + scriptpubkeyAddress: "3E1n17NnhVmWTGNbvH6ffKVNFjYT4Jke7G", + value: 71445709, + ) + ], +); + +final tx2 = Transaction( + txid: "9765cc2efa42ffdfb5ec86e6ae043de4cf2158a00ed19a182c4244bc548334ba", + confirmedStatus: true, + confirmations: 175, + txType: "Sent", + amount: 3000000, + fees: 227000, + height: 756732, + address: "1DPrEZBKKVG1Pf4HXeuCkw2Xk65EunR7CM", + timestamp: 1662553616, + worthNow: "0.00", + worthAtBlockTimestamp: "0.0", + inputSize: 1, + outputSize: 2, + inputs: [ + Input( + txid: "61fedb3cb994917d2852191785ab59cb0d177d55d860bf10fd671f6a0a83247c", + vout: 0, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "1DPrEZBKKVG1Pf4HXeuCkw2Xk65EunR7CM", + value: 3000000, + ), + Output( + scriptpubkeyAddress: "16GbR1Xau2hKFTr1STgB39NbP8CEkGZjYG", + value: 3773000, + ), + ], +); + +final tx3 = Transaction( + txid: "070b45d901243b5856a0cccce8c5f5f548c19aaa00cb0059b37a6a9a3632288a", + confirmedStatus: true, + confirmations: 177, + txType: "Received", + amount: 2000000, + fees: 227, + height: 756738, + address: "1DPrEZBKKVG1Pf4HXeuCkw2Xk65EunR7CM", + timestamp: 1662555788, + worthNow: "0.00", + worthAtBlockTimestamp: "0.0", + inputSize: 1, + outputSize: 2, + inputs: [ + Input( + txid: "9765cc2efa42ffdfb5ec86e6ae043de4cf2158a00ed19a182c4244bc548334ba", + vout: 0, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "1DPrEZBKKVG1Pf4HXeuCkw2Xk65EunR7CM", + value: 2000000, + ), + Output( + scriptpubkeyAddress: "16GbR1Xau2hKFTr1STgB39NbP8CEkGZjYG", + value: 1772773, + ), + ], +); + +final tx4 = Transaction( + txid: "84aecde036ebe013aa3bd2fcb4741db504c7c040d34f7c33732c967646991855", + confirmedStatus: false, + confirmations: 0, + txType: "Received", + amount: 4000000, + fees: 400, + height: 757303, + address: "1PQaBto5KmiW3R2YeexYYoDWksMpEvhYZE", + timestamp: 1662893734, + worthNow: "0.00", + worthAtBlockTimestamp: "0.00", + inputSize: 1, + outputSize: 2, + inputs: [ + Input( + txid: "070b45d901243b5856a0cccce8c5f5f548c19aaa00cb0059b37a6a9a3632288a", + vout: 0, + ), + Input( + txid: "9765cc2efa42ffdfb5ec86e6ae043de4cf2158a00ed19a182c4244bc548334ba", + vout: 0, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "1JHcZyhgctuDCznjkxR51pQzKEJUujuc2j", + value: 999600, + ), + Output( + scriptpubkeyAddress: "1PQaBto5KmiW3R2YeexYYoDWksMpEvhYZE", + value: 4000000, + ) + ], +); + +final tx1Raw = { + "in_mempool": false, + "in_orphanpool": false, + "txid": "61fedb3cb994917d2852191785ab59cb0d177d55d860bf10fd671f6a0a83247c", + "size": 372, + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": + "f716d010786225004b41e35dd5eebfb11a4e5ea116e1a48235e5d3a591650732", + "vout": 1, + "scriptSig": { + "asm": + "0 3045022100d80e1d056e8787d7fac8e59ce14d56a2dbb2aceb43da1fee47e687e318049abd02204bb06be6e8af85250b93e0f5377da535557176557563a4d0121b607ffbf3e7c1[ALL|FORKID] 304402200c528edd5f1c0aa169178f5a4c1ec5044559326f1608db6987398bdc0761aaae02205a94bb7f8dac69400823a0093e0303eaa2905b9fadbb8bbb111c3fef0a452ef0[ALL|FORKID] 522103ff1450283f08568acdb4d5f569f32e4cd4d8c1960ea049a205436f69f9916df8210230ee6aec65bc0db7e9cf507b33067f681047e180e91906e1fde1bb549f233b24210366058482ecccb47075be9d1d3edb46df331c04fa5126cd6fd9dc6cee071237b453ae", + "hex": + "00483045022100d80e1d056e8787d7fac8e59ce14d56a2dbb2aceb43da1fee47e687e318049abd02204bb06be6e8af85250b93e0f5377da535557176557563a4d0121b607ffbf3e7c14147304402200c528edd5f1c0aa169178f5a4c1ec5044559326f1608db6987398bdc0761aaae02205a94bb7f8dac69400823a0093e0303eaa2905b9fadbb8bbb111c3fef0a452ef0414c69522103ff1450283f08568acdb4d5f569f32e4cd4d8c1960ea049a205436f69f9916df8210230ee6aec65bc0db7e9cf507b33067f681047e180e91906e1fde1bb549f233b24210366058482ecccb47075be9d1d3edb46df331c04fa5126cd6fd9dc6cee071237b453ae" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.07, + "n": 0, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 0f6ca2ddb50a473f809440f77d3d931335ac2940 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9140f6ca2ddb50a473f809440f77d3d931335ac294088ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["12QZH44735UHWAXFgb4hfdq756GjzXtZG7"] + } + }, + { + "value": 0.71445709, + "n": 1, + "scriptPubKey": { + "asm": "OP_HASH160 872dcab340b7a8500b2585781e51e9217f11dced OP_EQUAL", + "hex": "a914872dcab340b7a8500b2585781e51e9217f11dced87", + "reqSigs": 1, + "type": "scripthash", + "addresses": ["3E1n17NnhVmWTGNbvH6ffKVNFjYT4Jke7G"] + } + } + ], + "blockhash": + "00000000000000000529d5816d2f9c97cfbe8c06bb87e9a15d9e778281ff9225", + "confirmations": 187, + "time": 1662544771, + "blocktime": 1662544771, + "hex": + "010000000132076591a5d3e53582a4e116a15e4e1ab1bfeed55de3414b0025627810d016f701000000fdfd0000483045022100d80e1d056e8787d7fac8e59ce14d56a2dbb2aceb43da1fee47e687e318049abd02204bb06be6e8af85250b93e0f5377da535557176557563a4d0121b607ffbf3e7c14147304402200c528edd5f1c0aa169178f5a4c1ec5044559326f1608db6987398bdc0761aaae02205a94bb7f8dac69400823a0093e0303eaa2905b9fadbb8bbb111c3fef0a452ef0414c69522103ff1450283f08568acdb4d5f569f32e4cd4d8c1960ea049a205436f69f9916df8210230ee6aec65bc0db7e9cf507b33067f681047e180e91906e1fde1bb549f233b24210366058482ecccb47075be9d1d3edb46df331c04fa5126cd6fd9dc6cee071237b453aeffffffff02c0cf6a00000000001976a9140f6ca2ddb50a473f809440f77d3d931335ac294088accd2c42040000000017a914872dcab340b7a8500b2585781e51e9217f11dced8700000000" +}; + +final tx2Raw = { + "in_mempool": false, + "in_orphanpool": false, + "txid": "9765cc2efa42ffdfb5ec86e6ae043de4cf2158a00ed19a182c4244bc548334ba", + "size": 225, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": + "61fedb3cb994917d2852191785ab59cb0d177d55d860bf10fd671f6a0a83247c", + "vout": 0, + "scriptSig": { + "asm": + "304402207b3301ec0ab0c7dbba32690b71e369a6872ff2d0c0cacaddc831bb04d22cef7102206e0bb6d039c408e301d49978aa34597f57d075b9a69e1a9f120e08af159b167a[ALL|FORKID] 02cb6cdf3e5758112206b4b02f21838b3d8a26c601a88030f3c5705e357d8e4ea8", + "hex": + "47304402207b3301ec0ab0c7dbba32690b71e369a6872ff2d0c0cacaddc831bb04d22cef7102206e0bb6d039c408e301d49978aa34597f57d075b9a69e1a9f120e08af159b167a412102cb6cdf3e5758112206b4b02f21838b3d8a26c601a88030f3c5705e357d8e4ea8" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.03, + "n": 0, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 87f3c240183ef0f3efe4b056029dd16d3e3d5d4f OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a91487f3c240183ef0f3efe4b056029dd16d3e3d5d4f88ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["1DPrEZBKKVG1Pf4HXeuCkw2Xk65EunR7CM"] + } + }, + { + "value": 0.03773, + "n": 1, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 7fa80c90c0f8aa021074702c06b3300c0b247244 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9147fa80c90c0f8aa021074702c06b3300c0b24724488ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["1Cdz8cpH3ZRuZViuah32YaTkNGryCS3DZj"] + } + } + ], + "blockhash": + "000000000000000005d25c8d3722e4486c486bbf864f9261631993afab557832", + "confirmations": 175, + "time": 1662553616, + "blocktime": 1662553616, + "hex": + "02000000017c24830a6a1f67fd10bf60d8557d170dcb59ab85171952287d9194b93cdbfe61000000006a47304402207b3301ec0ab0c7dbba32690b71e369a6872ff2d0c0cacaddc831bb04d22cef7102206e0bb6d039c408e301d49978aa34597f57d075b9a69e1a9f120e08af159b167a412102cb6cdf3e5758112206b4b02f21838b3d8a26c601a88030f3c5705e357d8e4ea8ffffffff02c0c62d00000000001976a91487f3c240183ef0f3efe4b056029dd16d3e3d5d4f88ac48923900000000001976a9147fa80c90c0f8aa021074702c06b3300c0b24724488ac00000000" +}; + +final tx3Raw = { + "in_mempool": false, + "in_orphanpool": false, + "txid": "070b45d901243b5856a0cccce8c5f5f548c19aaa00cb0059b37a6a9a3632288a", + "size": 226, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": + "9765cc2efa42ffdfb5ec86e6ae043de4cf2158a00ed19a182c4244bc548334ba", + "vout": 1, + "scriptSig": { + "asm": + "3045022100ed38dc64e40a5cfe137d38fbe9b7c4fe8a09ef923d7f999f35c65b029aa233ac02206f119c8d881a1b475697ec1eef815cde2e0e456ce4e234c5762fc7ddbe04ac27[ALL|FORKID] 029845663b31ebf3136039db97b3413b939b61c5bef45e4ee23544165a28ed452b", + "hex": + "483045022100ed38dc64e40a5cfe137d38fbe9b7c4fe8a09ef923d7f999f35c65b029aa233ac02206f119c8d881a1b475697ec1eef815cde2e0e456ce4e234c5762fc7ddbe04ac274121029845663b31ebf3136039db97b3413b939b61c5bef45e4ee23544165a28ed452b" + }, + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.02, + "n": 0, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 87f3c240183ef0f3efe4b056029dd16d3e3d5d4f OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a91487f3c240183ef0f3efe4b056029dd16d3e3d5d4f88ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["1DPrEZBKKVG1Pf4HXeuCkw2Xk65EunR7CM"] + } + }, + { + "value": 0.01772773, + "n": 1, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 39cb987d75cbe99ec577de2f1918ff2b3539491a OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a91439cb987d75cbe99ec577de2f1918ff2b3539491a88ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["16GbR1Xau2hKFTr1STgB39NbP8CEkGZjYG"] + } + } + ], + "blockhash": + "00000000000000000227adf51d47ac640c7353e873a398901ecf9becbf5988d7", + "confirmations": 179, + "time": 1662555788, + "blocktime": 1662555788, + "hex": + "0200000001ba348354bc44422c189ad10ea05821cfe43d04aee686ecb5dfff42fa2ecc6597010000006b483045022100ed38dc64e40a5cfe137d38fbe9b7c4fe8a09ef923d7f999f35c65b029aa233ac02206f119c8d881a1b475697ec1eef815cde2e0e456ce4e234c5762fc7ddbe04ac274121029845663b31ebf3136039db97b3413b939b61c5bef45e4ee23544165a28ed452bffffffff0280841e00000000001976a91487f3c240183ef0f3efe4b056029dd16d3e3d5d4f88ace50c1b00000000001976a91439cb987d75cbe99ec577de2f1918ff2b3539491a88ac00000000" +}; + +final tx4Raw = { + "in_mempool": false, + "in_orphanpool": false, + "txid": "84aecde036ebe013aa3bd2fcb4741db504c7c040d34f7c33732c967646991855", + "size": 360, + "version": 1, + "locktime": 757301, + "vin": [ + { + "txid": + "070b45d901243b5856a0cccce8c5f5f548c19aaa00cb0059b37a6a9a3632288a", + "vout": 0, + "scriptSig": { + "asm": + "95a4d53e9059dc478b2f79dc486b4dd1ea2f34f3f2f870ba26a9c16530305ddc3e25b1d1d5adc42df75b4666b9fe6ec5b41813c0e82a579ce2167f6f7ed1b305[ALL|FORKID] 02d0825e4d527c9c24e0d423187055904f91218c82652b3fe575a212fef15531fd", + "hex": + "4195a4d53e9059dc478b2f79dc486b4dd1ea2f34f3f2f870ba26a9c16530305ddc3e25b1d1d5adc42df75b4666b9fe6ec5b41813c0e82a579ce2167f6f7ed1b305412102d0825e4d527c9c24e0d423187055904f91218c82652b3fe575a212fef15531fd" + }, + "sequence": 4294967294 + }, + { + "txid": + "9765cc2efa42ffdfb5ec86e6ae043de4cf2158a00ed19a182c4244bc548334ba", + "vout": 0, + "scriptSig": { + "asm": + "f2557ee7ae3eaf6488cc24972c73578ffc6ea2db047ffc4ff0b220f5d4efe491de01e1024ee77dc88d2cfa2f44b686bf394bd2a7114aac4fac48007547e2d313[ALL|FORKID] 02d0825e4d527c9c24e0d423187055904f91218c82652b3fe575a212fef15531fd", + "hex": + "41f2557ee7ae3eaf6488cc24972c73578ffc6ea2db047ffc4ff0b220f5d4efe491de01e1024ee77dc88d2cfa2f44b686bf394bd2a7114aac4fac48007547e2d313412102d0825e4d527c9c24e0d423187055904f91218c82652b3fe575a212fef15531fd" + }, + "sequence": 4294967294 + } + ], + "vout": [ + { + "value": 0.009996, + "n": 0, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 bd9e7c204b6d0d90ba018250fafa398d5ec1b39d OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914bd9e7c204b6d0d90ba018250fafa398d5ec1b39d88ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["1JHcZyhgctuDCznjkxR51pQzKEJUujuc2j"] + } + }, + { + "value": 0.04, + "n": 1, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 f5c809c469d24bc0bf4f6a17a9218df1a79cd247 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914f5c809c469d24bc0bf4f6a17a9218df1a79cd24788ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["1PQaBto5KmiW3R2YeexYYoDWksMpEvhYZE"] + } + } + ], + "blockhash": + "000000000000000005aa6b3094801ec56f36f74d4b25cad38b22dc3d24cd3e43", + "confirmations": 1, + "time": 1662893734, + "blocktime": 1662893734, + "hex": + "01000000028a2832369a6a7ab35900cb00aa9ac148f5f5c5e8cccca056583b2401d9450b0700000000644195a4d53e9059dc478b2f79dc486b4dd1ea2f34f3f2f870ba26a9c16530305ddc3e25b1d1d5adc42df75b4666b9fe6ec5b41813c0e82a579ce2167f6f7ed1b305412102d0825e4d527c9c24e0d423187055904f91218c82652b3fe575a212fef15531fdfeffffffba348354bc44422c189ad10ea05821cfe43d04aee686ecb5dfff42fa2ecc6597000000006441f2557ee7ae3eaf6488cc24972c73578ffc6ea2db047ffc4ff0b220f5d4efe491de01e1024ee77dc88d2cfa2f44b686bf394bd2a7114aac4fac48007547e2d313412102d0825e4d527c9c24e0d423187055904f91218c82652b3fe575a212fef15531fdfeffffff02b0400f00000000001976a914bd9e7c204b6d0d90ba018250fafa398d5ec1b39d88ac00093d00000000001976a914f5c809c469d24bc0bf4f6a17a9218df1a79cd24788ac358e0b00" +}; diff --git a/test/services/coins/bitcoincash/bitcoincash_utxo_sample_data.dart b/test/services/coins/bitcoincash/bitcoincash_utxo_sample_data.dart new file mode 100644 index 000000000..54ab1c37b --- /dev/null +++ b/test/services/coins/bitcoincash/bitcoincash_utxo_sample_data.dart @@ -0,0 +1,84 @@ +import 'package:stackwallet/models/paymint/utxo_model.dart'; + +final Map>> batchGetUTXOResponse0 = { + "some id 0": [ + { + "tx_pos": 0, + "value": 7000000, + "tx_hash": + "61fedb3cb994917d2852191785ab59cb0d177d55d860bf10fd671f6a0a83247c", + "height": 756720 + }, + { + "tx_pos": 0, + "value": 3000000, + "tx_hash": + "9765cc2efa42ffdfb5ec86e6ae043de4cf2158a00ed19a182c4244bc548334ba", + "height": 756732 + }, + ], + "some id 1": [ + { + "tx_pos": 1, + "value": 2000000, + "tx_hash": + "070b45d901243b5856a0cccce8c5f5f548c19aaa00cb0059b37a6a9a3632288a", + "height": 756738 + }, + ], + "some id 2": [], +}; + +final utxoList = [ + UtxoObject( + txid: "9765cc2efa42ffdfb5ec86e6ae043de4cf2158a00ed19a182c4244bc548334ba", + vout: 0, + status: Status( + confirmed: true, + confirmations: 175, + blockHeight: 756732, + blockTime: 1662553616, + blockHash: + "000000000000000005d25c8d3722e4486c486bbf864f9261631993afab557832", + ), + value: 3000000, + fiatWorth: "\$0", + txName: "1DPrEZBKKVG1Pf4HXeuCkw2Xk65EunR7CM", + blocked: false, + isCoinbase: false, + ), + UtxoObject( + txid: "f716d010786225004b41e35dd5eebfb11a4e5ea116e1a48235e5d3a591650732", + vout: 1, + status: Status( + confirmed: true, + confirmations: 11867, + blockHeight: 745443, + blockTime: 1655792385, + blockHash: + "000000000000000000065c982f4d86a402e7182d0c6a49fa6cfbdaf67a57f566", + ), + value: 78446451, + fiatWorth: "\$0", + txName: "3E1n17NnhVmWTGNbvH6ffKVNFjYT4Jke7G", + blocked: false, + isCoinbase: false, + ), + UtxoObject( + txid: "070b45d901243b5856a0cccce8c5f5f548c19aaa00cb0059b37a6a9a3632288a", + vout: 0, + status: Status( + confirmed: true, + confirmations: 572, + blockHeight: 756738, + blockTime: 1662555788, + blockHash: + "00000000000000000227adf51d47ac640c7353e873a398901ecf9becbf5988d7", + ), + value: 2000000, + fiatWorth: "\$0", + txName: "1DPrEZBKKVG1Pf4HXeuCkw2Xk65EunR7CM", + blocked: false, + isCoinbase: false, + ), +]; diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart new file mode 100644 index 000000000..c625dd4b7 --- /dev/null +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart @@ -0,0 +1,2824 @@ +import 'package:bitcoindart/bitcoindart.dart'; +import 'package:decimal/decimal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.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/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/bitcoincash/bitcoincash_wallet.dart'; +import 'package:stackwallet/services/price.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; + +import 'bitcoincash_history_sample_data.dart'; +import 'bitcoincash_wallet_test.mocks.dart'; +import 'bitcoincash_wallet_test_parameters.dart'; + +@GenerateMocks([CachedElectrumX, PriceAPI, TransactionNotificationTracker], + customMocks: [MockSpec(returnNullOnMissingStub: true)]) +void main() { + group("bitcoincash constants", () { + test("bitcoincash minimum confirmations", () async { + expect(MINIMUM_CONFIRMATIONS, 3); + }); + test("bitcoincash dust limit", () async { + expect(DUST_LIMIT, 1000000); + }); + test("bitcoincash mainnet genesis block hash", () async { + expect(GENESIS_HASH_MAINNET, + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"); + }); + + test("bitcoincash testnet genesis block hash", () async { + expect(GENESIS_HASH_TESTNET, + "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"); + }); + }); + + test("bitcoincash DerivePathType enum", () { + expect(DerivePathType.values.length, 1); + expect(DerivePathType.values.toString(), "[DerivePathType.bip44]"); + }); + + group("bip32 node/root", () { + test("getBip32Root", () { + final root = getBip32Root(TEST_MNEMONIC, bitcoincash); + expect(root.toWIF(), ROOT_WIF); + }); + + test("basic getBip32Node", () { + final node = + getBip32Node(0, 0, TEST_MNEMONIC, bitcoincash, DerivePathType.bip44); + expect(node.toWIF(), NODE_WIF_44); + }); + }); + + group("validate mainnet bitcoincash addresses", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + FakeSecureStorage? secureStore; + MockTransactionNotificationTracker? tracker; + + BitcoinCashWallet? mainnetWallet; + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + mainnetWallet = BitcoinCashWallet( + walletId: "validateAddressMainNet", + walletName: "validateAddressMainNet", + coin: Coin.bitcoincash, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("valid mainnet legacy/p2pkh address type", () { + expect( + mainnetWallet?.addressType( + address: "1DP3PUePwMa5CoZwzjznVKhzdLsZftjcAT"), + DerivePathType.bip44); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("invalid base58 address type", () { + expect( + () => mainnetWallet?.addressType( + address: "mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("invalid bech32 address type", () { + expect( + () => mainnetWallet?.addressType( + address: "tb1qzzlm6mnc8k54mx6akehl8p9ray8r439va5ndyq"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("address has no matching script", () { + expect( + () => mainnetWallet?.addressType( + address: "mpMk94ETazqonHutyC1v6ajshgtP8oiFKU"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("invalid mainnet bitcoincash legacy/p2pkh address", () { + expect( + mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + }); + + group("testNetworkConnection", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + FakeSecureStorage? secureStore; + MockTransactionNotificationTracker? tracker; + + BitcoinCashWallet? bch; + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + bch = BitcoinCashWallet( + walletId: "testNetworkConnection", + walletName: "testNetworkConnection", + coin: Coin.bitcoincash, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("attempted connection fails due to server error", () async { + when(client?.ping()).thenAnswer((_) async => false); + final bool? result = await bch?.testNetworkConnection(); + expect(result, false); + expect(secureStore?.interactions, 0); + verify(client?.ping()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("attempted connection fails due to exception", () async { + when(client?.ping()).thenThrow(Exception); + final bool? result = await bch?.testNetworkConnection(); + expect(result, false); + expect(secureStore?.interactions, 0); + verify(client?.ping()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("attempted connection test success", () async { + when(client?.ping()).thenAnswer((_) async => true); + final bool? result = await bch?.testNetworkConnection(); + expect(result, true); + expect(secureStore?.interactions, 0); + verify(client?.ping()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + }); + + group("basic getters, setters, and functions", () { + final bchcoin = Coin.bitcoincash; + final testWalletId = "BCHtestWalletID"; + final testWalletName = "BCHWallet"; + + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + FakeSecureStorage? secureStore; + MockTransactionNotificationTracker? tracker; + + BitcoinCashWallet? bch; + + setUp(() async { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + bch = BitcoinCashWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: bchcoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("get networkType main", () async { + expect(bch?.coin, bchcoin); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("get networkType test", () async { + bch = BitcoinCashWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: bchcoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + expect(bch?.coin, bchcoin); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("get cryptoCurrency", () async { + expect(Coin.bitcoincash, Coin.bitcoincash); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("get coinName", () async { + expect(Coin.bitcoincash, Coin.bitcoincash); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("get coinTicker", () async { + expect(Coin.bitcoincash, Coin.bitcoincash); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("get and set walletName", () async { + expect(Coin.bitcoincash, Coin.bitcoincash); + bch?.walletName = "new name"; + expect(bch?.walletName, "new name"); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("estimateTxFee", () async { + expect(bch?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); + expect(bch?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); + expect(bch?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); + expect(bch?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); + expect(bch?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); + expect(bch?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); + expect(bch?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); + expect(bch?.estimateTxFee(vSize: 356, feeRatePerKB: 12345), 4628); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("get fees succeeds", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.estimateFee(blocks: 1)) + .thenAnswer((realInvocation) async => Decimal.zero); + when(client?.estimateFee(blocks: 5)) + .thenAnswer((realInvocation) async => Decimal.one); + when(client?.estimateFee(blocks: 20)) + .thenAnswer((realInvocation) async => Decimal.ten); + + final fees = await bch?.fees; + expect(fees, isA()); + expect(fees?.slow, 1000000000); + expect(fees?.medium, 100000000); + expect(fees?.fast, 0); + + verify(client?.estimateFee(blocks: 1)).called(1); + verify(client?.estimateFee(blocks: 5)).called(1); + verify(client?.estimateFee(blocks: 20)).called(1); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("get fees fails", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.estimateFee(blocks: 1)) + .thenAnswer((realInvocation) async => Decimal.zero); + when(client?.estimateFee(blocks: 5)) + .thenAnswer((realInvocation) async => Decimal.one); + when(client?.estimateFee(blocks: 20)) + .thenThrow(Exception("some exception")); + + bool didThrow = false; + try { + await bch?.fees; + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + verify(client?.estimateFee(blocks: 1)).called(1); + verify(client?.estimateFee(blocks: 5)).called(1); + verify(client?.estimateFee(blocks: 20)).called(1); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("get maxFee", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.estimateFee(blocks: 20)) + .thenAnswer((realInvocation) async => Decimal.zero); + when(client?.estimateFee(blocks: 5)) + .thenAnswer((realInvocation) async => Decimal.one); + when(client?.estimateFee(blocks: 1)) + .thenAnswer((realInvocation) async => Decimal.ten); + + final maxFee = await bch?.maxFee; + expect(maxFee, 1000000000); + + verify(client?.estimateFee(blocks: 1)).called(1); + verify(client?.estimateFee(blocks: 5)).called(1); + verify(client?.estimateFee(blocks: 20)).called(1); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + }); + + group("BCHWallet service class functions that depend on shared storage", () { + final bchcoin = Coin.bitcoincash; + final bchtestcoin = Coin.bitcoincashTestnet; + final testWalletId = "BCHtestWalletID"; + final testWalletName = "BCHWallet"; + + bool hiveAdaptersRegistered = false; + + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + FakeSecureStorage? secureStore; + MockTransactionNotificationTracker? tracker; + + BitcoinCashWallet? bch; + + setUp(() async { + await setUpTestHive(); + if (!hiveAdaptersRegistered) { + hiveAdaptersRegistered = true; + + // Registering Transaction Model Adapters + Hive.registerAdapter(TransactionDataAdapter()); + Hive.registerAdapter(TransactionChunkAdapter()); + Hive.registerAdapter(TransactionAdapter()); + Hive.registerAdapter(InputAdapter()); + Hive.registerAdapter(OutputAdapter()); + + // Registering Utxo Model Adapters + Hive.registerAdapter(UtxoDataAdapter()); + Hive.registerAdapter(UtxoObjectAdapter()); + Hive.registerAdapter(StatusAdapter()); + + final wallets = await Hive.openBox('wallets'); + await wallets.put('currentWalletName', testWalletName); + } + + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + bch = BitcoinCashWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: bchcoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + // test("initializeWallet no network", () async { + // when(client?.ping()).thenAnswer((_) async => false); + // await Hive.openBox(testWalletId); + // await Hive.openBox(DB.boxNamePrefs); + // expect(bch?.initializeNew(), false); + // expect(secureStore?.interactions, 0); + // verify(client?.ping()).called(0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("initializeExisting no network exception", () async { + // when(client?.ping()).thenThrow(Exception("Network connection failed")); + // // bch?.initializeNew(); + // expect(bch?.initializeExisting(), false); + // expect(secureStore?.interactions, 0); + // verify(client?.ping()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + test("initializeNew mainnet throws bad network", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + expectLater(() => bch?.initializeNew(), throwsA(isA())) + .then((_) { + expect(secureStore?.interactions, 0); + verifyNever(client?.ping()).called(0); + verify(client?.getServerFeatures()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + }); + + test("initializeNew throws mnemonic overwrite exception", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + await secureStore?.write( + key: "${testWalletId}_mnemonic", value: "some mnemonic"); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + expectLater(() => bch?.initializeNew(), throwsA(isA())) + .then((_) { + expect(secureStore?.interactions, 2); + verifyNever(client?.ping()).called(0); + verify(client?.getServerFeatures()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + }); + + test("initializeExisting testnet throws bad network", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + + bch = BitcoinCashWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: bchcoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + expectLater(() => bch?.initializeNew(), throwsA(isA())) + .then((_) { + expect(secureStore?.interactions, 0); + verifyNever(client?.ping()).called(0); + verify(client?.getServerFeatures()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + }); + + // test("getCurrentNode", () async { + // // when(priceAPI?.getbitcoincashPrice(baseCurrency: "USD")) + // // .thenAnswer((realInvocation) async => Decimal.fromInt(10)); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // // await DebugService.instance.init(); + // expect(bch?.initializeExisting(), true); + // + // bool didThrow = false; + // try { + // await bch?.getCurrentNode(); + // } catch (_) { + // didThrow = true; + // } + // // expect no nodes on a fresh wallet unless set in db externally + // expect(didThrow, true); + // + // // set node + // final wallet = await Hive.openBox(testWalletId); + // await wallet.put("nodes", { + // "default": { + // "id": "some nodeID", + // "ipAddress": "some address", + // "port": "9000", + // "useSSL": true, + // } + // }); + // await wallet.put("activeNodeName", "default"); + // + // // try fetching again + // final node = await bch?.getCurrentNode(); + // expect(node.toString(), + // "ElectrumXNode: {address: some address, port: 9000, name: default, useSSL: true}"); + // + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("initializeWallet new main net wallet", () async { + // when(priceAPI?.getbitcoincashPrice(baseCurrency: "USD")) + // .thenAnswer((realInvocation) async => Decimal.fromInt(10)); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // expect(await bch?.initializeWallet(), true); + // + // final wallet = await Hive.openBox(testWalletId); + // + // expect(await wallet.get("addressBookEntries"), {}); + // expect(await wallet.get('notes'), null); + // expect(await wallet.get("id"), testWalletId); + // expect(await wallet.get("preferredFiatCurrency"), null); + // expect(await wallet.get("blocked_tx_hashes"), ["0xdefault"]); + // + // final changeAddressesP2PKH = await wallet.get("changeAddressesP2PKH"); + // expect(changeAddressesP2PKH, isA>()); + // expect(changeAddressesP2PKH.length, 1); + // expect(await wallet.get("changeIndexP2PKH"), 0); + // + // final receivingAddressesP2PKH = + // await wallet.get("receivingAddressesP2PKH"); + // expect(receivingAddressesP2PKH, isA>()); + // expect(receivingAddressesP2PKH.length, 1); + // expect(await wallet.get("receivingIndexP2PKH"), 0); + // + // final p2pkhReceiveDerivations = jsonDecode(await secureStore?.read( + // key: "${testWalletId}_receiveDerivationsP2PKH")); + // expect(p2pkhReceiveDerivations.length, 1); + // + // final p2pkhChangeDerivations = jsonDecode(await secureStore.read( + // key: "${testWalletId}_changeDerivationsP2PKH")); + // expect(p2pkhChangeDerivations.length, 1); + // + // expect(secureStore?.interactions, 10); + // expect(secureStore?.reads, 7); + // expect(secureStore?.writes, 3); + // expect(secureStore?.deletes, 0); + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // // test("initializeWallet existing main net wallet", () async { + // // when(priceAPI?.getbitcoincashPrice(baseCurrency: "USD")) + // // .thenAnswer((realInvocation) async => Decimal.fromInt(10)); + // // when(client?.ping()).thenAnswer((_) async => true); + // // when(client?.getBatchHistory(args: anyNamed("args"))) + // // .thenAnswer((_) async => {}); + // // when(client?.getServerFeatures()).thenAnswer((_) async => { + // // "hosts": {}, + // // "pruning": null, + // // "server_version": "Unit tests", + // // "protocol_min": "1.4", + // // "protocol_max": "1.4.2", + // // "genesis_hash": GENESIS_HASH_MAINNET, + // // "hash_function": "sha256", + // // "services": [] + // // }); + // // // init new wallet + // // expect(bch?.initializeNew(), true); + // // + // // // fetch data to compare later + // // final newWallet = await Hive.openBox(testWalletId); + // // + // // final addressBookEntries = await newWallet.get("addressBookEntries"); + // // final notes = await newWallet.get('notes'); + // // final wID = await newWallet.get("id"); + // // final currency = await newWallet.get("preferredFiatCurrency"); + // // final blockedHashes = await newWallet.get("blocked_tx_hashes"); + // // + // // final changeAddressesP2PKH = await newWallet.get("changeAddressesP2PKH"); + // // final changeIndexP2PKH = await newWallet.get("changeIndexP2PKH"); + // // + // // final receivingAddressesP2PKH = + // // await newWallet.get("receivingAddressesP2PKH"); + // // final receivingIndexP2PKH = await newWallet.get("receivingIndexP2PKH"); + // // + // // final p2pkhReceiveDerivations = jsonDecode(await secureStore?.read( + // // key: "${testWalletId}_receiveDerivationsP2PKH")); + // // + // // final p2pkhChangeDerivations = jsonDecode(await secureStore?.read( + // // key: "${testWalletId}_changeDerivationsP2PKH")); + // // + // // // exit new wallet + // // await bch?.exit(); + // // + // // // open existing/created wallet + // // bch = BitcoinCashWallet( + // // walletId: testWalletId, + // // walletName: testWalletName, + // // coin: dtestcoin, + // // client: client!, + // // cachedClient: cachedClient!, + // // priceAPI: priceAPI, + // // secureStore: secureStore, + // // ); + // // + // // // init existing + // // expect(bch?.initializeExisting(), true); + // // + // // // compare data to ensure state matches state of previously closed wallet + // // final wallet = await Hive.openBox(testWalletId); + // // + // // expect(await wallet.get("addressBookEntries"), addressBookEntries); + // // expect(await wallet.get('notes'), notes); + // // expect(await wallet.get("id"), wID); + // // expect(await wallet.get("preferredFiatCurrency"), currency); + // // expect(await wallet.get("blocked_tx_hashes"), blockedHashes); + // // + // // expect(await wallet.get("changeAddressesP2PKH"), changeAddressesP2PKH); + // // expect(await wallet.get("changeIndexP2PKH"), changeIndexP2PKH); + // // + // // expect( + // // await wallet.get("receivingAddressesP2PKH"), receivingAddressesP2PKH); + // // expect(await wallet.get("receivingIndexP2PKH"), receivingIndexP2PKH); + // // + // // expect( + // // jsonDecode(await secureStore?.read( + // // key: "${testWalletId}_receiveDerivationsP2PKH")), + // // p2pkhReceiveDerivations); + // // + // // expect( + // // jsonDecode(await secureStore?.read( + // // key: "${testWalletId}_changeDerivationsP2PKH")), + // // p2pkhChangeDerivations); + // // + // // expect(secureStore?.interactions, 12); + // // expect(secureStore?.reads, 9); + // // expect(secureStore?.writes, 3); + // // expect(secureStore?.deletes, 0); + // // verify(client?.ping()).called(2); + // // verify(client?.getServerFeatures()).called(1); + // // verifyNoMoreInteractions(client); + // // verifyNoMoreInteractions(cachedClient); + // // verifyNoMoreInteractions(priceAPI); + // // }); + + test("get current receiving addresses", () async { + bch = BitcoinCashWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: bchtestcoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + await bch?.initializeNew(); + await bch?.initializeExisting(); + expect( + Address.validateAddress( + await bch!.currentReceivingAddress, bitcoincashtestnet), + true); + expect( + Address.validateAddress( + await bch!.currentReceivingAddress, bitcoincashtestnet), + true); + expect( + Address.validateAddress( + await bch!.currentReceivingAddress, bitcoincashtestnet), + true); + + verifyNever(client?.ping()).called(0); + verify(client?.getServerFeatures()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get allOwnAddresses", () async { + bch = BitcoinCashWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: bchtestcoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + await bch?.initializeNew(); + await bch?.initializeExisting(); + final addresses = await bch?.allOwnAddresses; + expect(addresses, isA>()); + expect(addresses?.length, 2); + + for (int i = 0; i < 2; i++) { + expect( + Address.validateAddress(addresses![i], bitcoincashtestnet), true); + } + + verifyNever(client?.ping()).called(0); + verify(client?.getServerFeatures()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + // test("get utxos and balances", () async { + // bch = BitcoinCashWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: dtestcoin, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_TESTNET, + // "hash_function": "sha256", + // "services": [] + // }); + // + // await Hive.openBox(testWalletId); + // await Hive.openBox(DB.boxNamePrefs); + // + // when(client?.getBatchUTXOs(args: anyNamed("args"))) + // .thenAnswer((_) async => batchGetUTXOResponse0); + // + // when(client?.estimateFee(blocks: 20)) + // .thenAnswer((realInvocation) async => Decimal.zero); + // when(client?.estimateFee(blocks: 5)) + // .thenAnswer((realInvocation) async => Decimal.one); + // when(client?.estimateFee(blocks: 1)) + // .thenAnswer((realInvocation) async => Decimal.ten); + // + // when(cachedClient?.getTransaction( + // txHash: tx1.txid, + // coin: Coin.bitcoincashTestNet, + // )).thenAnswer((_) async => tx1Raw); + // when(cachedClient?.getTransaction( + // txHash: tx2.txid, + // coin: Coin.bitcoincashTestNet, + // )).thenAnswer((_) async => tx2Raw); + // when(cachedClient?.getTransaction( + // txHash: tx3.txid, + // coin: Coin.bitcoincashTestNet, + // )).thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // txHash: tx4.txid, + // coin: Coin.bitcoincashTestNet, + // )).thenAnswer((_) async => tx4Raw); + // + // await bch?.initializeNew(); + // await bch?.initializeExisting(); + // + // final utxoData = await bch?.utxoData; + // expect(utxoData, isA()); + // expect(utxoData.toString(), + // r"{totalUserCurrency: $103.2173, satoshiBalance: 1032173000, bitcoinBalance: null, unspentOutputArray: [{txid: 86198a91805b6c53839a6a97736c434a5a2f85d68595905da53df7df59b9f01a, vout: 0, value: 800000000, fiat: $80, blocked: false, status: {confirmed: true, blockHash: e52cabb4445eb9ceb3f4f8d68cc64b1ede8884ce560296c27826a48ecc477370, blockHeight: 4274457, blockTime: 1655755742, confirmations: 100}}, {txid: a4b6bd97a4b01b4305d0cf02e9bac6b7c37cda2f8e9dfe291ce4170b810ed469, vout: 0, value: 72173000, fiat: $7.2173, blocked: false, status: {confirmed: false, blockHash: bd239f922b3ecec299a90e4d1ce389334e8df4b95470fb5919966b0b650bb95b, blockHeight: 4270459, blockTime: 1655500912, confirmations: 0}}, {txid: 68c159dcc2f962cbc61f7dd3c8d0dcc14da8adb443811107115531c853fc0c60, vout: 1, value: 100000000, fiat: $10, blocked: false, status: {confirmed: false, blockHash: 9fee9b9446cfe81abb1a17bec56e6c160d9a6527e5b68b1141a827573bc2649f, blockHeight: 4255659, blockTime: 1654553247, confirmations: 0}}, {txid: 628a78606058ce4036aee3907e042742156c1894d34419578de5671b53ea5800, vout: 0, value: 60000000, fiat: $6, blocked: false, status: {confirmed: true, blockHash: bc461ab43e3a80d9a4d856ee9ff70f41d86b239d5f0581ffd6a5c572889a6b86, blockHeight: 4270352, blockTime: 1652888705, confirmations: 100}}]}"); + // + // final outputs = await bch?.unspentOutputs; + // expect(outputs, isA>()); + // expect(outputs?.length, 4); + // + // final availableBalance = await bch?.availableBalance; + // expect(availableBalance, Decimal.parse("8.6")); + // + // final totalBalance = await bch?.totalBalance; + // expect(totalBalance, Decimal.parse("10.32173")); + // + // final pendingBalance = await bch?.pendingBalance; + // expect(pendingBalance, Decimal.parse("1.72173")); + // + // final balanceMinusMaxFee = await bch?.balanceMinusMaxFee; + // expect(balanceMinusMaxFee, Decimal.parse("7.6")); + // + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verify(client?.estimateFee(blocks: 1)).called(1); + // verify(client?.estimateFee(blocks: 5)).called(1); + // verify(client?.estimateFee(blocks: 20)).called(1); + // verify(client?.getBatchUTXOs(args: anyNamed("args"))).called(1); + // verify(cachedClient?.getTransaction( + // txHash: tx1.txid, + // coin: Coin.bitcoincashTestNet, + // )).called(1); + // verify(cachedClient?.getTransaction( + // txHash: tx2.txid, + // coin: Coin.bitcoincashTestNet, + // )).called(1); + // verify(cachedClient?.getTransaction( + // txHash: tx3.txid, + // coin: Coin.bitcoincashTestNet, + // )).called(1); + // verify(cachedClient?.getTransaction( + // txHash: tx4.txid, + // coin: Coin.bitcoincashTestNet, + // )).called(1); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // // test("get utxos - multiple batches", () async { + // // bch = BitcoinCashWallet( + // // walletId: testWalletId, + // // walletName: testWalletName, + // // coin: dtestcoin, + // // client: client!, + // // cachedClient: cachedClient!, + // // priceAPI: priceAPI, + // // secureStore: secureStore, + // // ); + // // when(client?.ping()).thenAnswer((_) async => true); + // // when(client?.getServerFeatures()).thenAnswer((_) async => { + // // "hosts": {}, + // // "pruning": null, + // // "server_version": "Unit tests", + // // "protocol_min": "1.4", + // // "protocol_max": "1.4.2", + // // "genesis_hash": GENESIS_HASH_TESTNET, + // // "hash_function": "sha256", + // // "services": [] + // // }); + // // + // // when(client?.getBatchUTXOs(args: anyNamed("args"))) + // // .thenAnswer((_) async => {}); + // // + // // when(priceAPI?.getbitcoincashPrice(baseCurrency: "USD")) + // // .thenAnswer((realInvocation) async => Decimal.fromInt(10)); + // // + // // await bch?.initializeWallet(); + // // + // // // add some extra addresses to make sure we have more than the single batch size of 10 + // // final wallet = await Hive.openBox(testWalletId); + // // final addresses = await wallet.get("receivingAddressesP2PKH"); + // // addresses.add("DQaAi9R58GXMpDyhePys6hHCuif4fhc1sN"); + // // addresses.add("DBVhuF8QgeuxU2pssxzMgJqPhGCx5qyVkD"); + // // addresses.add("DCAokB2CXXPWC2JPj6jrK6hxANwTF2m21x"); + // // addresses.add("D6Y9brE3jUGPrqLmSEWh6yQdgY5b7ZkTib"); + // // addresses.add("DKdtobt3M5b3kQWZf1zRUZn3Ys6JTQwbPL"); + // // addresses.add("DBYiFr1BRc2zB19p8jxdSu6DvFGTdWvkVF"); + // // addresses.add("DE5ffowvbHPzzY6aRVGpzxR2QqikXxUKPG"); + // // addresses.add("DA97TLg1741J2aLK6z9bVZoWysgQbMR45K"); + // // addresses.add("DGGmf9q4PKcJXauPRstsFetu9DjW1VSBYk"); + // // addresses.add("D9bXqnTtufcb6oJyuZniCXbst8MMLzHxUd"); + // // addresses.add("DA6nv8M4kYL4RxxKrcsPaPUA1KrFA7CTfN"); + // // await wallet.put("receivingAddressesP2PKH", addresses); + // // + // // final utxoData = await bch?.utxoData; + // // expect(utxoData, isA()); + // // + // // final outputs = await bch?.unspentOutputs; + // // expect(outputs, isA>()); + // // expect(outputs?.length, 0); + // // + // // verify(client?.ping()).called(1); + // // verify(client?.getServerFeatures()).called(1); + // // verify(client?.getBatchUTXOs(args: anyNamed("args"))).called(2); + // // verify(priceAPI?.getbitcoincashPrice(baseCurrency: "USD")).called(1); + // // + // // verifyNoMoreInteractions(client); + // // verifyNoMoreInteractions(cachedClient); + // // verifyNoMoreInteractions(priceAPI); + // // }); + // + test("get utxos fails", () async { + bch = BitcoinCashWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: bchtestcoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + when(client?.getBatchUTXOs(args: anyNamed("args"))) + .thenThrow(Exception("some exception")); + + await bch?.initializeNew(); + await bch?.initializeExisting(); + + final utxoData = await bch?.utxoData; + expect(utxoData, isA()); + expect(utxoData.toString(), + r"{totalUserCurrency: 0.00, satoshiBalance: 0, bitcoinBalance: 0, unspentOutputArray: []}"); + + final outputs = await bch?.unspentOutputs; + expect(outputs, isA>()); + expect(outputs?.length, 0); + + verifyNever(client?.ping()).called(0); + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchUTXOs(args: anyNamed("args"))).called(1); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("chain height fetch, update, and get", () async { + bch = BitcoinCashWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: bchtestcoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + await bch?.initializeNew(); + await bch?.initializeExisting(); + + // get stored + expect(await bch?.storedChainHeight, 0); + + // fetch fails + when(client?.getBlockHeadTip()).thenThrow(Exception("Some exception")); + expect(await bch?.chainHeight, -1); + + // fetch succeeds + when(client?.getBlockHeadTip()).thenAnswer((realInvocation) async => { + "height": 100, + "hex": "some block hex", + }); + expect(await bch?.chainHeight, 100); + + // update + await bch?.updateStoredChainHeight(newHeight: 1000); + + // fetch updated + expect(await bch?.storedChainHeight, 1000); + + verifyNever(client?.ping()).called(0); + verify(client?.getServerFeatures()).called(1); + verify(client?.getBlockHeadTip()).called(2); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("getTxCount succeeds", () async { + when(client?.getHistory( + scripthash: + "1df1cab6d109d506aa424b00b6a013c5e1947dc13b78d62b4d0e9f518b3035d1")) + .thenAnswer((realInvocation) async => [ + { + "height": 757727, + "tx_hash": + "aaac451c49c2e3bcbccb8a9fded22257eeb94c1702b456171aa79250bc1b20e0" + }, + { + "height": 0, + "tx_hash": + "9ac29f35b72ca596bc45362d1f9556b0555e1fb633ca5ac9147a7fd467700afe" + } + ]); + + final count = + await bch?.getTxCount(address: "1MMi672ueYFXLLdtZqPe4FsrS46gNDyRq1"); + + expect(count, 2); + + verify(client?.getHistory( + scripthash: + "1df1cab6d109d506aa424b00b6a013c5e1947dc13b78d62b4d0e9f518b3035d1")) + .called(1); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + //TODO - Needs refactoring + test("getTxCount fails", () async { + when(client?.getHistory( + scripthash: + "64953f7db441a21172de206bf70b920c8c718ed4f03df9a85073c0400be0053c")) + .thenThrow(Exception("some exception")); + + bool didThrow = false; + try { + await bch?.getTxCount(address: "D6biRASajCy7GcJ8R6ZP4RE94fNRerJLCC"); + } catch (_) { + didThrow = true; + } + expect(didThrow, true); + + verifyNever(client?.getHistory( + scripthash: + "64953f7db441a21172de206bf70b920c8c718ed4f03df9a85073c0400be0053c")) + .called(0); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("_checkCurrentReceivingAddressesForTransactions succeeds", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getHistory(scripthash: anyNamed("scripthash"))) + .thenAnswer((realInvocation) async => [ + { + "height": 4270385, + "tx_hash": + "c07f740ad72c0dd759741f4c9ab4b1586a22bc16545584364ac9b3d845766271" + }, + { + "height": 4270459, + "tx_hash": + "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a" + } + ]); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + await bch?.initializeNew(); + await bch?.initializeExisting(); + + bool didThrow = false; + try { + await bch?.checkCurrentReceivingAddressesForTransactions(); + } catch (_) { + didThrow = true; + } + expect(didThrow, false); + + verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); + verify(client?.getServerFeatures()).called(1); + verifyNever(client?.ping()).called(0); + + expect(secureStore?.interactions, 11); + expect(secureStore?.reads, 7); + expect(secureStore?.writes, 4); + expect(secureStore?.deletes, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("_checkCurrentReceivingAddressesForTransactions fails", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getHistory(scripthash: anyNamed("scripthash"))) + .thenThrow(Exception("some exception")); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + await bch?.initializeNew(); + await bch?.initializeExisting(); + + bool didThrow = false; + try { + await bch?.checkCurrentReceivingAddressesForTransactions(); + } catch (_) { + didThrow = true; + } + expect(didThrow, true); + + verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); + verify(client?.getServerFeatures()).called(1); + verifyNever(client?.ping()).called(0); + + expect(secureStore?.interactions, 8); + expect(secureStore?.reads, 5); + expect(secureStore?.writes, 3); + expect(secureStore?.deletes, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("_checkCurrentChangeAddressesForTransactions succeeds", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getHistory(scripthash: anyNamed("scripthash"))) + .thenAnswer((realInvocation) async => [ + { + "height": 4286283, + "tx_hash": + "4c119685401e28982283e644c57d84fde6aab83324012e35c9b49e6efd99b49b" + }, + { + "height": 4286295, + "tx_hash": + "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a" + } + ]); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + await bch?.initializeNew(); + await bch?.initializeExisting(); + + bool didThrow = false; + try { + await bch?.checkCurrentChangeAddressesForTransactions(); + } catch (_) { + didThrow = true; + } + expect(didThrow, false); + + verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); + verify(client?.getServerFeatures()).called(1); + verifyNever(client?.ping()).called(0); + + expect(secureStore?.interactions, 11); + expect(secureStore?.reads, 7); + expect(secureStore?.writes, 4); + expect(secureStore?.deletes, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("_checkCurrentChangeAddressesForTransactions fails", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getHistory(scripthash: anyNamed("scripthash"))) + .thenThrow(Exception("some exception")); + + await Hive.openBox(testWalletId); + await Hive.openBox(DB.boxNamePrefs); + + await bch?.initializeNew(); + await bch?.initializeExisting(); + + bool didThrow = false; + try { + await bch?.checkCurrentChangeAddressesForTransactions(); + } catch (_) { + didThrow = true; + } + expect(didThrow, true); + + verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); + verify(client?.getServerFeatures()).called(1); + verifyNever(client?.ping()).called(0); + + expect(secureStore?.interactions, 8); + expect(secureStore?.reads, 5); + expect(secureStore?.writes, 3); + expect(secureStore?.deletes, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + // test("getAllTxsToWatch", () async { + // TestWidgetsFlutterBinding.ensureInitialized(); + // var notifications = {"show": 0}; + // const MethodChannel('dexterous.com/flutter/local_notifications') + // .setMockMethodCallHandler((call) async { + // notifications[call.method]++; + // }); + // + // bch?.pastUnconfirmedTxs = { + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c", + // "b39bac02b65af46a49e2985278fe24ca00dd5d627395d88f53e35568a04e10fa", + // }; + // + // await bch?.getAllTxsToWatch(transactionData); + // expect(notifications.length, 1); + // expect(notifications["show"], 3); + // + // expect(bch?.unconfirmedTxs, { + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // 'dcca229760b44834478f0b266c9b3f5801e0139fdecacdc0820e447289a006d3', + // }); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("refreshIfThereIsNewData true A", () async { + // when(client?.getTransaction( + // txHash: + // "a4b6bd97a4b01b4305d0cf02e9bac6b7c37cda2f8e9dfe291ce4170b810ed469", + // )).thenAnswer((_) async => tx2Raw); + // when(client?.getTransaction( + // txHash: + // "86198a91805b6c53839a6a97736c434a5a2f85d68595905da53df7df59b9f01a", + // )).thenAnswer((_) async => tx1Raw); + // + // bch = BitcoinCashWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: dtestcoin, + // client: client!, + // cachedClient: cachedClient!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // final wallet = await Hive.openBox(testWalletId); + // await wallet.put('receivingAddressesP2PKH', []); + // + // await wallet.put('changeAddressesP2PKH', []); + // + // bch?.unconfirmedTxs = { + // "a4b6bd97a4b01b4305d0cf02e9bac6b7c37cda2f8e9dfe291ce4170b810ed469", + // "86198a91805b6c53839a6a97736c434a5a2f85d68595905da53df7df59b9f01a" + // }; + // + // final result = await bch?.refreshIfThereIsNewData(); + // + // expect(result, true); + // + // verify(client?.getTransaction( + // txHash: + // "a4b6bd97a4b01b4305d0cf02e9bac6b7c37cda2f8e9dfe291ce4170b810ed469", + // )).called(1); + // verify(client?.getTransaction( + // txHash: + // "86198a91805b6c53839a6a97736c434a5a2f85d68595905da53df7df59b9f01a", + // )).called(1); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("refreshIfThereIsNewData true B", () async { + // // when(priceAPI.getbitcoincashPrice(baseCurrency: "USD")) + // // .thenAnswer((_) async => Decimal.fromInt(10)); + // + // when(client?.getBatchHistory(args: anyNamed("args"))) + // .thenAnswer((realInvocation) async { + // final uuids = Map>.from(realInvocation + // .namedArguments.values.first as Map) + // .keys + // .toList(growable: false); + // return { + // uuids[0]: [ + // { + // "tx_hash": + // "351a94874379a5444c8891162472acf66de538a1abc647d4753f3e1eb5ec66f9", + // "height": 4286305 + // }, + // { + // "tx_hash": + // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // "height": 4286295 + // } + // ], + // uuids[1]: [ + // { + // "tx_hash": + // "4c119685401e28982283e644c57d84fde6aab83324012e35c9b49e6efd99b49b", + // "height": 4286283 + // } + // ], + // }; + // }); + // + // when(client?.getTransaction( + // txHash: + // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // )).thenAnswer((_) async => tx2Raw); + // when(client?.getTransaction( + // txHash: + // "4c119685401e28982283e644c57d84fde6aab83324012e35c9b49e6efd99b49b", + // )).thenAnswer((_) async => tx1Raw); + // + // when(cachedClient?.getTransaction( + // txHash: + // "351a94874379a5444c8891162472acf66de538a1abc647d4753f3e1eb5ec66f9", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "351a94874379a5444c8891162472acf66de538a1abc647d4753f3e1eb5ec66f9", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "4c119685401e28982283e644c57d84fde6aab83324012e35c9b49e6efd99b49b", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx1Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "4493caff0e1b4f248e3c6219e7f288cfdb46c32b72a77aec469098c5f7f5154e", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx5Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "e095cbe5531d174c3fc5c9c39a0e6ba2769489cdabdc17b35b2e3a33a3c2fc61", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx6Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "d3054c63fe8cfafcbf67064ec66b9fbe1ac293860b5d6ffaddd39546658b72de", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx7Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "7b34e60cc37306f866667deb67b14096f4ea2add941fd6e2238a639000642b82", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx4Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "a70c6f0690fa84712dc6b3d20ee13862fe015a08cf2dc8949c4300d49c3bdeb5", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx8Raw); + // + // bch = BitcoinCashWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: dtestcoin, + // client: client!, + // cachedClient: cachedClient!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // final wallet = await Hive.openBox(testWalletId); + // await wallet.put('receivingAddressesP2PKH', []); + // + // await wallet.put('changeAddressesP2PKH', []); + // + // bch?.unconfirmedTxs = { + // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // }; + // + // final result = await bch?.refreshIfThereIsNewData(); + // + // expect(result, true); + // + // verify(client?.getBatchHistory(args: anyNamed("args"))).called(2); + // verify(client?.getTransaction( + // txHash: + // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // )).called(1); + // verify(cachedClient?.getTransaction( + // txHash: anyNamed("tx_hash"), + // verbose: true, + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .called(9); + // // verify(priceAPI?.getbitcoincashPrice(baseCurrency: "USD")).called(1); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("refreshIfThereIsNewData false A", () async { + // // when(priceAPI.getbitcoincashPrice(baseCurrency: "USD")) + // // .thenAnswer((_) async => Decimal.fromInt(10)); + // + // when(client?.getBatchHistory(args: anyNamed("args"))) + // .thenAnswer((realInvocation) async { + // final uuids = Map>.from(realInvocation + // .namedArguments.values.first as Map) + // .keys + // .toList(growable: false); + // return { + // uuids[0]: [ + // { + // "tx_hash": + // "351a94874379a5444c8891162472acf66de538a1abc647d4753f3e1eb5ec66f9", + // "height": 4286305 + // }, + // { + // "tx_hash": + // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // "height": 4286295 + // } + // ], + // uuids[1]: [ + // { + // "tx_hash": + // "4c119685401e28982283e644c57d84fde6aab83324012e35c9b49e6efd99b49b", + // "height": 4286283 + // } + // ], + // }; + // }); + // + // when(client?.getTransaction( + // txHash: + // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // )).thenAnswer((_) async => tx2Raw); + // when(client?.getTransaction( + // txHash: + // "4c119685401e28982283e644c57d84fde6aab83324012e35c9b49e6efd99b49b", + // )).thenAnswer((_) async => tx1Raw); + // + // when(cachedClient?.getTransaction( + // txHash: + // "4c119685401e28982283e644c57d84fde6aab83324012e35c9b49e6efd99b49b", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx1Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx2Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "351a94874379a5444c8891162472acf66de538a1abc647d4753f3e1eb5ec66f9", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "4493caff0e1b4f248e3c6219e7f288cfdb46c32b72a77aec469098c5f7f5154e", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx5Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "7b34e60cc37306f866667deb67b14096f4ea2add941fd6e2238a639000642b82", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx4Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "e095cbe5531d174c3fc5c9c39a0e6ba2769489cdabdc17b35b2e3a33a3c2fc61", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx6Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "d3054c63fe8cfafcbf67064ec66b9fbe1ac293860b5d6ffaddd39546658b72de", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx7Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "a70c6f0690fa84712dc6b3d20ee13862fe015a08cf2dc8949c4300d49c3bdeb5", + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx8Raw); + // + // bch = BitcoinCashWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: dtestcoin, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // final wallet = await Hive.openBox(testWalletId); + // await wallet.put('receivingAddressesP2PKH', []); + // + // await wallet.put('changeAddressesP2PKH', []); + // + // bch?.unconfirmedTxs = { + // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // "351a94874379a5444c8891162472acf66de538a1abc647d4753f3e1eb5ec66f9" + // }; + // + // final result = await bch?.refreshIfThereIsNewData(); + // + // expect(result, false); + // + // verify(client?.getBatchHistory(args: anyNamed("args"))).called(2); + // verify(client?.getTransaction( + // txHash: + // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // )).called(1); + // verify(cachedClient?.getTransaction( + // txHash: anyNamed("tx_hash"), + // verbose: true, + // coin: Coin.bitcoincashTestNet, + // callOutSideMainIsolate: false)) + // .called(15); + // // verify(priceAPI.getbitcoincashPrice(baseCurrency: "USD")).called(1); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // // test("refreshIfThereIsNewData false B", () async { + // // when(client?.getBatchHistory(args: anyNamed("args"))) + // // .thenThrow(Exception("some exception")); + // // + // // when(client?.getTransaction( + // // txHash: + // // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // // )).thenAnswer((_) async => tx2Raw); + // // + // // bch = BitcoinCashWallet( + // // walletId: testWalletId, + // // walletName: testWalletName, + // // coin: dtestcoin, + // // client: client!, + // // cachedClient: cachedClient!, + // // tracker: tracker!, + // // priceAPI: priceAPI, + // // secureStore: secureStore, + // // ); + // // final wallet = await Hive.openBox(testWalletId); + // // await wallet.put('receivingAddressesP2PKH', []); + // // + // // await wallet.put('changeAddressesP2PKH', []); + // // + // // bch?.unconfirmedTxs = { + // // "82da70c660daf4d42abd403795d047918c4021ff1d706b61790cda01a1c5ae5a", + // // }; + // // + // // final result = await bch?.refreshIfThereIsNewData(); + // // + // // expect(result, false); + // // + // // verify(client?.getBatchHistory(args: anyNamed("args"))).called(1); + // // verify(client?.getTransaction( + // // txHash: + // // "a4b6bd97a4b01b4305d0cf02e9bac6b7c37cda2f8e9dfe291ce4170b810ed469", + // // )).called(1); + // // + // // expect(secureStore?.interactions, 0); + // // verifyNoMoreInteractions(client); + // // verifyNoMoreInteractions(cachedClient); + // // verifyNoMoreInteractions(priceAPI); + // // }); + + test("get mnemonic list", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + + // when(client?.getBatchHistory(args: anyNamed("args"))) + // .thenAnswer((thing) async { + // print(jsonEncode(thing.namedArguments.entries.first.value)); + // return {}; + // }); + + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + + final wallet = await Hive.openBox(testWalletId); + + // add maxNumberOfIndexesToCheck and height + await bch?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + expect(await bch?.mnemonic, TEST_MNEMONIC.split(" ")); + // + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test( + "recoverFromMnemonic using empty seed on mainnet fails due to bad genesis hash match", + () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + + bool hasThrown = false; + try { + await bch?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, true); + + verify(client?.getServerFeatures()).called(1); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test( + "recoverFromMnemonic using empty seed on testnet fails due to bad genesis hash match", + () async { + bch = BitcoinCashWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.bitcoincash, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + + bool hasThrown = false; + try { + await bch?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, true); + + verify(client?.getServerFeatures()).called(1); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test( + "recoverFromMnemonic using empty seed on mainnet fails due to attempted overwrite of mnemonic", + () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + + await secureStore?.write( + key: "${testWalletId}_mnemonic", value: "some mnemonic words"); + + bool hasThrown = false; + try { + await bch?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, true); + + verify(client?.getServerFeatures()).called(1); + + expect(secureStore?.interactions, 2); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("recoverFromMnemonic using non empty seed on mainnet succeeds", + () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenAnswer((_) async => historyBatchResponse); + + final wallet = await Hive.openBox(testWalletId); + + bool hasThrown = false; + try { + await bch?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + + expect(secureStore?.interactions, 6); + expect(secureStore?.writes, 3); + expect(secureStore?.reads, 3); + expect(secureStore?.deletes, 0); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("fullRescan succeeds", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenAnswer((_) async => historyBatchResponse); + + when(cachedClient?.clearSharedTransactionCache(coin: Coin.bitcoincash)) + .thenAnswer((realInvocation) async {}); + + final wallet = await Hive.openBox(testWalletId); + + // restore so we have something to rescan + await bch?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + // fetch valid wallet data + final preReceivingAddressesP2PKH = + await wallet.get('receivingAddressesP2PKH'); + final preChangeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); + final preReceivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); + final preChangeIndexP2PKH = await wallet.get('changeIndexP2PKH'); + final preUtxoData = await wallet.get('latest_utxo_model'); + final preReceiveDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2PKH"); + final preChangeDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2PKH"); + + // destroy the data that the rescan will fix + await wallet.put( + 'receivingAddressesP2PKH', ["some address", "some other address"]); + await wallet + .put('changeAddressesP2PKH', ["some address", "some other address"]); + + await wallet.put('receivingIndexP2PKH', 123); + await wallet.put('changeIndexP2PKH', 123); + await secureStore?.write( + key: "${testWalletId}_receiveDerivationsP2PKH", value: "{}"); + await secureStore?.write( + key: "${testWalletId}_changeDerivationsP2PKH", value: "{}"); + + bool hasThrown = false; + try { + await bch?.fullRescan(2, 1000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + // fetch wallet data again + final receivingAddressesP2PKH = + await wallet.get('receivingAddressesP2PKH'); + final changeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); + final receivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); + final changeIndexP2PKH = await wallet.get('changeIndexP2PKH'); + final utxoData = await wallet.get('latest_utxo_model'); + final receiveDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2PKH"); + final changeDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2PKH"); + + expect(preReceivingAddressesP2PKH, receivingAddressesP2PKH); + expect(preChangeAddressesP2PKH, changeAddressesP2PKH); + expect(preReceivingIndexP2PKH, receivingIndexP2PKH); + expect(preChangeIndexP2PKH, changeIndexP2PKH); + expect(preUtxoData, utxoData); + expect(preReceiveDerivationsStringP2PKH, receiveDerivationsStringP2PKH); + expect(preChangeDerivationsStringP2PKH, changeDerivationsStringP2PKH); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs1)).called(2); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.bitcoincash)) + .called(1); + + expect(secureStore?.writes, 9); + expect(secureStore?.reads, 12); + expect(secureStore?.deletes, 2); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("fullRescan fails", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + when(cachedClient?.clearSharedTransactionCache(coin: Coin.bitcoincash)) + .thenAnswer((realInvocation) async {}); + + final wallet = await Hive.openBox(testWalletId); + + // restore so we have something to rescan + await bch?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + // fetch wallet data + final preReceivingAddressesP2PKH = + await wallet.get('receivingAddressesP2PKH'); + + final preChangeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); + final preReceivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); + final preChangeIndexP2PKH = await wallet.get('changeIndexP2PKH'); + final preUtxoData = await wallet.get('latest_utxo_model'); + final preReceiveDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2PKH"); + final preChangeDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2PKH"); + + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenThrow(Exception("fake exception")); + + bool hasThrown = false; + try { + await bch?.fullRescan(2, 1000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, true); + + // fetch wallet data again + final receivingAddressesP2PKH = + await wallet.get('receivingAddressesP2PKH'); + + final changeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); + final receivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); + final changeIndexP2PKH = await wallet.get('changeIndexP2PKH'); + final utxoData = await wallet.get('latest_utxo_model'); + final receiveDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2PKH"); + final changeDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2PKH"); + + expect(preReceivingAddressesP2PKH, receivingAddressesP2PKH); + expect(preChangeAddressesP2PKH, changeAddressesP2PKH); + expect(preReceivingIndexP2PKH, receivingIndexP2PKH); + expect(preChangeIndexP2PKH, changeIndexP2PKH); + expect(preUtxoData, utxoData); + expect(preReceiveDerivationsStringP2PKH, receiveDerivationsStringP2PKH); + expect(preChangeDerivationsStringP2PKH, changeDerivationsStringP2PKH); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(2); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.bitcoincash)) + .called(1); + + expect(secureStore?.writes, 7); + expect(secureStore?.reads, 12); + expect(secureStore?.deletes, 4); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + // // test("fetchBuildTxData succeeds", () async { + // // when(client.getServerFeatures()).thenAnswer((_) async => { + // // "hosts": {}, + // // "pruning": null, + // // "server_version": "Unit tests", + // // "protocol_min": "1.4", + // // "protocol_max": "1.4.2", + // // "genesis_hash": GENESIS_HASH_MAINNET, + // // "hash_function": "sha256", + // // "services": [] + // // }); + // // when(client.getBatchHistory(args: historyBatchArgs0)) + // // .thenAnswer((_) async => historyBatchResponse); + // // when(client.getBatchHistory(args: historyBatchArgs1)) + // // .thenAnswer((_) async => historyBatchResponse); + // // when(cachedClient.getTransaction( + // // tx_hash: + // // "339dac760e4c9c81ed30a7fde7062785cb20712b18e108accdc39800f884fda9", + // // coinName: "bitcoincash", + // // callOutSideMainIsolate: false)) + // // .thenAnswer((_) async => tx9Raw); + // // when(cachedClient.getTransaction( + // // tx_hash: + // // "c2edf283df75cc2724320b866857a82d80266a59d69ab5a7ca12033adbffa44e", + // // coinName: "bitcoincash", + // // callOutSideMainIsolate: false)) + // // .thenAnswer((_) async => tx10Raw); + // // when(cachedClient.getTransaction( + // // tx_hash: + // // "d0c451513bee7d96cb88824d9d720e6b5b90073721b4985b439687f894c3989c", + // // coinName: "bitcoincash", + // // callOutSideMainIsolate: false)) + // // .thenAnswer((_) async => tx11Raw); + // // + // // // recover to fill data + // // await bch.recoverFromMnemonic( + // // mnemonic: TEST_MNEMONIC, + // // maxUnusedAddressGap: 2, + // // maxNumberOfIndexesToCheck: 1000, + // // height: 4000); + // // + // // // modify addresses to trigger all change code branches + // // final chg44 = + // // await secureStore.read(key: testWalletId + "_changeDerivationsP2PKH"); + // // await secureStore.write( + // // key: testWalletId + "_changeDerivationsP2PKH", + // // value: chg44.replaceFirst("1vFHF5q21GccoBwrB4zEUAs9i3Bfx797U", + // // "D5cQWPnhM3RRJVDz8wWC5jWt3PRCfg1zA6")); + // // + // // final data = await bch.fetchBuildTxData(utxoList); + // // + // // expect(data.length, 3); + // // expect( + // // data["339dac760e4c9c81ed30a7fde7062785cb20712b18e108accdc39800f884fda9"] + // // .length, + // // 2); + // // expect( + // // data["c2edf283df75cc2724320b866857a82d80266a59d69ab5a7ca12033adbffa44e"] + // // .length, + // // 3); + // // expect( + // // data["d0c451513bee7d96cb88824d9d720e6b5b90073721b4985b439687f894c3989c"] + // // .length, + // // 2); + // // expect( + // // data["339dac760e4c9c81ed30a7fde7062785cb20712b18e108accdc39800f884fda9"] + // // ["output"], + // // isA()); + // // expect( + // // data["c2edf283df75cc2724320b866857a82d80266a59d69ab5a7ca12033adbffa44e"] + // // ["output"], + // // isA()); + // // expect( + // // data["d0c451513bee7d96cb88824d9d720e6b5b90073721b4985b439687f894c3989c"] + // // ["output"], + // // isA()); + // // expect( + // // data["339dac760e4c9c81ed30a7fde7062785cb20712b18e108accdc39800f884fda9"] + // // ["keyPair"], + // // isA()); + // // expect( + // // data["c2edf283df75cc2724320b866857a82d80266a59d69ab5a7ca12033adbffa44e"] + // // ["keyPair"], + // // isA()); + // // expect( + // // data["d0c451513bee7d96cb88824d9d720e6b5b90073721b4985b439687f894c3989c"] + // // ["keyPair"], + // // isA()); + // // expect( + // // data["c2edf283df75cc2724320b866857a82d80266a59d69ab5a7ca12033adbffa44e"] + // // ["redeemScript"], + // // isA()); + // // + // // // modify addresses to trigger all receiving code branches + // // final rcv44 = await secureStore.read( + // // key: testWalletId + "_receiveDerivationsP2PKH"); + // // await secureStore.write( + // // key: testWalletId + "_receiveDerivationsP2PKH", + // // value: rcv44.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // // "D5cQWPnhM3RRJVDz8wWC5jWt3PRCfg1zA6")); + // // + // // final data2 = await bch.fetchBuildTxData(utxoList); + // // + // // expect(data2.length, 3); + // // expect( + // // data2["339dac760e4c9c81ed30a7fde7062785cb20712b18e108accdc39800f884fda9"] + // // .length, + // // 2); + // // expect( + // // data2["c2edf283df75cc2724320b866857a82d80266a59d69ab5a7ca12033adbffa44e"] + // // .length, + // // 3); + // // expect( + // // data2["d0c451513bee7d96cb88824d9d720e6b5b90073721b4985b439687f894c3989c"] + // // .length, + // // 2); + // // expect( + // // data2["339dac760e4c9c81ed30a7fde7062785cb20712b18e108accdc39800f884fda9"] + // // ["output"], + // // isA()); + // // expect( + // // data2["c2edf283df75cc2724320b866857a82d80266a59d69ab5a7ca12033adbffa44e"] + // // ["output"], + // // isA()); + // // expect( + // // data2["d0c451513bee7d96cb88824d9d720e6b5b90073721b4985b439687f894c3989c"] + // // ["output"], + // // isA()); + // // expect( + // // data2["339dac760e4c9c81ed30a7fde7062785cb20712b18e108accdc39800f884fda9"] + // // ["keyPair"], + // // isA()); + // // expect( + // // data2["c2edf283df75cc2724320b866857a82d80266a59d69ab5a7ca12033adbffa44e"] + // // ["keyPair"], + // // isA()); + // // expect( + // // data2["d0c451513bee7d96cb88824d9d720e6b5b90073721b4985b439687f894c3989c"] + // // ["keyPair"], + // // isA()); + // // expect( + // // data2["c2edf283df75cc2724320b866857a82d80266a59d69ab5a7ca12033adbffa44e"] + // // ["redeemScript"], + // // isA()); + // // + // // verify(client.getServerFeatures()).called(1); + // // verify(cachedClient.getTransaction( + // // tx_hash: + // // "339dac760e4c9c81ed30a7fde7062785cb20712b18e108accdc39800f884fda9", + // // coinName: "bitcoincash", + // // callOutSideMainIsolate: false)) + // // .called(2); + // // verify(cachedClient.getTransaction( + // // tx_hash: + // // "c2edf283df75cc2724320b866857a82d80266a59d69ab5a7ca12033adbffa44e", + // // coinName: "bitcoincash", + // // callOutSideMainIsolate: false)) + // // .called(2); + // // verify(cachedClient.getTransaction( + // // tx_hash: + // // "d0c451513bee7d96cb88824d9d720e6b5b90073721b4985b439687f894c3989c", + // // coinName: "bitcoincash", + // // callOutSideMainIsolate: false)) + // // .called(2); + // // verify(client.getBatchHistory(args: historyBatchArgs0)).called(1); + // // verify(client.getBatchHistory(args: historyBatchArgs1)).called(1); + // // + // // expect(secureStore.interactions, 38); + // // expect(secureStore.writes, 13); + // // expect(secureStore.reads, 25); + // // expect(secureStore.deletes, 0); + // // + // // verifyNoMoreInteractions(client); + // // verifyNoMoreInteractions(cachedClient); + // // verifyNoMoreInteractions(priceAPI); + // // }); + + // test("fetchBuildTxData throws", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(cachedClient?.getTransaction( + // txHash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .thenThrow(Exception("some exception")); + // + // // recover to fill data + // await bch?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000, + // height: 4000); + // + // bool didThrow = false; + // try { + // await bch?.fetchBuildTxData(utxoList); + // } catch (_) { + // didThrow = true; + // } + // expect(didThrow, true); + // + // verify(client?.getServerFeatures()).called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // + // expect(secureStore?.interactions, 14); + // expect(secureStore?.writes, 7); + // expect(secureStore?.reads, 7); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("build transaction succeeds", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(cachedClient?.getTransaction( + // txHash: + // "e9673acb3bfa928f92a7d5a545151a672e9613fdf972f3849e16094c1ed28268", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "fa5bfa4eb581bedb28ca96a65ee77d8e81159914b70d5b7e215994221cc02a63", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "694617f0000499be2f6af5f8d1ddbcf1a70ad4710c0cee6f33a13a64bba454ed", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx11Raw); + // + // // recover to fill data + // await bch?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000, + // height: 4000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "D5cQWPnhM3RRJVDz8wWC5jWt3PRCfg1zA6")); + // + // final data = await bch?.fetchBuildTxData(utxoList); + // + // final txData = await bch?.buildTransaction( + // utxosToUse: utxoList, + // utxoSigningData: data!, + // recipients: ["DS7cKFKdfbarMrYjFBQqEcHR5km6D51c74"], + // satoshiAmounts: [13000]); + // + // expect(txData?.length, 2); + // expect(txData?["hex"], isA()); + // expect(txData?["vSize"], isA()); + // + // verify(client?.getServerFeatures()).called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "d3054c63fe8cfafcbf67064ec66b9fbe1ac293860b5d6ffaddd39546658b72de", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "fa5bfa4eb581bedb28ca96a65ee77d8e81159914b70d5b7e215994221cc02a63", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "694617f0000499be2f6af5f8d1ddbcf1a70ad4710c0cee6f33a13a64bba454ed", + // coin: Coin.bitcoincash, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // + // expect(secureStore?.interactions, 26); + // expect(secureStore?.writes, 10); + // expect(secureStore?.reads, 16); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + test("confirmSend error 1", () async { + bool didThrow = false; + try { + await bch?.confirmSend(txData: 1); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("confirmSend error 2", () async { + bool didThrow = false; + try { + await bch?.confirmSend(txData: 2); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("confirmSend some other error code", () async { + bool didThrow = false; + try { + await bch?.confirmSend(txData: 42); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("confirmSend no hex", () async { + bool didThrow = false; + try { + await bch?.confirmSend(txData: {"some": "strange map"}); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("confirmSend fails due to vSize being greater than fee", () async { + bool didThrow = false; + try { + await bch + ?.confirmSend(txData: {"hex": "a string", "fee": 1, "vSize": 10}); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + verify(client?.broadcastTransaction( + rawTx: "a string", requestID: anyNamed("requestID"))) + .called(1); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("confirmSend fails when broadcast transactions throws", () async { + when(client?.broadcastTransaction( + rawTx: "a string", requestID: anyNamed("requestID"))) + .thenThrow(Exception("some exception")); + + bool didThrow = false; + try { + await bch + ?.confirmSend(txData: {"hex": "a string", "fee": 10, "vSize": 10}); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + verify(client?.broadcastTransaction( + rawTx: "a string", requestID: anyNamed("requestID"))) + .called(1); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("refresh wallet mutex locked", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + + final wallet = await Hive.openBox(testWalletId); + + // recover to fill data + await bch?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + bch?.refreshMutex = true; + + await bch?.refresh(); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + + expect(secureStore?.interactions, 6); + expect(secureStore?.writes, 3); + expect(secureStore?.reads, 3); + expect(secureStore?.deletes, 0); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("refresh wallet throws", () async { + when(client?.getBlockHeadTip()).thenThrow(Exception("some exception")); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + when(client?.getHistory(scripthash: anyNamed("scripthash"))) + .thenThrow(Exception("some exception")); + + final wallet = await Hive.openBox(testWalletId); + + // recover to fill data + await bch?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + await bch?.refresh(); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + verify(client?.getBlockHeadTip()).called(1); + verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); + + expect(secureStore?.interactions, 6); + expect(secureStore?.writes, 3); + expect(secureStore?.reads, 3); + expect(secureStore?.deletes, 0); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + // test("refresh wallet normally", () async { + // when(client?.getBlockHeadTip()).thenAnswer((realInvocation) async => + // {"height": 520481, "hex": "some block hex"}); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getHistory(scripthash: anyNamed("scripthash"))) + // .thenAnswer((_) async => []); + // when(client?.estimateFee(blocks: anyNamed("blocks"))) + // .thenAnswer((_) async => Decimal.one); + // // when(priceAPI?.getPricesAnd24hChange(baseCurrency: "USD")) + // // .thenAnswer((_) async => Decimal.one); + // + // await Hive.openBox(testWalletId); + // await Hive.openBox(DB.boxNamePrefs); + // + // // recover to fill data + // await bch?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000, + // height: 4000); + // + // when(client?.getBatchHistory(args: anyNamed("args"))) + // .thenAnswer((_) async => {}); + // when(client?.getBatchUTXOs(args: anyNamed("args"))) + // .thenAnswer((_) async => emptyHistoryBatchResponse); + // + // await bch?.refresh(); + // + // verify(client?.getServerFeatures()).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // verify(client?.getBatchHistory(args: anyNamed("args"))).called(1); + // verify(client?.getBatchUTXOs(args: anyNamed("args"))).called(1); + // verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(2); + // verify(client?.estimateFee(blocks: anyNamed("blocks"))).called(3); + // verify(client?.getBlockHeadTip()).called(1); + // // verify(priceAPI?.getPricesAnd24hChange(baseCurrency: "USD")).called(2); + // + // expect(secureStore?.interactions, 6); + // expect(secureStore?.writes, 2); + // expect(secureStore?.reads, 2); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + }); + + tearDown(() async { + await tearDownTestHive(); + }); +} diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart new file mode 100644 index 000000000..fa38e1f9e --- /dev/null +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -0,0 +1,348 @@ +// Mocks generated by Mockito 5.2.0 from annotations +// in stackwallet/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i7; + +import 'package:decimal/decimal.dart' as _i4; +import 'package:http/http.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i5; +import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i6; +import 'package:stackwallet/services/price.dart' as _i9; +import 'package:stackwallet/services/transaction_notification_tracker.dart' + as _i11; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i8; +import 'package:stackwallet/utilities/prefs.dart' as _i2; +import 'package:tuple/tuple.dart' as _i10; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakePrefs_0 extends _i1.Fake implements _i2.Prefs {} + +class _FakeClient_1 extends _i1.Fake implements _i3.Client {} + +class _FakeDecimal_2 extends _i1.Fake implements _i4.Decimal {} + +/// A class which mocks [CachedElectrumX]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCachedElectrumX extends _i1.Mock implements _i5.CachedElectrumX { + MockCachedElectrumX() { + _i1.throwOnMissingStub(this); + } + + @override + String get server => + (super.noSuchMethod(Invocation.getter(#server), returnValue: '') + as String); + @override + int get port => + (super.noSuchMethod(Invocation.getter(#port), returnValue: 0) as int); + @override + bool get useSSL => + (super.noSuchMethod(Invocation.getter(#useSSL), returnValue: false) + as bool); + @override + _i2.Prefs get prefs => (super.noSuchMethod(Invocation.getter(#prefs), + returnValue: _FakePrefs_0()) as _i2.Prefs); + @override + List<_i6.ElectrumXNode> get failovers => + (super.noSuchMethod(Invocation.getter(#failovers), + returnValue: <_i6.ElectrumXNode>[]) as List<_i6.ElectrumXNode>); + @override + _i7.Future> getAnonymitySet( + {String? groupId, String? blockhash = r'', _i8.Coin? coin}) => + (super.noSuchMethod( + Invocation.method(#getAnonymitySet, [], + {#groupId: groupId, #blockhash: blockhash, #coin: coin}), + returnValue: + Future>.value({})) + as _i7.Future>); + @override + _i7.Future> getTransaction( + {String? txHash, _i8.Coin? coin, bool? verbose = true}) => + (super.noSuchMethod( + Invocation.method(#getTransaction, [], + {#txHash: txHash, #coin: coin, #verbose: verbose}), + returnValue: + Future>.value({})) + as _i7.Future>); + @override + _i7.Future> getUsedCoinSerials( + {_i8.Coin? coin, int? startNumber = 0}) => + (super.noSuchMethod( + Invocation.method(#getUsedCoinSerials, [], + {#coin: coin, #startNumber: startNumber}), + returnValue: Future>.value([])) + as _i7.Future>); + @override + _i7.Future clearSharedTransactionCache({_i8.Coin? coin}) => + (super.noSuchMethod( + Invocation.method(#clearSharedTransactionCache, [], {#coin: coin}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i7.Future); +} + +/// A class which mocks [PriceAPI]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPriceAPI extends _i1.Mock implements _i9.PriceAPI { + MockPriceAPI() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Client get client => (super.noSuchMethod(Invocation.getter(#client), + returnValue: _FakeClient_1()) as _i3.Client); + @override + void resetLastCalledToForceNextCallToUpdateCache() => super.noSuchMethod( + Invocation.method(#resetLastCalledToForceNextCallToUpdateCache, []), + returnValueForMissingStub: null); + @override + _i7.Future>> + getPricesAnd24hChange({String? baseCurrency}) => (super.noSuchMethod( + Invocation.method( + #getPricesAnd24hChange, [], {#baseCurrency: baseCurrency}), + returnValue: + Future>>.value( + <_i8.Coin, _i10.Tuple2<_i4.Decimal, double>>{})) + as _i7.Future>>); +} + +/// A class which mocks [TransactionNotificationTracker]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTransactionNotificationTracker extends _i1.Mock + implements _i11.TransactionNotificationTracker { + MockTransactionNotificationTracker() { + _i1.throwOnMissingStub(this); + } + + @override + String get walletId => + (super.noSuchMethod(Invocation.getter(#walletId), returnValue: '') + as String); + @override + List get pendings => + (super.noSuchMethod(Invocation.getter(#pendings), returnValue: []) + as List); + @override + List get confirmeds => (super + .noSuchMethod(Invocation.getter(#confirmeds), returnValue: []) + as List); + @override + bool wasNotifiedPending(String? txid) => + (super.noSuchMethod(Invocation.method(#wasNotifiedPending, [txid]), + returnValue: false) as bool); + @override + _i7.Future addNotifiedPending(String? txid) => + (super.noSuchMethod(Invocation.method(#addNotifiedPending, [txid]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i7.Future); + @override + bool wasNotifiedConfirmed(String? txid) => + (super.noSuchMethod(Invocation.method(#wasNotifiedConfirmed, [txid]), + returnValue: false) as bool); + @override + _i7.Future addNotifiedConfirmed(String? txid) => + (super.noSuchMethod(Invocation.method(#addNotifiedConfirmed, [txid]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i7.Future); +} + +/// A class which mocks [ElectrumX]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockElectrumX extends _i1.Mock implements _i6.ElectrumX { + @override + set failovers(List<_i6.ElectrumXNode>? _failovers) => + super.noSuchMethod(Invocation.setter(#failovers, _failovers), + returnValueForMissingStub: null); + @override + int get currentFailoverIndex => + (super.noSuchMethod(Invocation.getter(#currentFailoverIndex), + returnValue: 0) as int); + @override + set currentFailoverIndex(int? _currentFailoverIndex) => super.noSuchMethod( + Invocation.setter(#currentFailoverIndex, _currentFailoverIndex), + returnValueForMissingStub: null); + @override + String get host => + (super.noSuchMethod(Invocation.getter(#host), returnValue: '') as String); + @override + int get port => + (super.noSuchMethod(Invocation.getter(#port), returnValue: 0) as int); + @override + bool get useSSL => + (super.noSuchMethod(Invocation.getter(#useSSL), returnValue: false) + as bool); + @override + _i7.Future request( + {String? command, + List? args = const [], + Duration? connectionTimeout = const Duration(seconds: 60), + String? requestID, + int? retries = 2}) => + (super.noSuchMethod( + Invocation.method(#request, [], { + #command: command, + #args: args, + #connectionTimeout: connectionTimeout, + #requestID: requestID, + #retries: retries + }), + returnValue: Future.value()) as _i7.Future); + @override + _i7.Future>> batchRequest( + {String? command, + Map>? args, + Duration? connectionTimeout = const Duration(seconds: 60), + int? retries = 2}) => + (super.noSuchMethod( + Invocation.method(#batchRequest, [], { + #command: command, + #args: args, + #connectionTimeout: connectionTimeout, + #retries: retries + }), + returnValue: Future>>.value( + >[])) + as _i7.Future>>); + @override + _i7.Future ping({String? requestID, int? retryCount = 1}) => + (super.noSuchMethod( + Invocation.method( + #ping, [], {#requestID: requestID, #retryCount: retryCount}), + returnValue: Future.value(false)) as _i7.Future); + @override + _i7.Future> getBlockHeadTip({String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getBlockHeadTip, [], {#requestID: requestID}), + returnValue: + Future>.value({})) + as _i7.Future>); + @override + _i7.Future> getServerFeatures({String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getServerFeatures, [], {#requestID: requestID}), + returnValue: + Future>.value({})) as _i7 + .Future>); + @override + _i7.Future broadcastTransaction({String? rawTx, String? requestID}) => + (super.noSuchMethod( + Invocation.method(#broadcastTransaction, [], + {#rawTx: rawTx, #requestID: requestID}), + returnValue: Future.value('')) as _i7.Future); + @override + _i7.Future> getBalance( + {String? scripthash, String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getBalance, [], + {#scripthash: scripthash, #requestID: requestID}), + returnValue: + Future>.value({})) + as _i7.Future>); + @override + _i7.Future>> getHistory( + {String? scripthash, String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getHistory, [], + {#scripthash: scripthash, #requestID: requestID}), + returnValue: Future>>.value( + >[])) + as _i7.Future>>); + @override + _i7.Future>>> getBatchHistory( + {Map>? args}) => + (super.noSuchMethod( + Invocation.method(#getBatchHistory, [], {#args: args}), + returnValue: Future>>>.value( + >>{})) as _i7 + .Future>>>); + @override + _i7.Future>> getUTXOs( + {String? scripthash, String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getUTXOs, [], {#scripthash: scripthash, #requestID: requestID}), + returnValue: Future>>.value( + >[])) as _i7 + .Future>>); + @override + _i7.Future>>> getBatchUTXOs( + {Map>? args}) => + (super.noSuchMethod(Invocation.method(#getBatchUTXOs, [], {#args: args}), + returnValue: Future>>>.value( + >>{})) as _i7 + .Future>>>); + @override + _i7.Future> getTransaction( + {String? txHash, bool? verbose = true, String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getTransaction, [], + {#txHash: txHash, #verbose: verbose, #requestID: requestID}), + returnValue: + Future>.value({})) + as _i7.Future>); + @override + _i7.Future> getAnonymitySet( + {String? groupId = r'1', + String? blockhash = r'', + String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getAnonymitySet, [], { + #groupId: groupId, + #blockhash: blockhash, + #requestID: requestID + }), + returnValue: + Future>.value({})) + as _i7.Future>); + @override + _i7.Future getMintData({dynamic mints, String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getMintData, [], {#mints: mints, #requestID: requestID}), + returnValue: Future.value()) as _i7.Future); + @override + _i7.Future> getUsedCoinSerials( + {String? requestID, int? startNumber}) => + (super.noSuchMethod( + Invocation.method(#getUsedCoinSerials, [], + {#requestID: requestID, #startNumber: startNumber}), + returnValue: + Future>.value({})) + as _i7.Future>); + @override + _i7.Future getLatestCoinId({String? requestID}) => (super.noSuchMethod( + Invocation.method(#getLatestCoinId, [], {#requestID: requestID}), + returnValue: Future.value(0)) as _i7.Future); + @override + _i7.Future> getFeeRate({String? requestID}) => (super + .noSuchMethod(Invocation.method(#getFeeRate, [], {#requestID: requestID}), + returnValue: + Future>.value({})) as _i7 + .Future>); + @override + _i7.Future<_i4.Decimal> estimateFee({String? requestID, int? blocks}) => + (super.noSuchMethod( + Invocation.method( + #estimateFee, [], {#requestID: requestID, #blocks: blocks}), + returnValue: Future<_i4.Decimal>.value(_FakeDecimal_2())) + as _i7.Future<_i4.Decimal>); + @override + _i7.Future<_i4.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + Invocation.method(#relayFee, [], {#requestID: requestID}), + returnValue: Future<_i4.Decimal>.value(_FakeDecimal_2())) + as _i7.Future<_i4.Decimal>); +} diff --git a/test/services/coins/namecoin/namecoin_history_sample_data.dart b/test/services/coins/namecoin/namecoin_history_sample_data.dart new file mode 100644 index 000000000..baa0535ec --- /dev/null +++ b/test/services/coins/namecoin/namecoin_history_sample_data.dart @@ -0,0 +1,95 @@ +final Map> historyBatchArgs0 = { + "k_0_0": ["bd84ae7e09414b0ccf5dcbf70a1f89f2fd42119a98af35dd4ecc80210fed0487"], + "k_0_1": ["3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6"] +}; +final Map> historyBatchArgs1 = { + "k_0_0": ["dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7"], + "k_0_1": ["71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d"] +}; +final Map> historyBatchArgs2 = { + "k_0_0": ["c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9"] +}; + +final Map>> historyBatchResponse = { + "k_0_0": [], + "s_0_0": [{}, {}], + "w_0_0": [], + "k_0_1": [{}], + "s_0_1": [], + "w_0_1": [{}, {}, {}], + "k_0_2": [], + "s_0_2": [], + "w_0_2": [], + "k_0_3": [], + "s_0_3": [], + "w_0_3": [], + "k_0_4": [], + "s_0_4": [], + "w_0_4": [], + "k_0_5": [], + "s_0_5": [], + "w_0_5": [], + "k_0_6": [], + "s_0_6": [], + "w_0_6": [], + "k_0_7": [], + "s_0_7": [], + "w_0_7": [], + "k_0_8": [], + "s_0_8": [], + "w_0_8": [], + "k_0_9": [], + "s_0_9": [], + "w_0_9": [], + "k_0_10": [], + "s_0_10": [], + "w_0_10": [], + "k_0_11": [], + "s_0_11": [], + "w_0_11": [] +}; + +final Map>> emptyHistoryBatchResponse = { + "k_0_0": [], + "s_0_0": [], + "w_0_0": [], + "k_0_1": [], + "s_0_1": [], + "w_0_1": [], + "k_0_2": [], + "s_0_2": [], + "w_0_2": [], + "k_0_3": [], + "s_0_3": [], + "w_0_3": [], + "k_0_4": [], + "s_0_4": [], + "w_0_4": [], + "k_0_5": [], + "s_0_5": [], + "w_0_5": [], + "k_0_6": [], + "s_0_6": [], + "w_0_6": [], + "k_0_7": [], + "s_0_7": [], + "w_0_7": [], + "k_0_8": [], + "s_0_8": [], + "w_0_8": [], + "k_0_9": [], + "s_0_9": [], + "w_0_9": [], + "k_0_10": [], + "s_0_10": [], + "w_0_10": [], + "k_0_11": [], + "s_0_11": [], + "w_0_11": [] +}; + +final List activeScriptHashes = [ + "83b744ccb88827d544081c1a03ea782a7d00d6224ff9fddb7d0fbad399e1cae7", + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874", + "5baba32b1899d5e740838559ef39b7d8e9ba302bd24b732eeedd4c0e6ec65b51", +]; diff --git a/test/services/coins/namecoin/namecoin_transaction_data_samples.dart b/test/services/coins/namecoin/namecoin_transaction_data_samples.dart new file mode 100644 index 000000000..2199cfcc3 --- /dev/null +++ b/test/services/coins/namecoin/namecoin_transaction_data_samples.dart @@ -0,0 +1,355 @@ +import 'package:stackwallet/models/paymint/transactions_model.dart'; + +final transactionData = TransactionData.fromMap({ + "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6": tx1, + "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7": tx2, + "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d": tx3, + "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9": tx4, +}); + +final tx1 = Transaction( + txid: "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6", + confirmedStatus: true, + confirmations: 212, + txType: "Received", + amount: 1000000, + fees: 23896, + height: 629633, + address: "nc1qwfda4s9qmdqpnykgpjf85n09ath983srtuxcqx", + timestamp: 1663093275, + worthNow: "0.00", + worthAtBlockTimestamp: "0.00", + inputSize: 2, + outputSize: 2, + inputs: [ + Input( + txid: "290904699ccbebd0921c4acc4f7a10f41141ee6a07bc64ebca5674c1e5ee8dfa", + vout: 1, + ), + Input( + txid: "bd84ae7e09414b0ccf5dcbf70a1f89f2fd42119a98af35dd4ecc80210fed0487", + vout: 0, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "nc1qwfda4s9qmdqpnykgpjf85n09ath983srtuxcqx", + value: 1000000, + ), + Output( + scriptpubkeyAddress: "nc1qp7h7fxcnkqcpul202z6nh8yjy8jpt39jcpeapj", + value: 29853562, + ) + ], +); + +final tx2 = Transaction( + txid: "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + confirmedStatus: true, + confirmations: 150, + txType: "Sent", + amount: 988567, + fees: 11433, + height: 629695, + address: "nc1qraffwaq3cxngwp609e03ynwsx8ykgjnjve9f3y", + timestamp: 1663142110, + worthNow: "0.00", + worthAtBlockTimestamp: "0.00", + inputSize: 1, + outputSize: 1, + inputs: [ + Input( + txid: "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6", + vout: 0, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "nc1qraffwaq3cxngwp609e03ynwsx8ykgjnjve9f3y", + value: 988567, + ), + ], +); + +final tx3 = Transaction( + txid: "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + confirmedStatus: true, + confirmations: 147, + txType: "Received", + amount: 988567, + fees: 11433, + height: 629699, + address: "nc1qw4srwqq2semrxje4x6zcrg53g07q0pr3yqv5kr", + timestamp: 1663145287, + worthNow: "0.00", + worthAtBlockTimestamp: "0.00", + inputSize: 2, + outputSize: 1, + inputs: [ + Input( + txid: "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + vout: 0, + ), + Input( + txid: "80f8c6de5be2243013348219bbb7043a6d8d00ddc716baf6a69eab517f9a6fc1", + vout: 1, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "nc1qw4srwqq2semrxje4x6zcrg53g07q0pr3yqv5kr", + value: 1000000, + ), + Output( + scriptpubkeyAddress: "nc1qsgr7u4hd22rc64r9vlef69en9wzlvmjt8dzyrm", + value: 28805770, + ), + ], +); + +final tx4 = Transaction( + txid: "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", + confirmedStatus: true, + confirmations: 130, + txType: "Sent", + amount: 988567, + fees: 11433, + height: 629717, + address: "nc1qmdt0fxhpwx7x5ymmm9gvh229adu0kmtukfcsjk", + timestamp: 1663155739, + worthNow: "0.00", + worthAtBlockTimestamp: "0.00", + inputSize: 1, + outputSize: 1, + inputs: [ + Input( + txid: "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + vout: 0, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "nc1qmdt0fxhpwx7x5ymmm9gvh229adu0kmtukfcsjk", + value: 988567, + ), + ], +); + +final tx1Raw = { + "txid": "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6", + "hash": "40c8dd876cf111dc00d3aa2fedc93a77c18b391931939d4f99a760226cbff675", + "version": 2, + "size": 394, + "vsize": 232, + "weight": 925, + "locktime": 0, + "vin": [ + { + "txid": + "290904699ccbebd0921c4acc4f7a10f41141ee6a07bc64ebca5674c1e5ee8dfa", + "vout": 1, + "scriptSig": { + "asm": "001466d2173325f3d379c6beb0a4949e937308edb152", + "hex": "16001466d2173325f3d379c6beb0a4949e937308edb152" + }, + "txinwitness": [ + "3044022062d0f32dc051ed1e91889a96070121c77d895f69d2ed5a307d8b320e0352186702206a0c2613e708e5ef8a935aba61b8fa14ddd6ca4e9a80a8b4ded126a879217dd101", + "0303cd92ed121ef22398826af055f3006769210e019f8fb43bd2f5556282d84997" + ], + "sequence": 4294967295 + }, + { + "txid": + "bd84ae7e09414b0ccf5dcbf70a1f89f2fd42119a98af35dd4ecc80210fed0487", + "vout": 0, + "scriptSig": {"asm": "", "hex": ""}, + "txinwitness": [ + "3045022100e8814706766a2d7588908c51209c3b7095241bbc681febdd6b317b7e9b6ea97502205c33c63e4d8a675c19122bfe0057afce2159e6bd86f2c9aced214de77099dc8b01", + "03c35212e3a4c0734735eccae9219987dc78d9cf6245ab247942d430d0a01d61be" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.01, + "n": 0, + "scriptPubKey": { + "asm": "0 725bdac0a0db401992c80c927a4de5eaee53c603", + "hex": "0014725bdac0a0db401992c80c927a4de5eaee53c603", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": ["nc1qwfda4s9qmdqpnykgpjf85n09ath983srtuxcqx"] + } + }, + { + "value": 0.29853562, + "n": 1, + "scriptPubKey": { + "asm": "0 0fafe49b13b0301e7d4f50b53b9c9221e415c4b2", + "hex": "00140fafe49b13b0301e7d4f50b53b9c9221e415c4b2", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": ["nc1qp7h7fxcnkqcpul202z6nh8yjy8jpt39jcpeapj"] + } + } + ], + "hex": + "02000000000102fa8deee5c17456caeb64bc076aee4111f4107a4fcc4a1c92d0ebcb9c69040929010000001716001466d2173325f3d379c6beb0a4949e937308edb152ffffffff8704ed0f2180cc4edd35af989a1142fdf2891f0af7cb5dcf0c4b41097eae84bd0000000000ffffffff0240420f0000000000160014725bdac0a0db401992c80c927a4de5eaee53c6037a87c701000000001600140fafe49b13b0301e7d4f50b53b9c9221e415c4b202473044022062d0f32dc051ed1e91889a96070121c77d895f69d2ed5a307d8b320e0352186702206a0c2613e708e5ef8a935aba61b8fa14ddd6ca4e9a80a8b4ded126a879217dd101210303cd92ed121ef22398826af055f3006769210e019f8fb43bd2f5556282d8499702483045022100e8814706766a2d7588908c51209c3b7095241bbc681febdd6b317b7e9b6ea97502205c33c63e4d8a675c19122bfe0057afce2159e6bd86f2c9aced214de77099dc8b012103c35212e3a4c0734735eccae9219987dc78d9cf6245ab247942d430d0a01d61be00000000", + "blockhash": + "c9f53cc7cbf654cbcc400e17b33e03a32706d6e6647ad7085c688540f980a378", + "confirmations": 212, + "time": 1663093275, + "blocktime": 1663093275 +}; + +final tx2Raw = { + "txid": "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + "hash": "32dbc0d21327e0cb94ec6069a8d235affd99689ffc5f68959bfb720bafc04bcf", + "version": 2, + "size": 192, + "vsize": 110, + "weight": 438, + "locktime": 0, + "vin": [ + { + "txid": + "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6", + "vout": 0, + "scriptSig": {"asm": "", "hex": ""}, + "txinwitness": [ + "30450221009d58ebfaab8eae297910bca93a7fd48f94ce52a1731cf27fb4c043368fa10e8d02207e88f5d868113d9567999793be0a5b752ad704d04224046839763cefe46463a501", + "02f6ca5274b59dfb014f6a0d690671964290dac7f97fe825f723204e6cb8daf086" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.00988567, + "n": 0, + "scriptPubKey": { + "asm": "0 1f52977411c1a687074f2e5f124dd031c9644a72", + "hex": "00141f52977411c1a687074f2e5f124dd031c9644a72", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": ["nc1qraffwaq3cxngwp609e03ynwsx8ykgjnjve9f3y"] + } + } + ], + "hex": + "02000000000101c6ccf4ddc2a21434ed634636378923d01014b2d3b2f124999f3e7c88d043f53e0000000000ffffffff0197150f00000000001600141f52977411c1a687074f2e5f124dd031c9644a72024830450221009d58ebfaab8eae297910bca93a7fd48f94ce52a1731cf27fb4c043368fa10e8d02207e88f5d868113d9567999793be0a5b752ad704d04224046839763cefe46463a5012102f6ca5274b59dfb014f6a0d690671964290dac7f97fe825f723204e6cb8daf08600000000", + "blockhash": + "ae1129ee834853c45b9edbb7228497c7fa423d7d1bdec8fd155f9e3c429c84d3", + "confirmations": 150, + "time": 1663142110, + "blocktime": 1663142110 +}; + +final tx3Raw = { + "txid": "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + "hash": "bb25567e1ffb2fd6ec9aa3925a7a8dd3055a29521f7811b2b2bc01ce7d8a216e", + "version": 2, + "size": 370, + "vsize": 208, + "weight": 832, + "locktime": 0, + "vin": [ + { + "txid": + "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + "vout": 0, + "scriptSig": {"asm": "", "hex": ""}, + "txinwitness": [ + "304402203535cf570aca7c1acfa6e8d2f43e0b188b76d0b7a75ffca448e6af953ffe8b6302202ea52b312aaaf6d615d722bd92535d1e8b25fa9584a8dbe34dfa1ea9c18105ca01", + "038b68078a95f73f8710e8464dec52c61f9e21675ddf69d4f61b93cc417cf73d74" + ], + "sequence": 4294967295 + }, + { + "txid": + "80f8c6de5be2243013348219bbb7043a6d8d00ddc716baf6a69eab517f9a6fc1", + "vout": 1, + "scriptSig": {"asm": "", "hex": ""}, + "txinwitness": [ + "3044022045268613674326251c46caeaf435081ca753e4ee2018d79480c4930ad7d5e19f022050090a9add82e7272b8206b9d369675e7e9a5f1396fc93490143f0053666102901", + "028e2ede901e69887cb80603c8e207839f61a477d59beff17705162a2045dd974e" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.01, + "n": 0, + "scriptPubKey": { + "asm": "0 756037000a8676334b35368581a29143fc078471", + "hex": "0014756037000a8676334b35368581a29143fc078471", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": ["nc1qw4srwqq2semrxje4x6zcrg53g07q0pr3yqv5kr"] + } + }, + { + "value": 0.2880577, + "n": 1, + "scriptPubKey": { + "asm": "0 8207ee56ed52878d546567f29d17332b85f66e4b", + "hex": "00148207ee56ed52878d546567f29d17332b85f66e4b", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": ["nc1qsgr7u4hd22rc64r9vlef69en9wzlvmjt8dzyrm"] + } + } + ], + "hex": + "02000000000102d7609f2ebf00afdc6b8cda9a5e92b4b9a0b8aaafadf890fbf99721854395fadf0000000000ffffffffc16f9a7f51ab9ea6f6ba16c7dd008d6d3a04b7bb198234133024e25bdec6f8800100000000ffffffff0240420f0000000000160014756037000a8676334b35368581a29143fc0784718a8ab701000000001600148207ee56ed52878d546567f29d17332b85f66e4b0247304402203535cf570aca7c1acfa6e8d2f43e0b188b76d0b7a75ffca448e6af953ffe8b6302202ea52b312aaaf6d615d722bd92535d1e8b25fa9584a8dbe34dfa1ea9c18105ca0121038b68078a95f73f8710e8464dec52c61f9e21675ddf69d4f61b93cc417cf73d7402473044022045268613674326251c46caeaf435081ca753e4ee2018d79480c4930ad7d5e19f022050090a9add82e7272b8206b9d369675e7e9a5f1396fc93490143f005366610290121028e2ede901e69887cb80603c8e207839f61a477d59beff17705162a2045dd974e00000000", + "blockhash": + "98f388ba99e3b6fc421c23edf3c699ada082b01e5a5d130af7550b7fa6184f2f", + "confirmations": 147, + "time": 1663145287, + "blocktime": 1663145287 +}; + +final tx4Raw = { + "txid": "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", + "hash": "c6b544ddd7d901fcc7218208a6cfc8e1819c403a22cc8a1f1a7029aafa427925", + "version": 2, + "size": 192, + "vsize": 110, + "weight": 438, + "locktime": 0, + "vin": [ + { + "txid": + "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + "vout": 0, + "scriptSig": {"asm": "", "hex": ""}, + "txinwitness": [ + "3045022100c664c6ad206999e019954c5206a26c2eca1ae2572288c0f78074c279a4a210ce022017456fdf85f744d694fa2e4638acee782d809268ea4808c04d91da3ac4fe7fd401", + "035456b63e86c0a6235cb3debfb9654966a4c2362ec678ae3b9beec53d31a25eba" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.00988567, + "n": 0, + "scriptPubKey": { + "asm": "0 db56f49ae171bc6a137bd950cba945eb78fb6d7c", + "hex": "0014db56f49ae171bc6a137bd950cba945eb78fb6d7c", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": ["nc1qmdt0fxhpwx7x5ymmm9gvh229adu0kmtukfcsjk"] + } + } + ], + "hex": + "020000000001014da0dde1ee465c062356dd3e2f9d04430753148b0f0dc3d81b32e7e93265b5710000000000ffffffff0197150f0000000000160014db56f49ae171bc6a137bd950cba945eb78fb6d7c02483045022100c664c6ad206999e019954c5206a26c2eca1ae2572288c0f78074c279a4a210ce022017456fdf85f744d694fa2e4638acee782d809268ea4808c04d91da3ac4fe7fd40121035456b63e86c0a6235cb3debfb9654966a4c2362ec678ae3b9beec53d31a25eba00000000", + "blockhash": + "6f60029ff3a32ca2d7e7e23c02b9cb35f61e7f9481992f9c3ded2c60c7b1de9b", + "confirmations": 130, + "time": 1663155739, + "blocktime": 1663155739 +}; diff --git a/test/services/coins/namecoin/namecoin_utxo_sample_data.dart b/test/services/coins/namecoin/namecoin_utxo_sample_data.dart new file mode 100644 index 000000000..d54f00f24 --- /dev/null +++ b/test/services/coins/namecoin/namecoin_utxo_sample_data.dart @@ -0,0 +1,58 @@ +import 'package:stackwallet/models/paymint/utxo_model.dart'; + +final Map>> batchGetUTXOResponse0 = { + "some id 0": [ + { + "tx_pos": 0, + "value": 988567, + "tx_hash": + "32dbc0d21327e0cb94ec6069a8d235affd99689ffc5f68959bfb720bafc04bcf", + "height": 629695 + }, + { + "tx_pos": 0, + "value": 1000000, + "tx_hash": + "40c8dd876cf111dc00d3aa2fedc93a77c18b391931939d4f99a760226cbff675", + "height": 629633 + }, + ], + "some id 1": [], +}; + +final utxoList = [ + UtxoObject( + txid: "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + vout: 0, + status: Status( + confirmed: true, + confirmations: 150, + blockHeight: 629695, + blockTime: 1663142110, + blockHash: + "32dbc0d21327e0cb94ec6069a8d235affd99689ffc5f68959bfb720bafc04bcf", + ), + value: 988567, + fiatWorth: "\$0", + txName: "nc1qraffwaq3cxngwp609e03ynwsx8ykgjnjve9f3y", + blocked: false, + isCoinbase: false, + ), + UtxoObject( + txid: "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6", + vout: 0, + status: Status( + confirmed: true, + confirmations: 212, + blockHeight: 629633, + blockTime: 1663093275, + blockHash: + "40c8dd876cf111dc00d3aa2fedc93a77c18b391931939d4f99a760226cbff675", + ), + value: 1000000, + fiatWorth: "\$0", + txName: "nc1qwfda4s9qmdqpnykgpjf85n09ath983srtuxcqx", + blocked: false, + isCoinbase: false, + ), +]; diff --git a/test/services/coins/namecoin/namecoin_wallet_test.dart b/test/services/coins/namecoin/namecoin_wallet_test.dart new file mode 100644 index 000000000..82238310b --- /dev/null +++ b/test/services/coins/namecoin/namecoin_wallet_test.dart @@ -0,0 +1,4448 @@ +import 'package:bitcoindart/bitcoindart.dart'; +import 'package:decimal/decimal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx.dart'; +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/namecoin/namecoin_wallet.dart'; +import 'package:stackwallet/services/price.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:tuple/tuple.dart'; + +import 'namecoin_history_sample_data.dart'; +import 'namecoin_transaction_data_samples.dart'; +import 'namecoin_utxo_sample_data.dart'; +import 'namecoin_wallet_test.mocks.dart'; +import 'namecoin_wallet_test_parameters.dart'; + +@GenerateMocks( + [ElectrumX, CachedElectrumX, PriceAPI, TransactionNotificationTracker]) +void main() { + group("namecoin constants", () { + test("namecoin minimum confirmations", () async { + expect(MINIMUM_CONFIRMATIONS, 2); + }); + test("namecoin dust limit", () async { + expect(DUST_LIMIT, 294); + }); + test("namecoin mainnet genesis block hash", () async { + expect(GENESIS_HASH_MAINNET, + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"); + }); + }); + + test("namecoin DerivePathType enum", () { + expect(DerivePathType.values.length, 3); + expect(DerivePathType.values.toString(), + "[DerivePathType.bip44, DerivePathType.bip49, DerivePathType.bip84]"); + }); + + group("bip32 node/root", () { + // test("getBip32Root", () { + // final root = getBip32Root(TEST_MNEMONIC, namecoin); + // expect(root.toWIF(), ROOT_WIF); + // }); + + // test("getBip32NodeFromRoot", () { + // final root = getBip32Root(TEST_MNEMONIC, namecoin); + // // two mainnet + // final node44 = getBip32NodeFromRoot(0, 0, root, DerivePathType.bip44); + // expect(node44.toWIF(), NODE_WIF_44); + // final node49 = getBip32NodeFromRoot(0, 0, root, DerivePathType.bip49); + // expect(node49.toWIF(), NODE_WIF_49); + // // and one on testnet + // final node84 = getBip32NodeFromRoot( + // 0, 0, getBip32Root(TEST_MNEMONIC, testnet), DerivePathType.bip84); + // expect(node84.toWIF(), NODE_WIF_84); + // // a bad derive path + // bool didThrow = false; + // try { + // getBip32NodeFromRoot(0, 0, root, null); + // } catch (_) { + // didThrow = true; + // } + // expect(didThrow, true); + // // finally an invalid network + // didThrow = false; + // final invalidNetwork = NetworkType( + // messagePrefix: '\x18hello world\n', + // bech32: 'gg', + // bip32: Bip32Type(public: 0x055521e, private: 0x055555), + // pubKeyHash: 0x55, + // scriptHash: 0x55, + // wif: 0x00); + // try { + // getBip32NodeFromRoot(0, 0, getBip32Root(TEST_MNEMONIC, invalidNetwork), + // DerivePathType.bip44); + // } catch (_) { + // didThrow = true; + // } + // expect(didThrow, true); + // }); + + // test("basic getBip32Node", () { + // final node = + // getBip32Node(0, 0, TEST_MNEMONIC, testnet, DerivePathType.bip84); + // expect(node.toWIF(), NODE_WIF_84); + // }); + }); + + // group("validate testnet namecoin addresses", () { + // MockElectrumX? client; + // MockCachedElectrumX? cachedClient; + // MockPriceAPI? priceAPI; + // FakeSecureStorage? secureStore; + // MockTransactionNotificationTracker? tracker; + // + // NamecoinWallet? testnetWallet; + // + // setUp(() { + // client = MockElectrumX(); + // cachedClient = MockCachedElectrumX(); + // priceAPI = MockPriceAPI(); + // secureStore = FakeSecureStorage(); + // tracker = MockTransactionNotificationTracker(); + // + // testnetWallet = NamecoinWallet( + // walletId: "validateAddressTestNet", + // walletName: "validateAddressTestNet", + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // }); + // + // test("valid testnet namecoin legacy/p2pkh address", () { + // expect( + // testnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), + // true); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("valid testnet namecoin p2sh-p2wpkh address", () { + // expect( + // testnetWallet?.validateAddress("2Mugf9hpSYdQPPLNtWiU2utCi6cM9v5Pnro"), + // true); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("valid testnet namecoin p2wpkh address", () { + // expect( + // testnetWallet + // ?.validateAddress("tb1qzzlm6mnc8k54mx6akehl8p9ray8r439va5ndyq"), + // true); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("invalid testnet namecoin legacy/p2pkh address", () { + // expect( + // testnetWallet?.validateAddress("16YB85zQHjro7fqjR2hMcwdQWCX8jNVtr5"), + // false); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("invalid testnet namecoin p2sh-p2wpkh address", () { + // expect( + // testnetWallet?.validateAddress("3Ns8HuQmkyyKnVixk2yQtG7pN3GcJ6xctk"), + // false); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("invalid testnet namecoin p2wpkh address", () { + // expect( + // testnetWallet + // ?.validateAddress("bc1qc5ymmsay89r6gr4fy2kklvrkuvzyln4shdvjhf"), + // false); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // }); + + group("validate mainnet namecoin addresses", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + FakeSecureStorage? secureStore; + MockTransactionNotificationTracker? tracker; + + NamecoinWallet? mainnetWallet; + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + mainnetWallet = NamecoinWallet( + walletId: "validateAddressMainNet", + walletName: "validateAddressMainNet", + coin: Coin.namecoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + // test("valid mainnet legacy/p2pkh address type", () { + // expect( + // mainnetWallet?.addressType( + // address: "nc1qmdt0fxhpwx7x5ymmm9gvh229adu0kmtukfcsjk"), + // DerivePathType.bip44); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(tracker); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("valid mainnet p2sh-p2wpkh address type", () { + // expect( + // mainnetWallet?.addressType( + // address: "3Ns8HuQmkyyKnVixk2yQtG7pN3GcJ6xctk"), + // DerivePathType.bip49); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(tracker); + // verifyNoMoreInteractions(priceAPI); + // }); + + test("valid mainnet bech32 p2wpkh address type", () { + expect( + mainnetWallet?.addressType( + address: "bc1qc5ymmsay89r6gr4fy2kklvrkuvzyln4shdvjhf"), + DerivePathType.bip84); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + // test("invalid base58 address type", () { + // expect( + // () => mainnetWallet?.addressType( + // address: "mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), + // throwsArgumentError); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(tracker); + // verifyNoMoreInteractions(priceAPI); + // }); + + test("invalid bech32 address type", () { + expect( + () => mainnetWallet?.addressType( + address: "tb1qzzlm6mnc8k54mx6akehl8p9ray8r439va5ndyq"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("address has no matching script", () { + expect( + () => mainnetWallet?.addressType( + address: "mpMk94ETazqonHutyC1v6ajshgtP8oiFKU"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + // test("valid mainnet namecoin legacy/p2pkh address", () { + // expect( + // mainnetWallet?.validateAddress("16YB85zQHjro7fqjR2hMcwdQWCX8jNVtr5"), + // true); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(tracker); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("valid mainnet namecoin p2sh-p2wpkh address", () { + // expect( + // mainnetWallet?.validateAddress("3Ns8HuQmkyyKnVixk2yQtG7pN3GcJ6xctk"), + // true); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(tracker); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("valid mainnet namecoin p2wpkh address", () { + // expect( + // mainnetWallet + // ?.validateAddress("bc1qc5ymmsay89r6gr4fy2kklvrkuvzyln4shdvjhf"), + // true); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(tracker); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("invalid mainnet namecoin legacy/p2pkh address", () { + // expect( + // mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), + // false); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(tracker); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("invalid mainnet namecoin p2sh-p2wpkh address", () { + // expect( + // mainnetWallet?.validateAddress("2Mugf9hpSYdQPPLNtWiU2utCi6cM9v5Pnro"), + // false); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(tracker); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("invalid mainnet namecoin p2wpkh address", () { + // expect( + // mainnetWallet + // ?.validateAddress("tb1qzzlm6mnc8k54mx6akehl8p9ray8r439va5ndyq"), + // false); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(tracker); + // verifyNoMoreInteractions(priceAPI); + // }); + }); + + group("testNetworkConnection", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + FakeSecureStorage? secureStore; + MockTransactionNotificationTracker? tracker; + + NamecoinWallet? btc; + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + btc = NamecoinWallet( + walletId: "testNetworkConnection", + walletName: "testNetworkConnection", + coin: Coin.namecoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("attempted connection fails due to server error", () async { + when(client?.ping()).thenAnswer((_) async => false); + final bool? result = await btc?.testNetworkConnection(); + expect(result, false); + expect(secureStore?.interactions, 0); + verify(client?.ping()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("attempted connection fails due to exception", () async { + when(client?.ping()).thenThrow(Exception); + final bool? result = await btc?.testNetworkConnection(); + expect(result, false); + expect(secureStore?.interactions, 0); + verify(client?.ping()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("attempted connection test success", () async { + when(client?.ping()).thenAnswer((_) async => true); + final bool? result = await btc?.testNetworkConnection(); + expect(result, true); + expect(secureStore?.interactions, 0); + verify(client?.ping()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + }); + + group("basic getters, setters, and functions", () { + final testWalletId = "BTCtestWalletID"; + final testWalletName = "BTCWallet"; + + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + FakeSecureStorage? secureStore; + MockTransactionNotificationTracker? tracker; + + NamecoinWallet? btc; + + setUp(() async { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + btc = NamecoinWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.namecoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("get networkType main", () async { + expect(Coin.namecoin, Coin.namecoin); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get networkType test", () async { + btc = NamecoinWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.bitcoinTestNet, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + expect(Coin.bitcoinTestNet, Coin.bitcoinTestNet); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get cryptoCurrency", () async { + expect(Coin.namecoin, Coin.namecoin); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get coinName", () async { + expect(Coin.namecoin, Coin.namecoin); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get coinTicker", () async { + expect(Coin.namecoin, Coin.namecoin); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get and set walletName", () async { + expect(Coin.namecoin, Coin.namecoin); + btc?.walletName = "new name"; + expect(btc?.walletName, "new name"); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("estimateTxFee", () async { + expect(btc?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); + expect(btc?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); + expect(btc?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); + expect(btc?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); + expect(btc?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); + expect(btc?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); + expect(btc?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); + expect(btc?.estimateTxFee(vSize: 356, feeRatePerKB: 12345), 4628); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get fees succeeds", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.estimateFee(blocks: 1)) + .thenAnswer((realInvocation) async => Decimal.zero); + when(client?.estimateFee(blocks: 5)) + .thenAnswer((realInvocation) async => Decimal.one); + when(client?.estimateFee(blocks: 20)) + .thenAnswer((realInvocation) async => Decimal.ten); + + final fees = await btc?.fees; + expect(fees, isA()); + expect(fees?.slow, 1000000000); + expect(fees?.medium, 100000000); + expect(fees?.fast, 0); + + verify(client?.estimateFee(blocks: 1)).called(1); + verify(client?.estimateFee(blocks: 5)).called(1); + verify(client?.estimateFee(blocks: 20)).called(1); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get fees fails", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.estimateFee(blocks: 1)) + .thenAnswer((realInvocation) async => Decimal.zero); + when(client?.estimateFee(blocks: 5)) + .thenAnswer((realInvocation) async => Decimal.one); + when(client?.estimateFee(blocks: 20)) + .thenThrow(Exception("some exception")); + + bool didThrow = false; + try { + await btc?.fees; + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + verify(client?.estimateFee(blocks: 1)).called(1); + verify(client?.estimateFee(blocks: 5)).called(1); + verify(client?.estimateFee(blocks: 20)).called(1); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + // test("get maxFee", () async { + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_TESTNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.estimateFee(blocks: 20)) + // .thenAnswer((realInvocation) async => Decimal.zero); + // when(client?.estimateFee(blocks: 5)) + // .thenAnswer((realInvocation) async => Decimal.one); + // when(client?.estimateFee(blocks: 1)) + // .thenAnswer((realInvocation) async => Decimal.ten); + // + // final maxFee = await btc?.maxFee; + // expect(maxFee, 1000000000); + // + // verify(client?.estimateFee(blocks: 1)).called(1); + // verify(client?.estimateFee(blocks: 5)).called(1); + // verify(client?.estimateFee(blocks: 20)).called(1); + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(tracker); + // verifyNoMoreInteractions(priceAPI); + // }); + }); + + group("Bitcoin service class functions that depend on shared storage", () { + final testWalletId = "BTCtestWalletID"; + final testWalletName = "BTCWallet"; + + bool hiveAdaptersRegistered = false; + + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + FakeSecureStorage? secureStore; + MockTransactionNotificationTracker? tracker; + + NamecoinWallet? btc; + + setUp(() async { + await setUpTestHive(); + if (!hiveAdaptersRegistered) { + hiveAdaptersRegistered = true; + + // Registering Transaction Model Adapters + Hive.registerAdapter(TransactionDataAdapter()); + Hive.registerAdapter(TransactionChunkAdapter()); + Hive.registerAdapter(TransactionAdapter()); + Hive.registerAdapter(InputAdapter()); + Hive.registerAdapter(OutputAdapter()); + + // Registering Utxo Model Adapters + Hive.registerAdapter(UtxoDataAdapter()); + Hive.registerAdapter(UtxoObjectAdapter()); + Hive.registerAdapter(StatusAdapter()); + + final wallets = await Hive.openBox('wallets'); + await wallets.put('currentWalletName', testWalletName); + } + + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + btc = NamecoinWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.namecoin, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + // test("initializeWallet no network", () async { + // when(client?.ping()).thenAnswer((_) async => false); + // expect(await btc?.initializeWallet(), false); + // expect(secureStore?.interactions, 0); + // verify(client?.ping()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("initializeWallet no network exception", () async { + // when(client?.ping()).thenThrow(Exception("Network connection failed")); + // final wallets = await Hive.openBox(testWalletId); + // expect(await btc?.initializeExisting(), false); + // expect(secureStore?.interactions, 0); + // verify(client?.ping()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + test("initializeWallet mainnet throws bad network", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + // await btc?.initializeNew(); + final wallets = await Hive.openBox(testWalletId); + + expectLater(() => btc?.initializeExisting(), throwsA(isA())) + .then((_) { + expect(secureStore?.interactions, 0); + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + }); + + test("initializeWallet throws mnemonic overwrite exception", () async { + when(client?.ping()).thenAnswer((_) async => true); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + await secureStore?.write( + key: "${testWalletId}_mnemonic", value: "some mnemonic"); + + final wallets = await Hive.openBox(testWalletId); + expectLater(() => btc?.initializeExisting(), throwsA(isA())) + .then((_) { + expect(secureStore?.interactions, 1); + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + }); + + // test("initializeWallet testnet throws bad network", () async { + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // + // expectLater(() => btc?.initializeWallet(), throwsA(isA())) + // .then((_) { + // expect(secureStore?.interactions, 0); + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // }); + + // test("getCurrentNode", () async { + // // when(priceAPI.getBitcoinPrice(baseCurrency: "USD")) + // // .thenAnswer((realInvocation) async => Decimal.fromInt(10)); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // expect(await btc?.initializeWallet(), true); + // + // bool didThrow = false; + // try { + // await btc?.getCurrentNode(); + // } catch (_) { + // didThrow = true; + // } + // // expect no nodes on a fresh wallet unless set in db externally + // expect(didThrow, true); + // + // // set node + // final wallet = await Hive.openBox(testWalletId); + // await wallet.put("nodes", { + // "default": { + // "id": "some nodeID", + // "ipAddress": "some address", + // "port": "9000", + // "useSSL": true, + // } + // }); + // await wallet.put("activeNodeID_Bitcoin", "default"); + // + // // try fetching again + // final node = await btc?.getCurrentNode(); + // expect(node.toString(), + // "ElectrumXNode: {address: some address, port: 9000, name: default, useSSL: true}"); + // + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("initializeWallet new main net wallet", () async { + // // when(priceAPI.getBitcoinPrice(baseCurrency: "USD")) + // // .thenAnswer((realInvocation) async => Decimal.fromInt(10)); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // expect(await btc?.initializeWallet(), true); + // + // final wallet = await Hive.openBox(testWalletId); + // + // expect(await wallet.get("addressBookEntries"), {}); + // expect(await wallet.get('notes'), null); + // expect(await wallet.get("id"), testWalletId); + // expect(await wallet.get("preferredFiatCurrency"), null); + // expect(await wallet.get("blocked_tx_hashes"), ["0xdefault"]); + // + // final changeAddressesP2PKH = await wallet.get("changeAddressesP2PKH"); + // expect(changeAddressesP2PKH, isA>()); + // expect(changeAddressesP2PKH.length, 1); + // expect(await wallet.get("changeIndexP2PKH"), 0); + // final changeAddressesP2SH = await wallet.get("changeAddressesP2SH"); + // expect(changeAddressesP2SH, isA>()); + // expect(changeAddressesP2SH.length, 1); + // expect(await wallet.get("changeIndexP2SH"), 0); + // final changeAddressesP2WPKH = await wallet.get("changeAddressesP2WPKH"); + // expect(changeAddressesP2WPKH, isA>()); + // expect(changeAddressesP2WPKH.length, 1); + // expect(await wallet.get("changeIndexP2WPKH"), 0); + // + // final receivingAddressesP2PKH = + // await wallet.get("receivingAddressesP2PKH"); + // expect(receivingAddressesP2PKH, isA>()); + // expect(receivingAddressesP2PKH.length, 1); + // expect(await wallet.get("receivingIndexP2PKH"), 0); + // final receivingAddressesP2SH = await wallet.get("receivingAddressesP2SH"); + // expect(receivingAddressesP2SH, isA>()); + // expect(receivingAddressesP2SH.length, 1); + // expect(await wallet.get("receivingIndexP2SH"), 0); + // final receivingAddressesP2WPKH = + // await wallet.get("receivingAddressesP2WPKH"); + // expect(receivingAddressesP2WPKH, isA>()); + // expect(receivingAddressesP2WPKH.length, 1); + // expect(await wallet.get("receivingIndexP2WPKH"), 0); + // + // final p2pkhReceiveDerivations = jsonDecode(await secureStore?.read( + // key: "${testWalletId}_receiveDerivationsP2PKH")); + // expect(p2pkhReceiveDerivations.length, 1); + // final p2shReceiveDerivations = jsonDecode(await secureStore.read( + // key: "${testWalletId}_receiveDerivationsP2SH")); + // expect(p2shReceiveDerivations.length, 1); + // final p2wpkhReceiveDerivations = jsonDecode(await secureStore.read( + // key: "${testWalletId}_receiveDerivationsP2WPKH")); + // expect(p2wpkhReceiveDerivations.length, 1); + // + // final p2pkhChangeDerivations = jsonDecode(await secureStore.read( + // key: "${testWalletId}_changeDerivationsP2PKH")); + // expect(p2pkhChangeDerivations.length, 1); + // final p2shChangeDerivations = jsonDecode( + // await secureStore.read(key: "${testWalletId}_changeDerivationsP2SH")); + // expect(p2shChangeDerivations.length, 1); + // final p2wpkhChangeDerivations = jsonDecode(await secureStore.read( + // key: "${testWalletId}_changeDerivationsP2WPKH")); + // expect(p2wpkhChangeDerivations.length, 1); + // + // expect(secureStore?.interactions, 26); // 20 in reality + 6 in this test + // expect(secureStore?.reads, 19); // 13 in reality + 6 in this test + // expect(secureStore?.writes, 7); + // expect(secureStore?.deletes, 0); + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("initializeWallet existing main net wallet", () async { + // // when(priceAPI.getBitcoinPrice(baseCurrency: "USD")) + // // .thenAnswer((realInvocation) async => Decimal.fromInt(10)); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getBatchHistory(args: anyNamed("args"))) + // .thenAnswer((_) async => {}); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // // init new wallet + // expect(await btc?.initializeWallet(), true); + // + // // fetch data to compare later + // final newWallet = await Hive.openBox(testWalletId); + // + // final addressBookEntries = await newWallet.get("addressBookEntries"); + // final notes = await newWallet.get('notes'); + // final wID = await newWallet.get("id"); + // final currency = await newWallet.get("preferredFiatCurrency"); + // final blockedHashes = await newWallet.get("blocked_tx_hashes"); + // + // final changeAddressesP2PKH = await newWallet.get("changeAddressesP2PKH"); + // final changeIndexP2PKH = await newWallet.get("changeIndexP2PKH"); + // final changeAddressesP2SH = await newWallet.get("changeAddressesP2SH"); + // final changeIndexP2SH = await newWallet.get("changeIndexP2SH"); + // final changeAddressesP2WPKH = + // await newWallet.get("changeAddressesP2WPKH"); + // final changeIndexP2WPKH = await newWallet.get("changeIndexP2WPKH"); + // + // final receivingAddressesP2PKH = + // await newWallet.get("receivingAddressesP2PKH"); + // final receivingIndexP2PKH = await newWallet.get("receivingIndexP2PKH"); + // final receivingAddressesP2SH = + // await newWallet.get("receivingAddressesP2SH"); + // final receivingIndexP2SH = await newWallet.get("receivingIndexP2SH"); + // final receivingAddressesP2WPKH = + // await newWallet.get("receivingAddressesP2WPKH"); + // final receivingIndexP2WPKH = await newWallet.get("receivingIndexP2WPKH"); + // + // final p2pkhReceiveDerivations = jsonDecode(await secureStore.read( + // key: "${testWalletId}_receiveDerivationsP2PKH")); + // final p2shReceiveDerivations = jsonDecode(await secureStore.read( + // key: "${testWalletId}_receiveDerivationsP2SH")); + // final p2wpkhReceiveDerivations = jsonDecode(await secureStore.read( + // key: "${testWalletId}_receiveDerivationsP2WPKH")); + // + // final p2pkhChangeDerivations = jsonDecode(await secureStore.read( + // key: "${testWalletId}_changeDerivationsP2PKH")); + // final p2shChangeDerivations = jsonDecode( + // await secureStore.read(key: "${testWalletId}_changeDerivationsP2SH")); + // final p2wpkhChangeDerivations = jsonDecode(await secureStore.read( + // key: "${testWalletId}_changeDerivationsP2WPKH")); + // + // // exit new wallet + // await btc?.exit(); + // + // // open existing/created wallet + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.namecoin, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // + // // init existing + // expect(await btc?.initializeWallet(), true); + // + // // compare data to ensure state matches state of previously closed wallet + // final wallet = await Hive.openBox(testWalletId); + // + // expect(await wallet.get("addressBookEntries"), addressBookEntries); + // expect(await wallet.get('notes'), notes); + // expect(await wallet.get("id"), wID); + // expect(await wallet.get("preferredFiatCurrency"), currency); + // expect(await wallet.get("blocked_tx_hashes"), blockedHashes); + // + // expect(await wallet.get("changeAddressesP2PKH"), changeAddressesP2PKH); + // expect(await wallet.get("changeIndexP2PKH"), changeIndexP2PKH); + // expect(await wallet.get("changeAddressesP2SH"), changeAddressesP2SH); + // expect(await wallet.get("changeIndexP2SH"), changeIndexP2SH); + // expect(await wallet.get("changeAddressesP2WPKH"), changeAddressesP2WPKH); + // expect(await wallet.get("changeIndexP2WPKH"), changeIndexP2WPKH); + // + // expect( + // await wallet.get("receivingAddressesP2PKH"), receivingAddressesP2PKH); + // expect(await wallet.get("receivingIndexP2PKH"), receivingIndexP2PKH); + // expect( + // await wallet.get("receivingAddressesP2SH"), receivingAddressesP2SH); + // expect(await wallet.get("receivingIndexP2SH"), receivingIndexP2SH); + // expect(await wallet.get("receivingAddressesP2WPKH"), + // receivingAddressesP2WPKH); + // expect(await wallet.get("receivingIndexP2WPKH"), receivingIndexP2WPKH); + // + // expect( + // jsonDecode(await secureStore.read( + // key: "${testWalletId}_receiveDerivationsP2PKH")), + // p2pkhReceiveDerivations); + // expect( + // jsonDecode(await secureStore.read( + // key: "${testWalletId}_receiveDerivationsP2SH")), + // p2shReceiveDerivations); + // expect( + // jsonDecode(await secureStore.read( + // key: "${testWalletId}_receiveDerivationsP2WPKH")), + // p2wpkhReceiveDerivations); + // + // expect( + // jsonDecode(await secureStore.read( + // key: "${testWalletId}_changeDerivationsP2PKH")), + // p2pkhChangeDerivations); + // expect( + // jsonDecode(await secureStore.read( + // key: "${testWalletId}_changeDerivationsP2SH")), + // p2shChangeDerivations); + // expect( + // jsonDecode(await secureStore.read( + // key: "${testWalletId}_changeDerivationsP2WPKH")), + // p2wpkhChangeDerivations); + // + // expect(secureStore?.interactions, 32); // 20 in reality + 12 in this test + // expect(secureStore?.reads, 25); // 13 in reality + 12 in this test + // expect(secureStore?.writes, 7); + // expect(secureStore?.deletes, 0); + // verify(client?.ping()).called(2); + // verify(client?.getServerFeatures()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // // test("get fiatPrice", () async { + // // // when(priceAPI.getBitcoinPrice(baseCurrency: "USD")) + // // // .thenAnswer((realInvocation) async => Decimal.fromInt(10)); + // // await Hive.openBox(testWalletId); + // // expect(await btc.basePrice, Decimal.fromInt(10)); + // // verify(priceAPI.getBitcoinPrice(baseCurrency: "USD")).called(1); + // // verifyNoMoreInteractions(client); + // // verifyNoMoreInteractions(cachedClient); + // // verifyNoMoreInteractions(priceAPI); + // // }); + // + // test("get current receiving addresses", () async { + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_TESTNET, + // "hash_function": "sha256", + // "services": [] + // }); + // await btc?.initializeWallet(); + // expect( + // Address.validateAddress(await btc!.currentReceivingAddress, testnet), + // true); + // expect( + // Address.validateAddress( + // await btc!.currentReceivingAddressP2SH, testnet), + // true); + // expect( + // Address.validateAddress( + // await btc!.currentLegacyReceivingAddress, testnet), + // true); + // + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("get allOwnAddresses", () async { + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_TESTNET, + // "hash_function": "sha256", + // "services": [] + // }); + // await btc?.initializeWallet(); + // final addresses = await btc?.allOwnAddresses; + // expect(addresses, isA>()); + // expect(addresses?.length, 6); + // + // for (int i = 0; i < 6; i++) { + // expect(Address.validateAddress(addresses[i], testnet), true); + // } + // + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get utxos and balances", () async { + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_TESTNET, + // "hash_function": "sha256", + // "services": [] + // }); + // + // when(client?.getBatchUTXOs(args: anyNamed("args"))) + // .thenAnswer((_) async => batchGetUTXOResponse0); + // + // when(client?.estimateFee(blocks: 10)) + // .thenAnswer((realInvocation) async => Decimal.zero); + // when(client?.estimateFee(blocks: 5)) + // .thenAnswer((realInvocation) async => Decimal.one); + // when(client?.estimateFee(blocks: 1)) + // .thenAnswer((realInvocation) async => Decimal.ten); + // // when(priceAPI.getBitcoinPrice(baseCurrency: "USD")) + // // .thenAnswer((realInvocation) async => Decimal.fromInt(10)); + // + // when(cachedClient?.getTransaction( + // txHash: tx1.txid, + // coin: Coin.bitcoinTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx1Raw); + // when(cachedClient?.getTransaction( + // txHash: tx2.txid, + // coin: Coin.bitcoinTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx2Raw); + // when(cachedClient?.getTransaction( + // txHash: tx3.txid, + // coin: Coin.bitcoinTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // txHash: tx4.txid, + // coin: Coin.bitcoinTestNet, + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx4Raw); + // + // await btc?.initializeNew(); + // await btc?.initializeExisting(); + // final utxoData = await btc?.utxoData; + // expect(utxoData, isA()); + // expect(utxoData.toString(), + // r"{totalUserCurrency: $0.0076497, satoshiBalance: 76497, bitcoinBalance: 0.00076497, unspentOutputArray: [{txid: 88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c, vout: 0, value: 17000, fiat: $0.0017, blocked: false, status: {confirmed: true, blockHash: 00000000000000198ca8300deab26c5c1ec1df0da5afd30c9faabd340d8fc194, blockHeight: 437146, blockTime: 1652994245, confirmations: 100}}, {txid: b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528, vout: 0, value: 36037, fiat: $0.0036037, blocked: false, status: {confirmed: false, blockHash: 000000000000003db63ad679a539f2088dcc97a149c99ca790ce0c5f7b5acff0, blockHeight: 441696, blockTime: 1652923129, confirmations: 0}}, {txid: dcca229760b44834478f0b266c9b3f5801e0139fdecacdc0820e447289a006d3, vout: 1, value: 14714, fiat: $0.0014714, blocked: false, status: {confirmed: false, blockHash: 0000000000000030bec9bc58a3ab4857de1cc63cfed74204a6be57f125fb2fa7, blockHeight: 437146, blockTime: 1652888705, confirmations: 0}}, {txid: b39bac02b65af46a49e2985278fe24ca00dd5d627395d88f53e35568a04e10fa, vout: 0, value: 8746, fiat: $0.0008746, blocked: false, status: {confirmed: true, blockHash: 0000000039b80e9a10b7bcaf0f193b51cb870a4febe9b427c1f41a3f42eaa80b, blockHeight: 441696, blockTime: 1652993683, confirmations: 22861}}]}"); + // + // final outputs = await btc?.unspentOutputs; + // expect(outputs, isA>()); + // expect(outputs?.length, 4); + // + // final availableBalance = await btc?.availableBalance; + // expect(availableBalance, Decimal.parse("0.00025746")); + // + // final totalBalance = await btc?.totalBalance; + // expect(totalBalance, Decimal.parse("0.00076497")); + // + // final pendingBalance = await btc?.pendingBalance; + // expect(pendingBalance, Decimal.parse("0.00050751")); + // + // final balanceMinusMaxFee = await btc?.balanceMinusMaxFee; + // expect(balanceMinusMaxFee, Decimal.parse("-9.99974254")); + // + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verify(client?.estimateFee(blocks: 1)).called(1); + // verify(client?.estimateFee(blocks: 5)).called(1); + // verify(client?.estimateFee(blocks: 10)).called(1); + // verify(client?.getBatchUTXOs(args: anyNamed("args"))).called(1); + // // verify(priceAPI.getBitcoinPrice(baseCurrency: "USD")).called(1); + // verify(cachedClient?.getTransaction( + // txHash: tx1.txid, + // coin: Coin.bitcoinTestNet, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: tx2.txid, + // coin: Coin.bitcoinTestNet, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: tx3.txid, + // coin: Coin.bitcoinTestNet, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: tx4.txid, + // coin: Coin.bitcoinTestNet, + // callOutSideMainIsolate: false)) + // .called(1); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get utxos - multiple batches", () async { + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_TESTNET, + // "hash_function": "sha256", + // "services": [] + // }); + // + // when(client?.getBatchUTXOs(args: anyNamed("args"))) + // .thenAnswer((_) async => {}); + // + // // when(priceAPI.getBitcoinPrice(baseCurrency: "USD")) + // // .thenAnswer((realInvocation) async => Decimal.fromInt(10)); + // + // await btc?.initializeNew(); + // await btc?.initializeExisting(); + // + // // add some extra addresses to make sure we have more than the single batch size of 10 + // final wallet = await Hive.openBox(DB); + // final addresses = await wallet.get("receivingAddressesP2WPKH"); + // addresses.add("tb1qpfl2uz3jvazy9wr4vqhwluyhgtd29rsmghpqxp"); + // addresses.add("tb1qznt3psdpcyz8lwj7xxl6q78hjw2mj095nd4gxu"); + // addresses.add("tb1q7yjjyh9h4uy7j0wdtcmptw3g083kxrqlvgjz86"); + // addresses.add("tb1qt05shktwcq7kgxccva20cfwt47kav9s6n8yr9p"); + // addresses.add("tb1q4nk5wdylywl4dg2a45naae7u08vtgyujqfrv58"); + // addresses.add("tb1qxwccgfq9tmd6lx823cuejuea9wdzpaml9wkapm"); + // addresses.add("tb1qk88negkdqusr8tpj0hpvs98lq6ka4vyw6kfnqf"); + // addresses.add("tb1qw0jzneqwp0t4ah9w3za4k9d8d4tz8y3zxqmtgx"); + // addresses.add("tb1qccqjlpndx46sv7t6uurlyyjre5vwjfdzzlf2vd"); + // addresses.add("tb1q3hfpe69rrhr5348xd04rfz9g3h22yk64pwur8v"); + // addresses.add("tb1q4rp373202aur96a28lp0pmts6kp456nka45e7d"); + // await wallet.put("receivingAddressesP2WPKH", addresses); + // + // final utxoData = await btc?.utxoData; + // expect(utxoData, isA()); + // + // final outputs = await btc?.unspentOutputs; + // expect(outputs, isA>()); + // expect(outputs?.length, 0); + // + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verify(client?.getBatchUTXOs(args: anyNamed("args"))).called(2); + // // verify(priceAPI.getBitcoinPrice(baseCurrency: "USD")).called(1); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("get utxos fails", () async { + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_TESTNET, + // "hash_function": "sha256", + // "services": [] + // }); + // + // when(client?.getBatchUTXOs(args: anyNamed("args"))) + // .thenThrow(Exception("some exception")); + // + // await btc?.initializeWallet(); + // final utxoData = await btc?.utxoData; + // expect(utxoData, isA()); + // expect(utxoData.toString(), + // r"{totalUserCurrency: $0.00, satoshiBalance: 0, bitcoinBalance: 0, unspentOutputArray: []}"); + // + // final outputs = await btc?.unspentOutputs; + // expect(outputs, isA>()); + // expect(outputs?.length, 0); + // + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verify(client?.getBatchUTXOs(args: anyNamed("args"))).called(1); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("chain height fetch, update, and get", () async { + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_TESTNET, + // "hash_function": "sha256", + // "services": [] + // }); + // await btc?.initializeWallet(); + // + // // get stored + // expect(await btc?.storedChainHeight, 0); + // + // // fetch fails + // when(client?.getBlockHeadTip()).thenThrow(Exception("Some exception")); + // expect(await btc?.chainHeight, -1); + // + // // fetch succeeds + // when(client?.getBlockHeadTip()).thenAnswer((realInvocation) async => { + // "height": 100, + // "hex": "some block hex", + // }); + // expect(await btc?.chainHeight, 100); + // + // // update + // await btc?.updateStoredChainHeight(newHeight: 1000); + // + // // fetch updated + // expect(await btc?.storedChainHeight, 1000); + // + // verify(client?.ping()).called(1); + // verify(client?.getServerFeatures()).called(1); + // verify(client?.getBlockHeadTip()).called(2); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("fetch and update useBiometrics", () async { + // // get + // expect(await btc?.useBiometrics, false); + // + // // then update + // await btc?.updateBiometricsUsage(true); + // + // // finally check updated + // expect(await btc?.useBiometrics, true); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("getTxCount succeeds", () async { + // when(client?.getHistory( + // scripthash: + // "4e94cc7b4a85791445260ae4403233b6a4784185f9716d73f136c6642615fce9")) + // .thenAnswer((realInvocation) async => [ + // { + // "height": 200004, + // "tx_hash": + // "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" + // }, + // { + // "height": 215008, + // "tx_hash": + // "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" + // } + // ]); + // + // final count = + // await btc?.getTxCount(address: "3Ns8HuQmkyyKnVixk2yQtG7pN3GcJ6xctk"); + // + // expect(count, 2); + // + // verify(client?.getHistory( + // scripthash: + // "4e94cc7b4a85791445260ae4403233b6a4784185f9716d73f136c6642615fce9")) + // .called(1); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("getTxCount fails", () async { + // when(client?.getHistory( + // scripthash: + // "4e94cc7b4a85791445260ae4403233b6a4784185f9716d73f136c6642615fce9")) + // .thenThrow(Exception("some exception")); + // + // bool didThrow = false; + // try { + // await btc?.getTxCount(address: "3Ns8HuQmkyyKnVixk2yQtG7pN3GcJ6xctk"); + // } catch (_) { + // didThrow = true; + // } + // expect(didThrow, true); + // + // verify(client?.getHistory( + // scripthash: + // "4e94cc7b4a85791445260ae4403233b6a4784185f9716d73f136c6642615fce9")) + // .called(1); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("_checkCurrentReceivingAddressesForTransactions succeeds", () async { + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getHistory(scripthash: anyNamed("scripthash"))) + // .thenAnswer((realInvocation) async => [ + // { + // "height": 200004, + // "tx_hash": + // "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" + // }, + // { + // "height": 215008, + // "tx_hash": + // "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" + // } + // ]); + // + // await btc?.initializeWallet(); + // + // bool didThrow = false; + // try { + // await btc?.checkCurrentReceivingAddressesForTransactions(); + // } catch (_) { + // didThrow = true; + // } + // expect(didThrow, false); + // + // verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(3); + // verify(client?.getServerFeatures()).called(1); + // verify(client?.ping()).called(1); + // + // expect(secureStore?.interactions, 29); + // expect(secureStore?.reads, 19); + // expect(secureStore?.writes, 10); + // expect(secureStore?.deletes, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("_checkCurrentReceivingAddressesForTransactions fails", () async { + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getHistory(scripthash: anyNamed("scripthash"))) + // .thenThrow(Exception("some exception")); + // final wallet = await Hive.openBox(testWalletId); + // + // await btc?.initializeNew(); + // await btc?.initializeExisting(); + // + // bool didThrow = false; + // try { + // await btc?.checkCurrentReceivingAddressesForTransactions(); + // } catch (_) { + // didThrow = true; + // } + // expect(didThrow, true); + // + // verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); + // verify(client?.getServerFeatures()).called(1); + // verify(client?.ping()).called(1); + // + // expect(secureStore?.interactions, 20); + // expect(secureStore?.reads, 13); + // expect(secureStore?.writes, 7); + // expect(secureStore?.deletes, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("_checkCurrentChangeAddressesForTransactions succeeds", () async { + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getHistory(scripthash: anyNamed("scripthash"))) + // .thenAnswer((realInvocation) async => [ + // { + // "height": 200004, + // "tx_hash": + // "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" + // }, + // { + // "height": 215008, + // "tx_hash": + // "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" + // } + // ]); + // + // await btc?.initializeWallet(); + // + // bool didThrow = false; + // try { + // await btc?.checkCurrentChangeAddressesForTransactions(); + // } catch (_) { + // didThrow = true; + // } + // expect(didThrow, false); + // + // verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(3); + // verify(client?.getServerFeatures()).called(1); + // verify(client?.ping()).called(1); + // + // expect(secureStore?.interactions, 29); + // expect(secureStore?.reads, 19); + // expect(secureStore?.writes, 10); + // expect(secureStore?.deletes, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("_checkCurrentChangeAddressesForTransactions fails", () async { + // when(client?.ping()).thenAnswer((_) async => true); + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getHistory(scripthash: anyNamed("scripthash"))) + // .thenThrow(Exception("some exception")); + // + // await btc?.initializeWallet(); + // + // bool didThrow = false; + // try { + // await btc?.checkCurrentChangeAddressesForTransactions(); + // } catch (_) { + // didThrow = true; + // } + // expect(didThrow, true); + // + // verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); + // verify(client?.getServerFeatures()).called(1); + // verify(client?.ping()).called(1); + // + // expect(secureStore?.interactions, 20); + // expect(secureStore?.reads, 13); + // expect(secureStore?.writes, 7); + // expect(secureStore?.deletes, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("getAllTxsToWatch", () async { + // TestWidgetsFlutterBinding.ensureInitialized(); + // var notifications = {"show": 0}; + // const MethodChannel('dexterous.com/flutter/local_notifications') + // .setMockMethodCallHandler((call) async { + // notifications[call.method]++; + // }); + // + // btc?.pastUnconfirmedTxs = { + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c", + // "b39bac02b65af46a49e2985278fe24ca00dd5d627395d88f53e35568a04e10fa", + // }; + // + // await btc?.getAllTxsToWatch(transactionData); + // expect(notifications.length, 1); + // expect(notifications["show"], 3); + // + // expect(btc?.unconfirmedTxs, { + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // 'dcca229760b44834478f0b266c9b3f5801e0139fdecacdc0820e447289a006d3', + // }); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("refreshIfThereIsNewData true A", () async { + // when(client?.getTransaction( + // tx_hash: + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // )).thenAnswer((_) async => tx2Raw); + // when(client?.getTransaction( + // tx_hash: + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c", + // )).thenAnswer((_) async => tx1Raw); + // + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // final wallet = await Hive.openBox(testWalletId); + // await wallet.put('receivingAddressesP2PKH', []); + // await wallet.put('receivingAddressesP2SH', [ + // "2Mv83bPh2HzPRXptuQg9ejbKpSp87Zi52zT", + // ]); + // await wallet.put('receivingAddressesP2WPKH', [ + // "tb1q3ywehep0ykrkaqkt0hrgsqyns4mnz2ls8nxfzg", + // ]); + // + // await wallet.put('changeAddressesP2PKH', []); + // await wallet.put('changeAddressesP2SH', []); + // await wallet.put('changeAddressesP2WPKH', []); + // + // btc?.unconfirmedTxs = { + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c" + // }; + // + // final result = await btc?.refreshIfThereIsNewData(); + // + // expect(result, true); + // + // verify(client?.getTransaction( + // tx_hash: + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // )).called(1); + // verify(client.getTransaction( + // tx_hash: + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c", + // )).called(1); + // + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("refreshIfThereIsNewData true B", () async { + // // when(priceAPI.getBitcoinPrice(baseCurrency: "USD")) + // // .thenAnswer((_) async => Decimal.fromInt(10)); + // + // when(client?.getBatchHistory(args: anyNamed("args"))) + // .thenAnswer((realInvocation) async { + // final uuids = Map>.from( + // realInvocation.namedArguments.values.first) + // .keys + // .toList(growable: false); + // return { + // uuids[0]: [ + // { + // "tx_hash": + // "dcca229760b44834478f0b266c9b3f5801e0139fdecacdc0820e447289a006d3", + // "height": 2226003 + // }, + // { + // "tx_hash": + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // "height": 2226102 + // } + // ], + // uuids[1]: [ + // { + // "tx_hash": + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c", + // "height": 2226326 + // } + // ], + // }; + // }); + // + // when(client?.getTransaction( + // tx_hash: + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // )).thenAnswer((_) async => tx2Raw); + // when(client?.getTransaction( + // tx_hash: + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c", + // )).thenAnswer((_) async => tx1Raw); + // + // when(cachedClient?.getTransaction( + // tx_hash: + // "dcca229760b44834478f0b266c9b3f5801e0139fdecacdc0820e447289a006d3", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx1Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "6261002b30122ab3b2ba8c481134e8a3ce08a3a1a429b8ebb3f28228b100ac1a", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx5Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "717080fc0054f655260b1591a0059bf377a589a98284173d20a1c8f3316c086e", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx6Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "1baec51e7630e3640ccf0e34f160c8ad3eb6021ecafe3618a1afae328f320f53", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx7Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "b39bac02b65af46a49e2985278fe24ca00dd5d627395d88f53e35568a04e10fa", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx4Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "46b1f19763ac68e39b8218429f4e29b150f850901562fe44a05fade9e0acd65f", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx8Raw); + // + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // final wallet = await Hive.openBox(testWalletId); + // await wallet.put('receivingAddressesP2PKH', []); + // await wallet.put('receivingAddressesP2SH', [ + // "2Mv83bPh2HzPRXptuQg9ejbKpSp87Zi52zT", + // ]); + // await wallet.put('receivingAddressesP2WPKH', [ + // "tb1q3ywehep0ykrkaqkt0hrgsqyns4mnz2ls8nxfzg", + // ]); + // + // await wallet.put('changeAddressesP2PKH', []); + // await wallet.put('changeAddressesP2SH', []); + // await wallet.put('changeAddressesP2WPKH', []); + // + // btc.unconfirmedTxs = { + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // }; + // + // final result = await btc?.refreshIfThereIsNewData(); + // + // expect(result, true); + // + // verify(client?.getBatchHistory(args: anyNamed("args"))).called(2); + // verify(client?.getTransaction( + // tx_hash: + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // )).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: anyNamed("tx_hash"), + // verbose: true, + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .called(9); + // // verify(priceAPI.getBitcoinPrice(baseCurrency: "USD")).called(1); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("refreshIfThereIsNewData false A", () async { + // // when(priceAPI.getBitcoinPrice(baseCurrency: "USD")) + // // .thenAnswer((_) async => Decimal.fromInt(10)); + // + // when(client?.getBatchHistory(args: anyNamed("args"))) + // .thenAnswer((realInvocation) async { + // final uuids = Map>.from( + // realInvocation.namedArguments.values.first) + // .keys + // .toList(growable: false); + // return { + // uuids[0]: [ + // { + // "tx_hash": + // "dcca229760b44834478f0b266c9b3f5801e0139fdecacdc0820e447289a006d3", + // "height": 2226003 + // }, + // { + // "tx_hash": + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // "height": 2226102 + // } + // ], + // uuids[1]: [ + // { + // "tx_hash": + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c", + // "height": 2226326 + // } + // ], + // }; + // }); + // + // when(client?.getTransaction( + // tx_hash: + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // )).thenAnswer((_) async => tx2Raw); + // when(client?.getTransaction( + // tx_hash: + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c", + // )).thenAnswer((_) async => tx1Raw); + // + // when(cachedClient?.getTransaction( + // tx_hash: + // "dcca229760b44834478f0b266c9b3f5801e0139fdecacdc0820e447289a006d3", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx2Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "88b7b5077d940dde1bc63eba37a09dec8e7b9dad14c183a2e879a21b6ec0ac1c", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx1Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "6261002b30122ab3b2ba8c481134e8a3ce08a3a1a429b8ebb3f28228b100ac1a", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx5Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "717080fc0054f655260b1591a0059bf377a589a98284173d20a1c8f3316c086e", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx6Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "1baec51e7630e3640ccf0e34f160c8ad3eb6021ecafe3618a1afae328f320f53", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx7Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "b39bac02b65af46a49e2985278fe24ca00dd5d627395d88f53e35568a04e10fa", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx4Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "46b1f19763ac68e39b8218429f4e29b150f850901562fe44a05fade9e0acd65f", + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx8Raw); + // + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // final wallet = await Hive.openBox(testWalletId); + // await wallet.put('receivingAddressesP2PKH', []); + // await wallet.put('receivingAddressesP2SH', [ + // "2Mv83bPh2HzPRXptuQg9ejbKpSp87Zi52zT", + // ]); + // await wallet.put('receivingAddressesP2WPKH', [ + // "tb1q3ywehep0ykrkaqkt0hrgsqyns4mnz2ls8nxfzg", + // ]); + // + // await wallet.put('changeAddressesP2PKH', []); + // await wallet.put('changeAddressesP2SH', []); + // await wallet.put('changeAddressesP2WPKH', []); + // + // btc?.unconfirmedTxs = { + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // }; + // + // final result = await btc?.refreshIfThereIsNewData(); + // + // expect(result, false); + // + // verify(client?.getBatchHistory(args: anyNamed("args"))).called(2); + // verify(client?.getTransaction( + // tx_hash: + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // )).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: anyNamed("tx_hash"), + // verbose: true, + // coinName: "tBitcoin", + // callOutSideMainIsolate: false)) + // .called(15); + // // verify(priceAPI.getBitcoinPrice(baseCurrency: "USD")).called(1); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("refreshIfThereIsNewData false B", () async { + // when(client?.getBatchHistory(args: anyNamed("args"))) + // .thenThrow(Exception("some exception")); + // + // when(client?.getTransaction( + // txHash: + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // )).thenAnswer((_) async => tx2Raw); + // + // btc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.bitcoinTestNet, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // final wallet = await Hive.openBox(testWalletId); + // await wallet.put('receivingAddressesP2PKH', []); + // await wallet.put('receivingAddressesP2SH', [ + // "2Mv83bPh2HzPRXptuQg9ejbKpSp87Zi52zT", + // ]); + // await wallet.put('receivingAddressesP2WPKH', [ + // "tb1q3ywehep0ykrkaqkt0hrgsqyns4mnz2ls8nxfzg", + // ]); + // + // await wallet.put('changeAddressesP2PKH', []); + // await wallet.put('changeAddressesP2SH', []); + // await wallet.put('changeAddressesP2WPKH', []); + // + // btc?.txTracker = { + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // }; + // + // // btc?.unconfirmedTxs = { + // // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // // }; + // + // final result = await btc?.refreshIfThereIsNewData(); + // + // expect(result, false); + // + // verify(client?.getBatchHistory(args: anyNamed("args"))).called(1); + // verify(client?.getTransaction( + // txHash: + // "b2f75a017a7435f1b8c2e080a865275d8f80699bba68d8dce99a94606e7b3528", + // )).called(1); + // + // expect(secureStore?.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + test( + "recoverFromMnemonic using empty seed on mainnet fails due to bad genesis hash match", + () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_TESTNET, + "hash_function": "sha256", + "services": [] + }); + + bool hasThrown = false; + try { + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, true); + + verify(client?.getServerFeatures()).called(1); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test( + "recoverFromMnemonic using empty seed on testnet fails due to bad genesis hash match", + () async { + btc = NamecoinWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.bitcoinTestNet, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + + bool hasThrown = false; + try { + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, true); + + verify(client?.getServerFeatures()).called(1); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test( + "recoverFromMnemonic using empty seed on mainnet fails due to attempted overwrite of mnemonic", + () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + + await secureStore?.write( + key: "${testWalletId}_mnemonic", value: "some mnemonic words"); + + bool hasThrown = false; + try { + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, true); + + verify(client?.getServerFeatures()).called(1); + + expect(secureStore?.interactions, 2); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("recoverFromMnemonic using empty seed on mainnet succeeds", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs2)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs3)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs4)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs5)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + // await DB.instance.init(); + final wallet = await Hive.openBox(testWalletId); + bool hasThrown = false; + try { + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs2)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs3)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs4)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs5)).called(1); + + expect(secureStore?.interactions, 20); + expect(secureStore?.writes, 7); + expect(secureStore?.reads, 13); + expect(secureStore?.deletes, 0); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get mnemonic list", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs2)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs3)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs4)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs5)) + .thenAnswer((_) async => emptyHistoryBatchResponse); + + final wallet = await Hive.openBox(testWalletId); + + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + expect(await btc?.mnemonic, TEST_MNEMONIC.split(" ")); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs2)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs3)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs4)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs5)).called(1); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("recoverFromMnemonic using non empty seed on mainnet succeeds", + () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs2)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs3)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs4)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs5)) + .thenAnswer((_) async => historyBatchResponse); + + List dynamicArgValues = []; + + when(client?.getBatchHistory(args: anyNamed("args"))) + .thenAnswer((realInvocation) async { + if (realInvocation.namedArguments.values.first.length == 1) { + dynamicArgValues.add(realInvocation.namedArguments.values.first); + } + + return historyBatchResponse; + }); + + await Hive.openBox(testWalletId); + + bool hasThrown = false; + try { + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs2)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs3)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs4)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs5)).called(1); + + for (final arg in dynamicArgValues) { + final map = Map>.from(arg as Map); + + verify(client?.getBatchHistory(args: map)).called(1); + expect(activeScriptHashes.contains(map.values.first.first as String), + true); + } + + expect(secureStore?.interactions, 14); + expect(secureStore?.writes, 7); + expect(secureStore?.reads, 7); + expect(secureStore?.deletes, 0); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("fullRescan succeeds", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs2)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs3)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs4)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs5)) + .thenAnswer((_) async => historyBatchResponse); + when(cachedClient?.clearSharedTransactionCache(coin: Coin.namecoin)) + .thenAnswer((realInvocation) async {}); + + List dynamicArgValues = []; + + when(client?.getBatchHistory(args: anyNamed("args"))) + .thenAnswer((realInvocation) async { + if (realInvocation.namedArguments.values.first.length == 1) { + dynamicArgValues.add(realInvocation.namedArguments.values.first); + } + + return historyBatchResponse; + }); + + final wallet = await Hive.openBox(testWalletId); + + // restore so we have something to rescan + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + // fetch valid wallet data + final preReceivingAddressesP2PKH = + await wallet.get('receivingAddressesP2PKH'); + final preReceivingAddressesP2SH = + await wallet.get('receivingAddressesP2SH'); + final preReceivingAddressesP2WPKH = + await wallet.get('receivingAddressesP2WPKH'); + final preChangeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); + final preChangeAddressesP2SH = await wallet.get('changeAddressesP2SH'); + final preChangeAddressesP2WPKH = + await wallet.get('changeAddressesP2WPKH'); + final preReceivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); + final preReceivingIndexP2SH = await wallet.get('receivingIndexP2SH'); + final preReceivingIndexP2WPKH = await wallet.get('receivingIndexP2WPKH'); + final preChangeIndexP2PKH = await wallet.get('changeIndexP2PKH'); + final preChangeIndexP2SH = await wallet.get('changeIndexP2SH'); + final preChangeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); + final preUtxoData = await wallet.get('latest_utxo_model'); + final preReceiveDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2PKH"); + final preChangeDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2PKH"); + final preReceiveDerivationsStringP2SH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2SH"); + final preChangeDerivationsStringP2SH = + await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); + final preReceiveDerivationsStringP2WPKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2WPKH"); + final preChangeDerivationsStringP2WPKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2WPKH"); + + // destroy the data that the rescan will fix + await wallet.put( + 'receivingAddressesP2PKH', ["some address", "some other address"]); + await wallet.put( + 'receivingAddressesP2SH', ["some address", "some other address"]); + await wallet.put( + 'receivingAddressesP2WPKH', ["some address", "some other address"]); + await wallet + .put('changeAddressesP2PKH', ["some address", "some other address"]); + await wallet + .put('changeAddressesP2SH', ["some address", "some other address"]); + await wallet + .put('changeAddressesP2WPKH', ["some address", "some other address"]); + await wallet.put('receivingIndexP2PKH', 123); + await wallet.put('receivingIndexP2SH', 123); + await wallet.put('receivingIndexP2WPKH', 123); + await wallet.put('changeIndexP2PKH', 123); + await wallet.put('changeIndexP2SH', 123); + await wallet.put('changeIndexP2WPKH', 123); + await secureStore?.write( + key: "${testWalletId}_receiveDerivationsP2PKH", value: "{}"); + await secureStore?.write( + key: "${testWalletId}_changeDerivationsP2PKH", value: "{}"); + await secureStore?.write( + key: "${testWalletId}_receiveDerivationsP2SH", value: "{}"); + await secureStore?.write( + key: "${testWalletId}_changeDerivationsP2SH", value: "{}"); + await secureStore?.write( + key: "${testWalletId}_receiveDerivationsP2WPKH", value: "{}"); + await secureStore?.write( + key: "${testWalletId}_changeDerivationsP2WPKH", value: "{}"); + + bool hasThrown = false; + try { + await btc?.fullRescan(2, 1000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + // fetch wallet data again + final receivingAddressesP2PKH = + await wallet.get('receivingAddressesP2PKH'); + final receivingAddressesP2SH = await wallet.get('receivingAddressesP2SH'); + final receivingAddressesP2WPKH = + await wallet.get('receivingAddressesP2WPKH'); + final changeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); + final changeAddressesP2SH = await wallet.get('changeAddressesP2SH'); + final changeAddressesP2WPKH = await wallet.get('changeAddressesP2WPKH'); + final receivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); + final receivingIndexP2SH = await wallet.get('receivingIndexP2SH'); + final receivingIndexP2WPKH = await wallet.get('receivingIndexP2WPKH'); + final changeIndexP2PKH = await wallet.get('changeIndexP2PKH'); + final changeIndexP2SH = await wallet.get('changeIndexP2SH'); + final changeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); + final utxoData = await wallet.get('latest_utxo_model'); + final receiveDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2PKH"); + final changeDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2PKH"); + final receiveDerivationsStringP2SH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2SH"); + final changeDerivationsStringP2SH = + await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); + final receiveDerivationsStringP2WPKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2WPKH"); + final changeDerivationsStringP2WPKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2WPKH"); + + expect(preReceivingAddressesP2PKH, receivingAddressesP2PKH); + expect(preReceivingAddressesP2SH, receivingAddressesP2SH); + expect(preReceivingAddressesP2WPKH, receivingAddressesP2WPKH); + expect(preChangeAddressesP2PKH, changeAddressesP2PKH); + expect(preChangeAddressesP2SH, changeAddressesP2SH); + expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); + expect(preReceivingIndexP2PKH, receivingIndexP2PKH); + expect(preReceivingIndexP2SH, receivingIndexP2SH); + expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); + expect(preChangeIndexP2PKH, changeIndexP2PKH); + expect(preChangeIndexP2SH, changeIndexP2SH); + expect(preChangeIndexP2WPKH, changeIndexP2WPKH); + expect(preUtxoData, utxoData); + expect(preReceiveDerivationsStringP2PKH, receiveDerivationsStringP2PKH); + expect(preChangeDerivationsStringP2PKH, changeDerivationsStringP2PKH); + expect(preReceiveDerivationsStringP2SH, receiveDerivationsStringP2SH); + expect(preChangeDerivationsStringP2SH, changeDerivationsStringP2SH); + expect(preReceiveDerivationsStringP2WPKH, receiveDerivationsStringP2WPKH); + expect(preChangeDerivationsStringP2WPKH, changeDerivationsStringP2WPKH); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs1)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs2)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs3)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs4)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs5)).called(2); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.namecoin)) + .called(1); + + for (final arg in dynamicArgValues) { + final map = Map>.from(arg as Map); + + verify(client?.getBatchHistory(args: map)).called(1); + expect(activeScriptHashes.contains(map.values.first.first as String), + true); + } + + expect(secureStore?.writes, 25); + expect(secureStore?.reads, 32); + expect(secureStore?.deletes, 6); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("fullRescan fails", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs2)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs3)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs4)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs5)) + .thenAnswer((_) async => historyBatchResponse); + when(cachedClient?.clearSharedTransactionCache(coin: Coin.namecoin)) + .thenAnswer((realInvocation) async {}); + + List dynamicArgValues = []; + + when(client?.getBatchHistory(args: anyNamed("args"))) + .thenAnswer((realInvocation) async { + if (realInvocation.namedArguments.values.first.length == 1) { + dynamicArgValues.add(realInvocation.namedArguments.values.first); + } + + return historyBatchResponse; + }); + + final wallet = await Hive.openBox(testWalletId); + + // restore so we have something to rescan + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + // fetch wallet data + final preReceivingAddressesP2PKH = + await wallet.get('receivingAddressesP2PKH'); + final preReceivingAddressesP2SH = + await wallet.get('receivingAddressesP2SH'); + final preReceivingAddressesP2WPKH = + await wallet.get('receivingAddressesP2WPKH'); + final preChangeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); + final preChangeAddressesP2SH = await wallet.get('changeAddressesP2SH'); + final preChangeAddressesP2WPKH = + await wallet.get('changeAddressesP2WPKH'); + final preReceivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); + final preReceivingIndexP2SH = await wallet.get('receivingIndexP2SH'); + final preReceivingIndexP2WPKH = await wallet.get('receivingIndexP2WPKH'); + final preChangeIndexP2PKH = await wallet.get('changeIndexP2PKH'); + final preChangeIndexP2SH = await wallet.get('changeIndexP2SH'); + final preChangeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); + final preUtxoData = await wallet.get('latest_utxo_model'); + final preReceiveDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2PKH"); + final preChangeDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2PKH"); + final preReceiveDerivationsStringP2SH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2SH"); + final preChangeDerivationsStringP2SH = + await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); + final preReceiveDerivationsStringP2WPKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2WPKH"); + final preChangeDerivationsStringP2WPKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2WPKH"); + + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenThrow(Exception("fake exception")); + + bool hasThrown = false; + try { + await btc?.fullRescan(2, 1000); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, true); + + // fetch wallet data again + final receivingAddressesP2PKH = + await wallet.get('receivingAddressesP2PKH'); + final receivingAddressesP2SH = await wallet.get('receivingAddressesP2SH'); + final receivingAddressesP2WPKH = + await wallet.get('receivingAddressesP2WPKH'); + final changeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); + final changeAddressesP2SH = await wallet.get('changeAddressesP2SH'); + final changeAddressesP2WPKH = await wallet.get('changeAddressesP2WPKH'); + final receivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); + final receivingIndexP2SH = await wallet.get('receivingIndexP2SH'); + final receivingIndexP2WPKH = await wallet.get('receivingIndexP2WPKH'); + final changeIndexP2PKH = await wallet.get('changeIndexP2PKH'); + final changeIndexP2SH = await wallet.get('changeIndexP2SH'); + final changeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); + final utxoData = await wallet.get('latest_utxo_model'); + final receiveDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2PKH"); + final changeDerivationsStringP2PKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2PKH"); + final receiveDerivationsStringP2SH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2SH"); + final changeDerivationsStringP2SH = + await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); + final receiveDerivationsStringP2WPKH = await secureStore?.read( + key: "${testWalletId}_receiveDerivationsP2WPKH"); + final changeDerivationsStringP2WPKH = await secureStore?.read( + key: "${testWalletId}_changeDerivationsP2WPKH"); + + expect(preReceivingAddressesP2PKH, receivingAddressesP2PKH); + expect(preReceivingAddressesP2SH, receivingAddressesP2SH); + expect(preReceivingAddressesP2WPKH, receivingAddressesP2WPKH); + expect(preChangeAddressesP2PKH, changeAddressesP2PKH); + expect(preChangeAddressesP2SH, changeAddressesP2SH); + expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); + expect(preReceivingIndexP2PKH, receivingIndexP2PKH); + expect(preReceivingIndexP2SH, receivingIndexP2SH); + expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); + expect(preChangeIndexP2PKH, changeIndexP2PKH); + expect(preChangeIndexP2SH, changeIndexP2SH); + expect(preChangeIndexP2WPKH, changeIndexP2WPKH); + expect(preUtxoData, utxoData); + expect(preReceiveDerivationsStringP2PKH, receiveDerivationsStringP2PKH); + expect(preChangeDerivationsStringP2PKH, changeDerivationsStringP2PKH); + expect(preReceiveDerivationsStringP2SH, receiveDerivationsStringP2SH); + expect(preChangeDerivationsStringP2SH, changeDerivationsStringP2SH); + expect(preReceiveDerivationsStringP2WPKH, receiveDerivationsStringP2WPKH); + expect(preChangeDerivationsStringP2WPKH, changeDerivationsStringP2WPKH); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs1)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs2)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs3)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs4)).called(2); + verify(client?.getBatchHistory(args: historyBatchArgs5)).called(2); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.namecoin)) + .called(1); + + for (final arg in dynamicArgValues) { + final map = Map>.from(arg as Map); + + verify(client?.getBatchHistory(args: map)).called(1); + expect(activeScriptHashes.contains(map.values.first.first as String), + true); + } + + expect(secureStore?.writes, 19); + expect(secureStore?.reads, 32); + expect(secureStore?.deletes, 12); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + // test("fetchBuildTxData succeeds", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx11Raw); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to trigger all change code branches + // final chg44 = await secureStore?.read( + // key: testWalletId + "_changeDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_changeDerivationsP2PKH", + // value: chg44?.replaceFirst("1vFHF5q21GccoBwrB4zEUAs9i3Bfx797U", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final chg49 = + // await secureStore?.read(key: testWalletId + "_changeDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_changeDerivationsP2SH", + // value: chg49?.replaceFirst("3ANTVqufTH1tLAuoQHhng8jndRsA9hcNy7", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final chg84 = await secureStore?.read( + // key: testWalletId + "_changeDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_changeDerivationsP2WPKH", + // value: chg84?.replaceFirst( + // "bc1qn2x7h96kufgfjxtkhsnq03jqwqde8zasffqvd2", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // final data = await btc?.fetchBuildTxData(utxoList); + // + // expect(data?.length, 3); + // expect( + // data?["2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703"] + // ?.length, + // 2); + // expect( + // data?["ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7"] + // .length, + // 3); + // expect( + // data?["3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4"] + // .length, + // 2); + // expect( + // data?["2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703"] + // ["output"], + // isA()); + // expect( + // data?["ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7"] + // ["output"], + // isA()); + // expect( + // data?["3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4"] + // ["output"], + // isA()); + // expect( + // data?["2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703"] + // ["keyPair"], + // isA()); + // expect( + // data?["ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7"] + // ["keyPair"], + // isA()); + // expect( + // data?["3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4"] + // ["keyPair"], + // isA()); + // expect( + // data?["ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7"] + // ["redeemScript"], + // isA()); + // + // // modify addresses to trigger all receiving code branches + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // final data2 = await btc?.fetchBuildTxData(utxoList); + // + // expect(data2?.length, 3); + // expect( + // data2?["2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703"] + // .length, + // 2); + // expect( + // data2?["ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7"] + // .length, + // 3); + // expect( + // data2?["3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4"] + // .length, + // 2); + // expect( + // data2?["2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703"] + // ["output"], + // isA()); + // expect( + // data2?["ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7"] + // ["output"], + // isA()); + // expect( + // data2?["3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4"] + // ["output"], + // isA()); + // expect( + // data2?["2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703"] + // ["keyPair"], + // isA()); + // expect( + // data2?["ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7"] + // ["keyPair"], + // isA()); + // expect( + // data2?["3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4"] + // ["keyPair"], + // isA()); + // expect( + // data2?["ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7"] + // ["redeemScript"], + // isA()); + // + // verify(client?.getServerFeatures()).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(2); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(2); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(2); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // + // expect(secureStore?.interactions, 38); + // expect(secureStore?.writes, 13); + // expect(secureStore?.reads, 25); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("fetchBuildTxData throws", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenThrow(Exception("some exception")); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // bool didThrow = false; + // try { + // await btc?.fetchBuildTxData(utxoList); + // } catch (_) { + // didThrow = true; + // } + // expect(didThrow, true); + // + // verify(client?.getServerFeatures()).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // + // expect(secureStore?.interactions, 14); + // expect(secureStore?.writes, 7); + // expect(secureStore?.reads, 7); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("build transaction succeeds", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx11Raw); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // final data = await btc?.fetchBuildTxData(utxoList); + // + // final txData = await btc?.buildTransaction( + // utxosToUse: utxoList, + // utxoSigningData: data!, + // recipients: ["bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc"], + // satoshiAmounts: [13000]); + // + // expect(txData?.length, 2); + // expect(txData?["hex"], isA()); + // expect(txData?["vSize"], isA()); + // + // verify(client?.getServerFeatures()).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // + // expect(secureStore?.interactions, 26); + // expect(secureStore?.writes, 10); + // expect(secureStore?.reads, 16); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("build transaction fails", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx11Raw); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // final data = await btc?.fetchBuildTxData(utxoList); + // + // // give bad data toi build tx + // data["ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7"] + // ["keyPair"] = null; + // + // bool didThrow = false; + // try { + // await btc?.buildTransaction( + // utxosToUse: utxoList, + // utxoSigningData: data!, + // recipients: ["bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc"], + // satoshiAmounts: [13000]); + // } catch (_) { + // didThrow = true; + // } + // expect(didThrow, true); + // + // verify(client?.getServerFeatures()).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // + // expect(secureStore?.interactions, 26); + // expect(secureStore?.writes, 10); + // expect(secureStore?.reads, 16); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("two output coinSelection succeeds", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getHistory(scripthash: anyNamed("scripthash"))) + // .thenAnswer((_) async => [ + // {"height": 1000, "tx_hash": "some tx hash"} + // ]); + // when(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx11Raw); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // final result = await btc?.coinSelection( + // 18000, 1000, "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc", + // utxos: utxoList); + // + // expect(result, isA>()); + // expect(result.length > 0, true); + // + // verify(client?.getServerFeatures()).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); + // + // expect(secureStore?.interactions, 29); + // expect(secureStore?.writes, 11); + // expect(secureStore?.reads, 18); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("one output option A coinSelection", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getHistory(scripthash: anyNamed("scripthash"))) + // .thenAnswer((_) async => [ + // {"height": 1000, "tx_hash": "some tx hash"} + // ]); + // when(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx11Raw); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // final result = await btc?.coinSelection( + // 18500, 1000, "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc", + // utxos: utxoList); + // + // expect(result, isA>()); + // expect(result.length > 0, true); + // + // verify(client?.getServerFeatures()).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // + // expect(secureStore?.interactions, 26); + // expect(secureStore?.writes, 10); + // expect(secureStore?.reads, 16); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("one output option B coinSelection", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getHistory(scripthash: anyNamed("scripthash"))) + // .thenAnswer((_) async => [ + // {"height": 1000, "tx_hash": "some tx hash"} + // ]); + // when(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx11Raw); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = + // await secureStore?.read(key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // final result = await btc?.coinSelection( + // 18651, 1000, "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc", + // utxos: utxoList); + // + // expect(result, isA>()); + // expect(result.length > 0, true); + // + // verify(client?.getServerFeatures()).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // + // expect(secureStore?.interactions, 26); + // expect(secureStore?.writes, 10); + // expect(secureStore?.reads, 16); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("insufficient funds option A coinSelection", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = + // await secureStore?.read(key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // final result = await btc?.coinSelection( + // 20000, 1000, "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc", + // utxos: utxoList); + // + // expect(result, 1); + // + // verify(client?.getServerFeatures()).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // + // expect(secureStore?.interactions, 20); + // expect(secureStore?.writes, 10); + // expect(secureStore?.reads, 10); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("insufficient funds option B coinSelection", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = + // await secureStore?.read(key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // final result = await btc?.coinSelection( + // 19000, 1000, "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc", + // utxos: utxoList); + // + // expect(result, 2); + // + // verify(client?.getServerFeatures()).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // + // expect(secureStore?.interactions, 20); + // expect(secureStore?.writes, 10); + // expect(secureStore?.reads, 10); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("insufficient funds option C coinSelection", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(cachedClient.?getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx11Raw); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = + // await secureStore?.read(key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // final result = await btc?.coinSelection( + // 18900, 1000, "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc", + // utxos: utxoList); + // + // expect(result, 2); + // + // verify(client?.getServerFeatures()).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // + // expect(secureStore?.interactions, 26); + // expect(secureStore?.writes, 10); + // expect(secureStore?.reads, 16); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("check for more outputs coinSelection", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx11Raw); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = + // await secureStore?.read(key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // when(client?.getHistory(scripthash: anyNamed("scripthash"))) + // .thenAnswer((_) async => [ + // {"height": 1000, "tx_hash": "some tx hash"} + // ]); + // + // final result = await btc?.coinSelection( + // 11900, 1000, "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc", + // utxos: utxoList); + // + // expect(result, isA>()); + // expect(result.length > 0, true); + // + // verify(client?.getServerFeatures()).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(2); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(2); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // + // verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); + // + // expect(secureStore?.interactions, 33); + // expect(secureStore?.writes, 11); + // expect(secureStore?.reads, 22); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + // + // test("prepareSend and confirmSend succeed", () async { + // when(client?.getServerFeatures()).thenAnswer((_) async => { + // "hosts": {}, + // "pruning": null, + // "server_version": "Unit tests", + // "protocol_min": "1.4", + // "protocol_max": "1.4.2", + // "genesis_hash": GENESIS_HASH_MAINNET, + // "hash_function": "sha256", + // "services": [] + // }); + // when(client?.getBatchHistory(args: historyBatchArgs0)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getBatchHistory(args: historyBatchArgs1)) + // .thenAnswer((_) async => historyBatchResponse); + // when(client?.getHistory(scripthash: anyNamed("scripthash"))) + // .thenAnswer((_) async => [ + // {"height": 1000, "tx_hash": "some tx hash"} + // ]); + // when(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx9Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx10Raw); + // when(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .thenAnswer((_) async => tx11Raw); + // + // // recover to fill data + // await btc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000); + // + // // modify addresses to properly mock data to build a tx + // final rcv44 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2PKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2PKH", + // value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + // "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + // final rcv49 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2SH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2SH", + // value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + // "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + // final rcv84 = await secureStore?.read( + // key: testWalletId + "_receiveDerivationsP2WPKH"); + // await secureStore?.write( + // key: testWalletId + "_receiveDerivationsP2WPKH", + // value: rcv84?.replaceFirst( + // "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + // "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + // + // btc?.outputsList = utxoList; + // + // final result = await btc?.prepareSend( + // toAddress: "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc", + // amount: 15000); + // + // expect(result, isA>()); + // expect(result?.length! > 0, true); + // + // when(client?.broadcastTransaction( + // rawTx: result!["hex"], requestID: anyNamed("requestID"))) + // .thenAnswer((_) async => "some txHash"); + // + // final sentResult = await btc?.confirmSend(txData: result!); + // expect(sentResult, "some txHash"); + // + // verify(client?.getServerFeatures()).called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // tx_hash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coinName: "Bitcoin", + // callOutSideMainIsolate: false)) + // .called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + // verify(client?.broadcastTransaction( + // rawTx: result!["hex"], requestID: anyNamed("requestID"))) + // .called(1); + // verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); + // + // expect(secureStore?.interactions, 29); + // expect(secureStore?.writes, 11); + // expect(secureStore?.reads, 18); + // expect(secureStore?.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + test("prepareSend fails", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs2)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs3)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs4)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs5)) + .thenAnswer((_) async => historyBatchResponse); + + List dynamicArgValues = []; + + when(client?.getBatchHistory(args: anyNamed("args"))) + .thenAnswer((realInvocation) async { + if (realInvocation.namedArguments.values.first.length == 1) { + dynamicArgValues.add(realInvocation.namedArguments.values.first); + } + + return historyBatchResponse; + }); + + await Hive.openBox(testWalletId); + + when(cachedClient?.getTransaction( + txHash: + "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + coin: Coin.namecoin)) + .thenAnswer((_) async => tx9Raw); + when(cachedClient?.getTransaction( + txHash: + "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + coin: Coin.namecoin)) + .thenAnswer((_) async => tx10Raw); + when(cachedClient?.getTransaction( + txHash: + "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + coin: Coin.namecoin, + )).thenAnswer((_) async => tx11Raw); + + // recover to fill data + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + // modify addresses to properly mock data to build a tx + final rcv44 = await secureStore?.read( + key: testWalletId + "_receiveDerivationsP2PKH"); + await secureStore?.write( + key: testWalletId + "_receiveDerivationsP2PKH", + value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", + "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); + final rcv49 = await secureStore?.read( + key: testWalletId + "_receiveDerivationsP2SH"); + await secureStore?.write( + key: testWalletId + "_receiveDerivationsP2SH", + value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", + "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); + final rcv84 = await secureStore?.read( + key: testWalletId + "_receiveDerivationsP2WPKH"); + await secureStore?.write( + key: testWalletId + "_receiveDerivationsP2WPKH", + value: rcv84?.replaceFirst( + "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", + "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc")); + + btc?.outputsList = utxoList; + + bool didThrow = false; + try { + await btc?.prepareSend( + address: "bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc", + satoshiAmount: 15000); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + verify(client?.getServerFeatures()).called(1); + + /// verify transaction no matching calls + + // verify(cachedClient?.getTransaction( + // txHash: + // "2087ce09bc316877c9f10971526a2bffa3078d52ea31752639305cdcd8230703", + // coin: Coin.namecoin, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coin: Coin.namecoin, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coin: Coin.namecoin, + // callOutSideMainIsolate: false)) + // .called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs2)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs3)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs4)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs5)).called(1); + + for (final arg in dynamicArgValues) { + final map = Map>.from(arg as Map); + + verify(client?.getBatchHistory(args: map)).called(1); + expect(activeScriptHashes.contains(map.values.first.first as String), + true); + } + + expect(secureStore?.interactions, 20); + expect(secureStore?.writes, 10); + expect(secureStore?.reads, 10); + expect(secureStore?.deletes, 0); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("confirmSend no hex", () async { + bool didThrow = false; + try { + await btc?.confirmSend(txData: {"some": "strange map"}); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("confirmSend hex is not string", () async { + bool didThrow = false; + try { + await btc?.confirmSend(txData: {"hex": true}); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("confirmSend hex is string but missing other data", () async { + bool didThrow = false; + try { + await btc?.confirmSend(txData: {"hex": "a string"}); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + verify(client?.broadcastTransaction( + rawTx: "a string", requestID: anyNamed("requestID"))) + .called(1); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("confirmSend fails due to vSize being greater than fee", () async { + bool didThrow = false; + try { + await btc + ?.confirmSend(txData: {"hex": "a string", "fee": 1, "vSize": 10}); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + verify(client?.broadcastTransaction( + rawTx: "a string", requestID: anyNamed("requestID"))) + .called(1); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("confirmSend fails when broadcast transactions throws", () async { + when(client?.broadcastTransaction( + rawTx: "a string", requestID: anyNamed("requestID"))) + .thenThrow(Exception("some exception")); + + bool didThrow = false; + try { + await btc + ?.confirmSend(txData: {"hex": "a string", "fee": 10, "vSize": 10}); + } catch (_) { + didThrow = true; + } + + expect(didThrow, true); + + verify(client?.broadcastTransaction( + rawTx: "a string", requestID: anyNamed("requestID"))) + .called(1); + + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + // + // // this test will create a non mocked electrumx client that will try to connect + // // to the provided ipAddress below. This will throw a bunch of errors + // // which what we want here as actually calling electrumx calls here is unwanted. + // // test("listen to NodesChangedEvent", () async { + // // btc = NamecoinWallet( + // // walletId: testWalletId, + // // walletName: testWalletName, + // // networkType: BasicNetworkType.test, + // // client: client, + // // cachedClient: cachedClient, + // // priceAPI: priceAPI, + // // secureStore: secureStore, + // // ); + // // + // // // set node + // // final wallet = await Hive.openBox(testWalletId); + // // await wallet.put("nodes", { + // // "default": { + // // "id": "some nodeID", + // // "ipAddress": "some address", + // // "port": "9000", + // // "useSSL": true, + // // } + // // }); + // // await wallet.put("activeNodeID_Bitcoin", "default"); + // // + // // final a = btc.cachedElectrumXClient; + // // + // // // return when refresh is called on node changed trigger + // // btc.longMutex = true; + // // + // // GlobalEventBus.instance + // // .fire(NodesChangedEvent(NodesChangedEventType.updatedCurrentNode)); + // // + // // // make sure event has processed before continuing + // // await Future.delayed(Duration(seconds: 5)); + // // + // // final b = btc.cachedElectrumXClient; + // // + // // expect(identical(a, b), false); + // // + // // await btc.exit(); + // // + // // expect(secureStore.interactions, 0); + // // verifyNoMoreInteractions(client); + // // verifyNoMoreInteractions(cachedClient); + // // verifyNoMoreInteractions(priceAPI); + // // }); + + test("refresh wallet mutex locked", () async { + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getBatchHistory(args: historyBatchArgs0)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs2)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs3)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs4)) + .thenAnswer((_) async => historyBatchResponse); + when(client?.getBatchHistory(args: historyBatchArgs5)) + .thenAnswer((_) async => historyBatchResponse); + + List dynamicArgValues = []; + + when(client?.getBatchHistory(args: anyNamed("args"))) + .thenAnswer((realInvocation) async { + if (realInvocation.namedArguments.values.first.length == 1) { + dynamicArgValues.add(realInvocation.namedArguments.values.first); + } + + return historyBatchResponse; + }); + + await Hive.openBox(testWalletId); + + // recover to fill data + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + btc?.refreshMutex = true; + + await btc?.refresh(); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs2)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs3)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs4)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs5)).called(1); + + for (final arg in dynamicArgValues) { + final map = Map>.from(arg as Map); + + verify(client?.getBatchHistory(args: map)).called(1); + expect(activeScriptHashes.contains(map.values.first.first as String), + true); + } + + expect(secureStore?.interactions, 14); + expect(secureStore?.writes, 7); + expect(secureStore?.reads, 7); + expect(secureStore?.deletes, 0); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("refresh wallet normally", () async { + when(client?.getBlockHeadTip()).thenAnswer((realInvocation) async => + {"height": 520481, "hex": "some block hex"}); + when(client?.getServerFeatures()).thenAnswer((_) async => { + "hosts": {}, + "pruning": null, + "server_version": "Unit tests", + "protocol_min": "1.4", + "protocol_max": "1.4.2", + "genesis_hash": GENESIS_HASH_MAINNET, + "hash_function": "sha256", + "services": [] + }); + when(client?.getHistory(scripthash: anyNamed("scripthash"))) + .thenAnswer((_) async => []); + when(client?.estimateFee(blocks: anyNamed("blocks"))) + .thenAnswer((_) async => Decimal.one); + + when(priceAPI?.getPricesAnd24hChange(baseCurrency: "USD")) + .thenAnswer((_) async => {Coin.namecoin: Tuple2(Decimal.one, 0.3)}); + + final List dynamicArgValues = []; + + when(client?.getBatchHistory(args: anyNamed("args"))) + .thenAnswer((realInvocation) async { + dynamicArgValues.add(realInvocation.namedArguments.values.first); + return historyBatchResponse; + }); + + await Hive.openBox(testWalletId); + + // recover to fill data + await btc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + when(client?.getBatchHistory(args: anyNamed("args"))) + .thenAnswer((_) async => {}); + when(client?.getBatchUTXOs(args: anyNamed("args"))) + .thenAnswer((_) async => emptyHistoryBatchResponse); + + await btc?.refresh(); + + verify(client?.getServerFeatures()).called(1); + verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(4); + verify(client?.estimateFee(blocks: anyNamed("blocks"))).called(3); + verify(client?.getBlockHeadTip()).called(1); + verify(priceAPI?.getPricesAnd24hChange(baseCurrency: "USD")).called(2); + + for (final arg in dynamicArgValues) { + final map = Map>.from(arg as Map); + + verify(client?.getBatchHistory(args: map)).called(1); + } + + expect(secureStore?.interactions, 14); + expect(secureStore?.writes, 7); + expect(secureStore?.reads, 7); + expect(secureStore?.deletes, 0); + + // verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + tearDown(() async { + await tearDownTestHive(); + }); + }); +} diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart new file mode 100644 index 000000000..d86949a61 --- /dev/null +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -0,0 +1,352 @@ +// Mocks generated by Mockito 5.2.0 from annotations +// in stackwallet/test/services/coins/namecoin/namecoin_wallet_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i6; + +import 'package:decimal/decimal.dart' as _i2; +import 'package:http/http.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i7; +import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i5; +import 'package:stackwallet/services/price.dart' as _i9; +import 'package:stackwallet/services/transaction_notification_tracker.dart' + as _i11; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i8; +import 'package:stackwallet/utilities/prefs.dart' as _i3; +import 'package:tuple/tuple.dart' as _i10; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeDecimal_0 extends _i1.Fake implements _i2.Decimal {} + +class _FakePrefs_1 extends _i1.Fake implements _i3.Prefs {} + +class _FakeClient_2 extends _i1.Fake implements _i4.Client {} + +/// A class which mocks [ElectrumX]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockElectrumX extends _i1.Mock implements _i5.ElectrumX { + MockElectrumX() { + _i1.throwOnMissingStub(this); + } + + @override + set failovers(List<_i5.ElectrumXNode>? _failovers) => + super.noSuchMethod(Invocation.setter(#failovers, _failovers), + returnValueForMissingStub: null); + @override + int get currentFailoverIndex => + (super.noSuchMethod(Invocation.getter(#currentFailoverIndex), + returnValue: 0) as int); + @override + set currentFailoverIndex(int? _currentFailoverIndex) => super.noSuchMethod( + Invocation.setter(#currentFailoverIndex, _currentFailoverIndex), + returnValueForMissingStub: null); + @override + String get host => + (super.noSuchMethod(Invocation.getter(#host), returnValue: '') as String); + @override + int get port => + (super.noSuchMethod(Invocation.getter(#port), returnValue: 0) as int); + @override + bool get useSSL => + (super.noSuchMethod(Invocation.getter(#useSSL), returnValue: false) + as bool); + @override + _i6.Future request( + {String? command, + List? args = const [], + Duration? connectionTimeout = const Duration(seconds: 60), + String? requestID, + int? retries = 2}) => + (super.noSuchMethod( + Invocation.method(#request, [], { + #command: command, + #args: args, + #connectionTimeout: connectionTimeout, + #requestID: requestID, + #retries: retries + }), + returnValue: Future.value()) as _i6.Future); + @override + _i6.Future>> batchRequest( + {String? command, + Map>? args, + Duration? connectionTimeout = const Duration(seconds: 60), + int? retries = 2}) => + (super.noSuchMethod( + Invocation.method(#batchRequest, [], { + #command: command, + #args: args, + #connectionTimeout: connectionTimeout, + #retries: retries + }), + returnValue: Future>>.value( + >[])) + as _i6.Future>>); + @override + _i6.Future ping({String? requestID, int? retryCount = 1}) => + (super.noSuchMethod( + Invocation.method( + #ping, [], {#requestID: requestID, #retryCount: retryCount}), + returnValue: Future.value(false)) as _i6.Future); + @override + _i6.Future> getBlockHeadTip({String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getBlockHeadTip, [], {#requestID: requestID}), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future> getServerFeatures({String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getServerFeatures, [], {#requestID: requestID}), + returnValue: + Future>.value({})) as _i6 + .Future>); + @override + _i6.Future broadcastTransaction({String? rawTx, String? requestID}) => + (super.noSuchMethod( + Invocation.method(#broadcastTransaction, [], + {#rawTx: rawTx, #requestID: requestID}), + returnValue: Future.value('')) as _i6.Future); + @override + _i6.Future> getBalance( + {String? scripthash, String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getBalance, [], + {#scripthash: scripthash, #requestID: requestID}), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future>> getHistory( + {String? scripthash, String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getHistory, [], + {#scripthash: scripthash, #requestID: requestID}), + returnValue: Future>>.value( + >[])) + as _i6.Future>>); + @override + _i6.Future>>> getBatchHistory( + {Map>? args}) => + (super.noSuchMethod( + Invocation.method(#getBatchHistory, [], {#args: args}), + returnValue: Future>>>.value( + >>{})) as _i6 + .Future>>>); + @override + _i6.Future>> getUTXOs( + {String? scripthash, String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getUTXOs, [], {#scripthash: scripthash, #requestID: requestID}), + returnValue: Future>>.value( + >[])) as _i6 + .Future>>); + @override + _i6.Future>>> getBatchUTXOs( + {Map>? args}) => + (super.noSuchMethod(Invocation.method(#getBatchUTXOs, [], {#args: args}), + returnValue: Future>>>.value( + >>{})) as _i6 + .Future>>>); + @override + _i6.Future> getTransaction( + {String? txHash, bool? verbose = true, String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getTransaction, [], + {#txHash: txHash, #verbose: verbose, #requestID: requestID}), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future> getAnonymitySet( + {String? groupId = r'1', + String? blockhash = r'', + String? requestID}) => + (super.noSuchMethod( + Invocation.method(#getAnonymitySet, [], { + #groupId: groupId, + #blockhash: blockhash, + #requestID: requestID + }), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future getMintData({dynamic mints, String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getMintData, [], {#mints: mints, #requestID: requestID}), + returnValue: Future.value()) as _i6.Future); + @override + _i6.Future> getUsedCoinSerials( + {String? requestID, int? startNumber}) => + (super.noSuchMethod( + Invocation.method(#getUsedCoinSerials, [], + {#requestID: requestID, #startNumber: startNumber}), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future getLatestCoinId({String? requestID}) => (super.noSuchMethod( + Invocation.method(#getLatestCoinId, [], {#requestID: requestID}), + returnValue: Future.value(0)) as _i6.Future); + @override + _i6.Future> getFeeRate({String? requestID}) => (super + .noSuchMethod(Invocation.method(#getFeeRate, [], {#requestID: requestID}), + returnValue: + Future>.value({})) as _i6 + .Future>); + @override + _i6.Future<_i2.Decimal> estimateFee({String? requestID, int? blocks}) => + (super.noSuchMethod( + Invocation.method( + #estimateFee, [], {#requestID: requestID, #blocks: blocks}), + returnValue: Future<_i2.Decimal>.value(_FakeDecimal_0())) + as _i6.Future<_i2.Decimal>); + @override + _i6.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + Invocation.method(#relayFee, [], {#requestID: requestID}), + returnValue: Future<_i2.Decimal>.value(_FakeDecimal_0())) + as _i6.Future<_i2.Decimal>); +} + +/// A class which mocks [CachedElectrumX]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { + MockCachedElectrumX() { + _i1.throwOnMissingStub(this); + } + + @override + String get server => + (super.noSuchMethod(Invocation.getter(#server), returnValue: '') + as String); + @override + int get port => + (super.noSuchMethod(Invocation.getter(#port), returnValue: 0) as int); + @override + bool get useSSL => + (super.noSuchMethod(Invocation.getter(#useSSL), returnValue: false) + as bool); + @override + _i3.Prefs get prefs => (super.noSuchMethod(Invocation.getter(#prefs), + returnValue: _FakePrefs_1()) as _i3.Prefs); + @override + List<_i5.ElectrumXNode> get failovers => + (super.noSuchMethod(Invocation.getter(#failovers), + returnValue: <_i5.ElectrumXNode>[]) as List<_i5.ElectrumXNode>); + @override + _i6.Future> getAnonymitySet( + {String? groupId, String? blockhash = r'', _i8.Coin? coin}) => + (super.noSuchMethod( + Invocation.method(#getAnonymitySet, [], + {#groupId: groupId, #blockhash: blockhash, #coin: coin}), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future> getTransaction( + {String? txHash, _i8.Coin? coin, bool? verbose = true}) => + (super.noSuchMethod( + Invocation.method(#getTransaction, [], + {#txHash: txHash, #coin: coin, #verbose: verbose}), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future> getUsedCoinSerials( + {_i8.Coin? coin, int? startNumber = 0}) => + (super.noSuchMethod( + Invocation.method(#getUsedCoinSerials, [], + {#coin: coin, #startNumber: startNumber}), + returnValue: Future>.value([])) + as _i6.Future>); + @override + _i6.Future clearSharedTransactionCache({_i8.Coin? coin}) => + (super.noSuchMethod( + Invocation.method(#clearSharedTransactionCache, [], {#coin: coin}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i6.Future); +} + +/// A class which mocks [PriceAPI]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPriceAPI extends _i1.Mock implements _i9.PriceAPI { + MockPriceAPI() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Client get client => (super.noSuchMethod(Invocation.getter(#client), + returnValue: _FakeClient_2()) as _i4.Client); + @override + void resetLastCalledToForceNextCallToUpdateCache() => super.noSuchMethod( + Invocation.method(#resetLastCalledToForceNextCallToUpdateCache, []), + returnValueForMissingStub: null); + @override + _i6.Future>> + getPricesAnd24hChange({String? baseCurrency}) => (super.noSuchMethod( + Invocation.method( + #getPricesAnd24hChange, [], {#baseCurrency: baseCurrency}), + returnValue: + Future>>.value( + <_i8.Coin, _i10.Tuple2<_i2.Decimal, double>>{})) + as _i6.Future>>); +} + +/// A class which mocks [TransactionNotificationTracker]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTransactionNotificationTracker extends _i1.Mock + implements _i11.TransactionNotificationTracker { + MockTransactionNotificationTracker() { + _i1.throwOnMissingStub(this); + } + + @override + String get walletId => + (super.noSuchMethod(Invocation.getter(#walletId), returnValue: '') + as String); + @override + List get pendings => + (super.noSuchMethod(Invocation.getter(#pendings), returnValue: []) + as List); + @override + List get confirmeds => (super + .noSuchMethod(Invocation.getter(#confirmeds), returnValue: []) + as List); + @override + bool wasNotifiedPending(String? txid) => + (super.noSuchMethod(Invocation.method(#wasNotifiedPending, [txid]), + returnValue: false) as bool); + @override + _i6.Future addNotifiedPending(String? txid) => + (super.noSuchMethod(Invocation.method(#addNotifiedPending, [txid]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i6.Future); + @override + bool wasNotifiedConfirmed(String? txid) => + (super.noSuchMethod(Invocation.method(#wasNotifiedConfirmed, [txid]), + returnValue: false) as bool); + @override + _i6.Future addNotifiedConfirmed(String? txid) => + (super.noSuchMethod(Invocation.method(#addNotifiedConfirmed, [txid]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i6.Future); +}