From 7e92be4b67ed2153e3620343e162c557034ee32a Mon Sep 17 00:00:00 2001 From: Likho Date: Tue, 25 Oct 2022 17:06:37 +0200 Subject: [PATCH 001/103] WIP: Add particle --- lib/pages/exchange_view/send_from_view.dart | 2 + .../add_edit_node_view.dart | 4 + lib/services/coins/coin_service.dart | 19 + .../coins/particl/particl_wallet.dart | 3813 +++++++++++++++++ lib/utilities/address_utils.dart | 5 + lib/utilities/assets.dart | 12 + lib/utilities/block_explorers.dart | 4 + lib/utilities/constants.dart | 6 + lib/utilities/default_nodes.dart | 29 + lib/utilities/enums/coin_enum.dart | 31 + lib/utilities/theme/color_theme.dart | 4 + lib/utilities/theme/stack_colors.dart | 3 + 12 files changed, 3932 insertions(+) create mode 100644 lib/services/coins/particl/particl_wallet.dart diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 51c420c82..7dd841d65 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -56,10 +56,12 @@ class _SendFromViewState extends ConsumerState { case Coin.epicCash: case Coin.firo: case Coin.namecoin: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.bitcoincashTestnet: case Coin.dogecoinTestNet: case Coin.firoTestNet: + case Coin.particlTestNet: return amount.toStringAsFixed(Constants.decimalPlaces); case Coin.monero: return amount.toStringAsFixed(Constants.decimalPlacesMonero); 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 143b1e84d..ce422ddf9 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 @@ -118,10 +118,12 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.dogecoin: case Coin.firo: case Coin.namecoin: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: + case Coin.particlTestNet: final client = ElectrumX( host: formData.host!, port: formData.port!, @@ -533,10 +535,12 @@ class _NodeFormState extends ConsumerState { case Coin.firo: case Coin.namecoin: case Coin.bitcoincash: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: + case Coin.particlTestNet: return false; case Coin.epicCash: diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 089b20a33..6d3cbd4e3 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; +import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; @@ -146,6 +147,24 @@ abstract class CoinServiceAPI { // tracker: tracker, ); + case Coin.particl: + return ParticlWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + client: client, + cachedClient: cachedClient, + tracker: tracker); + + case Coin.particlTestNet: + return ParticlWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + client: client, + cachedClient: cachedClient, + tracker: tracker); + case Coin.wownero: return WowneroWallet( walletId: walletId, diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart new file mode 100644 index 000000000..77d0fa7db --- /dev/null +++ b/lib/services/coins/particl/particl_wallet.dart @@ -0,0 +1,3813 @@ +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 = 1; +const int DUST_LIMIT = 294; + +const String GENESIS_HASH_MAINNET = + "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"; +const String GENESIS_HASH_TESTNET = + "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; + +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 0x6c: // PART mainnet wif + coinType = "44"; // PART mainnet + break; + case 0x2e: // PART testnet wif + coinType = "1"; // PART testnet + break; + default: + throw Exception("Invalid Particl 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 ParticlWallet 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.particl: + return particl; + case Coin.particlTestNet: + return particltestnet; + 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); + } 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.particl: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.particlTestNet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a ParticlWallet using a non particl 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) + .data), + network: _network) + .data + .address!; + break; + case DerivePathType.bip84: + address = P2WPKH( + network: _network, + data: PaymentData(pubkey: node.publicKey)) + .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); + } + + @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; + + ParticlWallet({ + 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.particl: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.particlTestNet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a ParticlWallet using a non particl 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).data), + network: _network) + .data + .address!; + break; + case DerivePathType.bip84: + address = P2WPKH(network: _network, data: data).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); + 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 particl address + String _convertToScriptHash(String particlAddress, NetworkType network) { + try { + final output = Address.addressToOutputScript(particlAddress, 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 = 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"]["address"] 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"]["address"] 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 String address = output["scriptPubKey"]!["address"] as String; + 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"]["address"]; + 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 = []; + + 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"]["address"] 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) + .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) + .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, + ).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, + ).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(1); + + // Add transaction inputs + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + txb.addInput(txid, utxosToUse[i].vout, null, + utxoSigningData[txid]["output"] as Uint8List); + } + + // Add transaction output + for (var i = 0; i < recipients.length; i++) { + txb.addOutput(recipients[i], satoshiAmounts[i]); + } + + try { + // Sign the transaction accordingly + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + txb.sign( + vin: i, + keyPair: utxoSigningData[txid]["keyPair"] as ECPair, + witnessValue: utxosToUse[i].value, + redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + ); + } + } catch (e, s) { + Logging.instance.log("Caught exception while signing transaction: $e\n$s", + level: LogLevel.Error); + rethrow; + } + + final builtTx = txb.build(); + final vSize = builtTx.virtualSize(); + + return {"hex": builtTx.toHex(), "vSize": vSize}; + } + + @override + Future 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; + } + } + + 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; + } + } +} + +// Particl Network +final particl = NetworkType( + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'pw', + bip32: Bip32Type(public: 0x696e82d1, private: 0x8f1daeb8), + pubKeyHash: 0x38, + scriptHash: 0x3c, + wif: 0x6c); + +final particltestnet = NetworkType( + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'tpw', + bip32: Bip32Type(public: 0xe1427800, private: 0x04889478), + pubKeyHash: 0x76, + scriptHash: 0x7a, + wif: 0x2e); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index d73c5d3ce..91cedefd9 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; +import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -58,6 +59,8 @@ class AddressUtils { RegExp("[a-zA-Z0-9]{106}").hasMatch(address); case Coin.namecoin: return Address.validateAddress(address, namecoin, namecoin.bech32!); + case Coin.particl: + return Address.validateAddress(address, particl); case Coin.bitcoinTestNet: return Address.validateAddress(address, testnet); case Coin.bitcoincashTestnet: @@ -66,6 +69,8 @@ class AddressUtils { return Address.validateAddress(address, firoTestNetwork); case Coin.dogecoinTestNet: return Address.validateAddress(address, dogecointestnet); + case Coin.particlTestNet: + return Address.validateAddress(address, particltestnet); } } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 1410d6442..df26f4d8f 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -142,6 +142,8 @@ class _SVG { String get monero => "assets/svg/coin_icons/Monero.svg"; String get wownero => "assets/svg/coin_icons/Wownero.svg"; String get namecoin => "assets/svg/coin_icons/Namecoin.svg"; + String get particl => + "assets/svg/coin_icons/Namecoin.svg"; //TODO - Update icon to particl String get chevronRight => "assets/svg/chevron-right.svg"; String get minimize => "assets/svg/minimize.svg"; @@ -154,6 +156,8 @@ class _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"; + String get particlTestnet => + "assets/svg/coin_icons/Dogecoin.svg"; //TODO - Update icon to particl String iconFor({required Coin coin}) { switch (coin) { @@ -173,6 +177,8 @@ class _SVG { return wownero; case Coin.namecoin: return namecoin; + case Coin.particl: + return particl; case Coin.bitcoinTestNet: return bitcoinTestnet; case Coin.bitcoincashTestnet: @@ -181,6 +187,8 @@ class _SVG { return firoTestnet; case Coin.dogecoinTestNet: return dogecoinTestnet; + case Coin.particlTestNet: + return particlTestnet; } } } @@ -199,6 +207,7 @@ class _PNG { String get epicCash => "assets/images/epic-cash.png"; String get bitcoincash => "assets/images/bitcoincash.png"; String get namecoin => "assets/images/namecoin.png"; + String get particl => "assets/images/namecoin.png"; //TODO - use particl png String imageFor({required Coin coin}) { switch (coin) { @@ -223,6 +232,9 @@ class _PNG { return wownero; case Coin.namecoin: return namecoin; + case Coin.particl: + case Coin.particlTestNet: + return particl; } } } diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index f89f270be..52d84b815 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -31,5 +31,9 @@ Uri getBlockExplorerTransactionUrlFor({ "https://blockexplorer.one/bitcoin-cash/testnet/tx/$txid"); case Coin.namecoin: return Uri.parse("https://chainz.cryptoid.info/nmc/tx.dws?$txid.htm"); + case Coin.particl: + return Uri.parse("https://chainz.cryptoid.info/part/tx.dws?$txid.htm"); + case Coin.particlTestNet: + return Uri.parse(""); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 2495a6be4..3258aa74f 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -51,6 +51,8 @@ abstract class Constants { case Coin.firoTestNet: case Coin.epicCash: case Coin.namecoin: + case Coin.particl: + case Coin.particlTestNet: values.addAll([24, 21, 18, 15, 12]); break; @@ -94,6 +96,10 @@ abstract class Constants { case Coin.namecoin: return 600; + + case Coin.particl: + case Coin.particlTestNet: + return 600; } } diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 566b829ce..8b6a9dadc 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -118,6 +118,17 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get particl => NodeModel( + host: "host", + port: 123, + name: defaultName, + id: _nodeId(Coin.particl), + useSSL: true, + enabled: true, + coinName: Coin.particl.name, + isFailover: true, + isDown: false); //TODO - UPDATE WITH CORRECT DETAILS + static NodeModel get bitcoinTestnet => NodeModel( host: "electrumx-testnet.cypherstack.com", port: 51002, @@ -166,6 +177,18 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get particlTestnet => NodeModel( + host: "host", + port: 60002, + name: defaultName, + id: _nodeId(Coin.particlTestNet), + useSSL: true, + enabled: true, + coinName: Coin.particlTestNet.name, + isFailover: true, + isDown: false, + ); + static NodeModel getNodeFor(Coin coin) { switch (coin) { case Coin.bitcoin: @@ -192,6 +215,9 @@ abstract class DefaultNodes { case Coin.namecoin: return namecoin; + case Coin.particl: + return namecoin; + case Coin.bitcoinTestNet: return bitcoinTestnet; @@ -203,6 +229,9 @@ abstract class DefaultNodes { case Coin.dogecoinTestNet: return dogecoinTestnet; + + case Coin.particlTestNet: + return particlTestnet; } } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index c25a5ca4e..0454dc1f2 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -10,6 +10,8 @@ import 'package:stackwallet/services/coins/monero/monero_wallet.dart' as xmr; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; +import 'package:stackwallet/services/coins/particl/particl_wallet.dart' + as particl; enum Coin { bitcoin, @@ -18,6 +20,7 @@ enum Coin { epicCash, firo, monero, + particl, wownero, namecoin, @@ -29,6 +32,7 @@ enum Coin { bitcoincashTestnet, dogecoinTestNet, firoTestNet, + particlTestNet } // remove firotestnet for now @@ -49,6 +53,8 @@ extension CoinExt on Coin { return "Firo"; case Coin.monero: return "Monero"; + case Coin.particl: + return "Particl"; case Coin.wownero: return "Wownero"; case Coin.namecoin: @@ -61,6 +67,8 @@ extension CoinExt on Coin { return "tFiro"; case Coin.dogecoinTestNet: return "tDogecoin"; + case Coin.particlTestNet: + return "tParticl"; } } @@ -78,6 +86,8 @@ extension CoinExt on Coin { return "FIRO"; case Coin.monero: return "XMR"; + case Coin.particl: + return "PART"; case Coin.wownero: return "WOW"; case Coin.namecoin: @@ -90,6 +100,8 @@ extension CoinExt on Coin { return "tFIRO"; case Coin.dogecoinTestNet: return "tDOGE"; + case Coin.particlTestNet: + return "tPART"; } } @@ -108,6 +120,8 @@ extension CoinExt on Coin { return "firo"; case Coin.monero: return "monero"; + case Coin.particl: + return "particl"; case Coin.wownero: return "wownero"; case Coin.namecoin: @@ -120,6 +134,8 @@ extension CoinExt on Coin { return "firo"; case Coin.dogecoinTestNet: return "dogecoin"; + case Coin.particlTestNet: + return "particl"; } } @@ -130,10 +146,12 @@ extension CoinExt on Coin { case Coin.dogecoin: case Coin.firo: case Coin.namecoin: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: + case Coin.particlTestNet: return true; case Coin.epicCash: @@ -167,6 +185,10 @@ extension CoinExt on Coin { case Coin.monero: return xmr.MINIMUM_CONFIRMATIONS; + case Coin.particl: + case Coin.particlTestNet: + return particl.MINIMUM_CONFIRMATIONS; + case Coin.wownero: return wow.MINIMUM_CONFIRMATIONS; @@ -203,6 +225,11 @@ Coin coinFromPrettyName(String name) { case "monero": return Coin.monero; + case "Particl": + case "particl": + case "particlTestNet": + return Coin.particl; + case "Namecoin": case "namecoin": return Coin.namecoin; @@ -255,6 +282,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.monero; case "nmc": return Coin.namecoin; + case "part": + return Coin.particl; case "tbtc": return Coin.bitcoinTestNet; case "tbch": @@ -263,6 +292,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.firoTestNet; case "tdoge": return Coin.dogecoinTestNet; + case "tparticl": + return Coin.particlTestNet; case "wow": return Coin.wownero; default: diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 361d922dc..443297363 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -188,6 +188,7 @@ class CoinThemeColor { Color get monero => const Color(0xFFFF9E6B); Color get namecoin => const Color(0xFF91B1E1); Color get wownero => const Color(0xFFED80C1); + Color get particl => const Color(0xFFED80C1); //TODO - Use part colors Color forCoin(Coin coin) { switch (coin) { @@ -211,6 +212,9 @@ class CoinThemeColor { return namecoin; case Coin.wownero: return wownero; + case Coin.particl: + case Coin.particlTestNet: + return particl; } } } diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 0481268b5..33c5e306d 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -1423,6 +1423,9 @@ class StackColors extends ThemeExtension { return _coin.namecoin; case Coin.wownero: return _coin.wownero; + case Coin.particl: + case Coin.particlTestNet: + return _coin.particl; } } From b60e2783d656c2769008d5d6b5679519fd41f89b Mon Sep 17 00:00:00 2001 From: Likho Date: Tue, 25 Oct 2022 17:43:58 +0200 Subject: [PATCH 002/103] Fix particl node --- lib/utilities/default_nodes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 8b6a9dadc..b2aad8fef 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -216,7 +216,7 @@ abstract class DefaultNodes { return namecoin; case Coin.particl: - return namecoin; + return particl; case Coin.bitcoinTestNet: return bitcoinTestnet; From 9baa30c1a40b422bb5f4746efc1220b52691ace6 Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 26 Oct 2022 10:52:02 +0200 Subject: [PATCH 003/103] REmove testnet --- lib/pages/exchange_view/send_from_view.dart | 1 - .../add_edit_node_view.dart | 2 -- lib/services/coins/coin_service.dart | 9 ------- .../coins/particl/particl_wallet.dart | 24 +------------------ lib/utilities/address_utils.dart | 2 -- lib/utilities/assets.dart | 3 --- lib/utilities/block_explorers.dart | 2 -- lib/utilities/constants.dart | 2 -- lib/utilities/default_nodes.dart | 19 ++------------- lib/utilities/enums/coin_enum.dart | 12 ---------- lib/utilities/theme/color_theme.dart | 1 - lib/utilities/theme/stack_colors.dart | 1 - 12 files changed, 3 insertions(+), 75 deletions(-) diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 7dd841d65..271e2b349 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -61,7 +61,6 @@ class _SendFromViewState extends ConsumerState { case Coin.bitcoincashTestnet: case Coin.dogecoinTestNet: case Coin.firoTestNet: - case Coin.particlTestNet: return amount.toStringAsFixed(Constants.decimalPlaces); case Coin.monero: return amount.toStringAsFixed(Constants.decimalPlacesMonero); 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 ce422ddf9..df4921af9 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 @@ -123,7 +123,6 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: - case Coin.particlTestNet: final client = ElectrumX( host: formData.host!, port: formData.port!, @@ -540,7 +539,6 @@ class _NodeFormState extends ConsumerState { case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: - case Coin.particlTestNet: return false; case Coin.epicCash: diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 6d3cbd4e3..d2ac174f5 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -156,15 +156,6 @@ abstract class CoinServiceAPI { cachedClient: cachedClient, tracker: tracker); - case Coin.particlTestNet: - return ParticlWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - client: client, - cachedClient: cachedClient, - tracker: tracker); - case Coin.wownero: return WowneroWallet( walletId: walletId, diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 77d0fa7db..5aaedfd23 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -46,7 +46,7 @@ const int MINIMUM_CONFIRMATIONS = 1; const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = - "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"; + "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"; const String GENESIS_HASH_TESTNET = "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; @@ -89,9 +89,6 @@ bip32.BIP32 getBip32NodeFromRoot( case 0x6c: // PART mainnet wif coinType = "44"; // PART mainnet break; - case 0x2e: // PART testnet wif - coinType = "1"; // PART testnet - break; default: throw Exception("Invalid Particl network type used!"); } @@ -153,8 +150,6 @@ class ParticlWallet extends CoinServiceAPI { switch (coin) { case Coin.particl: return particl; - case Coin.particlTestNet: - return particltestnet; default: throw Exception("Invalid network type!"); } @@ -352,10 +347,6 @@ class ParticlWallet extends CoinServiceAPI { throw Exception("genesis hash does not match main net!"); } break; - case Coin.particlTestNet: - if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - throw Exception("genesis hash does not match test net!"); - } break; default: throw Exception( @@ -1474,11 +1465,6 @@ class ParticlWallet extends CoinServiceAPI { throw Exception("genesis hash does not match main net!"); } break; - case Coin.particlTestNet: - if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - throw Exception("genesis hash does not match test net!"); - } - break; default: throw Exception( "Attempted to generate a ParticlWallet using a non particl coin type: ${coin.name}"); @@ -3803,11 +3789,3 @@ final particl = NetworkType( pubKeyHash: 0x38, scriptHash: 0x3c, wif: 0x6c); - -final particltestnet = NetworkType( - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'tpw', - bip32: Bip32Type(public: 0xe1427800, private: 0x04889478), - pubKeyHash: 0x76, - scriptHash: 0x7a, - wif: 0x2e); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 91cedefd9..3e0664b3d 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -69,8 +69,6 @@ class AddressUtils { return Address.validateAddress(address, firoTestNetwork); case Coin.dogecoinTestNet: return Address.validateAddress(address, dogecointestnet); - case Coin.particlTestNet: - return Address.validateAddress(address, particltestnet); } } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index df26f4d8f..a7fb6c3e2 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -187,8 +187,6 @@ class _SVG { return firoTestnet; case Coin.dogecoinTestNet: return dogecoinTestnet; - case Coin.particlTestNet: - return particlTestnet; } } } @@ -233,7 +231,6 @@ class _PNG { case Coin.namecoin: return namecoin; case Coin.particl: - case Coin.particlTestNet: return particl; } } diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index 52d84b815..bc76b8173 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -33,7 +33,5 @@ Uri getBlockExplorerTransactionUrlFor({ return Uri.parse("https://chainz.cryptoid.info/nmc/tx.dws?$txid.htm"); case Coin.particl: return Uri.parse("https://chainz.cryptoid.info/part/tx.dws?$txid.htm"); - case Coin.particlTestNet: - return Uri.parse(""); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 3258aa74f..acaac157f 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -52,7 +52,6 @@ abstract class Constants { case Coin.epicCash: case Coin.namecoin: case Coin.particl: - case Coin.particlTestNet: values.addAll([24, 21, 18, 15, 12]); break; @@ -98,7 +97,6 @@ abstract class Constants { return 600; case Coin.particl: - case Coin.particlTestNet: return 600; } } diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index b2aad8fef..92f1e6f8c 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -119,8 +119,8 @@ abstract class DefaultNodes { ); static NodeModel get particl => NodeModel( - host: "host", - port: 123, + host: "164.92.93.20", + port: 50002, name: defaultName, id: _nodeId(Coin.particl), useSSL: true, @@ -177,18 +177,6 @@ abstract class DefaultNodes { isDown: false, ); - static NodeModel get particlTestnet => NodeModel( - host: "host", - port: 60002, - name: defaultName, - id: _nodeId(Coin.particlTestNet), - useSSL: true, - enabled: true, - coinName: Coin.particlTestNet.name, - isFailover: true, - isDown: false, - ); - static NodeModel getNodeFor(Coin coin) { switch (coin) { case Coin.bitcoin: @@ -229,9 +217,6 @@ abstract class DefaultNodes { case Coin.dogecoinTestNet: return dogecoinTestnet; - - case Coin.particlTestNet: - return particlTestnet; } } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 0454dc1f2..3f578c669 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -32,7 +32,6 @@ enum Coin { bitcoincashTestnet, dogecoinTestNet, firoTestNet, - particlTestNet } // remove firotestnet for now @@ -67,8 +66,6 @@ extension CoinExt on Coin { return "tFiro"; case Coin.dogecoinTestNet: return "tDogecoin"; - case Coin.particlTestNet: - return "tParticl"; } } @@ -100,8 +97,6 @@ extension CoinExt on Coin { return "tFIRO"; case Coin.dogecoinTestNet: return "tDOGE"; - case Coin.particlTestNet: - return "tPART"; } } @@ -134,8 +129,6 @@ extension CoinExt on Coin { return "firo"; case Coin.dogecoinTestNet: return "dogecoin"; - case Coin.particlTestNet: - return "particl"; } } @@ -151,7 +144,6 @@ extension CoinExt on Coin { case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: - case Coin.particlTestNet: return true; case Coin.epicCash: @@ -186,7 +178,6 @@ extension CoinExt on Coin { return xmr.MINIMUM_CONFIRMATIONS; case Coin.particl: - case Coin.particlTestNet: return particl.MINIMUM_CONFIRMATIONS; case Coin.wownero: @@ -227,7 +218,6 @@ Coin coinFromPrettyName(String name) { case "Particl": case "particl": - case "particlTestNet": return Coin.particl; case "Namecoin": @@ -292,8 +282,6 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.firoTestNet; case "tdoge": return Coin.dogecoinTestNet; - case "tparticl": - return Coin.particlTestNet; case "wow": return Coin.wownero; default: diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 443297363..4512dabfc 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -213,7 +213,6 @@ class CoinThemeColor { case Coin.wownero: return wownero; case Coin.particl: - case Coin.particlTestNet: return particl; } } diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 33c5e306d..2a1ee7ce9 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -1424,7 +1424,6 @@ class StackColors extends ThemeExtension { case Coin.wownero: return _coin.wownero; case Coin.particl: - case Coin.particlTestNet: return _coin.particl; } } From b0cee75b76a9ce493ea4c3d9d9e4f7d5ac9344bc Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 26 Oct 2022 14:42:07 +0200 Subject: [PATCH 004/103] Fix address error, remove bip84 --- .../coins/particl/particl_wallet.dart | 446 ++---------------- 1 file changed, 50 insertions(+), 396 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 5aaedfd23..b939828ab 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -50,7 +50,7 @@ const String GENESIS_HASH_MAINNET = const String GENESIS_HASH_TESTNET = "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; -enum DerivePathType { bip44, bip49, bip84 } +enum DerivePathType { bip44, bip49 } bip32.BIP32 getBip32Node( int chain, @@ -97,8 +97,6 @@ bip32.BIP32 getBip32NodeFromRoot( 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."); } @@ -226,11 +224,7 @@ class ParticlWallet extends CoinServiceAPI { } @override - Future get currentReceivingAddress => _currentReceivingAddress ??= - _getCurrentAddressForChain(0, DerivePathType.bip84); - Future? _currentReceivingAddress; - - Future get currentLegacyReceivingAddress => + Future get currentReceivingAddress => _currentReceivingAddressP2PKH ??= _getCurrentAddressForChain(0, DerivePathType.bip44); Future? _currentReceivingAddressP2PKH; @@ -319,8 +313,7 @@ class ParticlWallet extends CoinServiceAPI { if (decodeBech32.version != 0) { throw ArgumentError('Invalid address version'); } - // P2WPKH - return DerivePathType.bip84; + throw ArgumentError('$address has no matching Script'); } } @@ -443,13 +436,6 @@ class ParticlWallet extends CoinServiceAPI { .data .address!; break; - case DerivePathType.bip84: - address = P2WPKH( - network: _network, - data: PaymentData(pubkey: node.publicKey)) - .data - .address!; - break; default: throw Exception("No Path type $type exists"); } @@ -531,28 +517,24 @@ class ParticlWallet extends CoinServiceAPI { 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 + // actual size is 24 due to p2pkh, p2sh so 12x2 const txCountBatchSize = 12; try { @@ -565,9 +547,6 @@ class ParticlWallet extends CoinServiceAPI { 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 @@ -577,16 +556,11 @@ class ParticlWallet extends CoinServiceAPI { 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 = @@ -601,12 +575,6 @@ class ParticlWallet extends CoinServiceAPI { 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; @@ -619,12 +587,6 @@ class ParticlWallet extends CoinServiceAPI { 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( @@ -638,12 +600,7 @@ class ParticlWallet extends CoinServiceAPI { derivePathType: DerivePathType.bip49, derivationsToAdd: p2shReceiveDerivations); } - if (p2wpkhReceiveDerivations.isNotEmpty) { - await addDerivations( - chain: 0, - derivePathType: DerivePathType.bip84, - derivationsToAdd: p2wpkhReceiveDerivations); - } + if (p2pkhChangeDerivations.isNotEmpty) { await addDerivations( chain: 1, @@ -656,12 +613,6 @@ class ParticlWallet extends CoinServiceAPI { 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 @@ -677,12 +628,6 @@ class ParticlWallet extends CoinServiceAPI { 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. @@ -698,21 +643,7 @@ class ParticlWallet extends CoinServiceAPI { 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', @@ -729,14 +660,6 @@ class ParticlWallet extends CoinServiceAPI { 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( @@ -966,7 +889,7 @@ class ParticlWallet extends CoinServiceAPI { GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); final changeAddressForTransactions = - _checkChangeAddressForTransactions(DerivePathType.bip84); + _checkChangeAddressForTransactions(DerivePathType.bip44); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); final currentReceivingAddressesForTransactions = @@ -1378,10 +1301,6 @@ class ParticlWallet extends CoinServiceAPI { 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 = @@ -1393,16 +1312,6 @@ class ParticlWallet extends CoinServiceAPI { 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); @@ -1481,10 +1390,6 @@ class ParticlWallet extends CoinServiceAPI { 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 @@ -1506,23 +1411,6 @@ class ParticlWallet extends CoinServiceAPI { // 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) { @@ -1558,42 +1446,10 @@ class ParticlWallet extends CoinServiceAPI { ), ]); - // // 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. + /// Generates a new internal or external chain address for the wallet using a 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( @@ -1627,9 +1483,6 @@ class ParticlWallet extends CoinServiceAPI { .data .address!; break; - case DerivePathType.bip84: - address = P2WPKH(network: _network, data: data).data.address!; - break; } // add generated address & info to derivations @@ -1657,9 +1510,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: indexKey += "P2SH"; break; - case DerivePathType.bip84: - indexKey += "P2WPKH"; - break; } final newIndex = @@ -1686,9 +1536,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: chainArray += "P2SH"; break; - case DerivePathType.bip84: - chainArray += "P2WPKH"; - break; } final addressArray = @@ -1726,9 +1573,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: arrayKey += "P2SH"; break; - case DerivePathType.bip84: - arrayKey += "P2WPKH"; - break; } final internalChainArray = DB.instance.get(boxName: walletId, key: arrayKey); @@ -1748,9 +1592,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: key = "${walletId}_${chainId}DerivationsP2SH"; break; - case DerivePathType.bip84: - key = "${walletId}_${chainId}DerivationsP2WPKH"; - break; } return key; } @@ -2065,9 +1906,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: indexKey += "P2SH"; break; - case DerivePathType.bip84: - indexKey += "P2WPKH"; - break; } final newReceivingIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; @@ -2089,9 +1927,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: _currentReceivingAddressP2SH = Future(() => newReceivingAddress); break; - case DerivePathType.bip84: - _currentReceivingAddress = Future(() => newReceivingAddress); - break; } } } catch (e, s) { @@ -2125,9 +1960,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: indexKey += "P2SH"; break; - case DerivePathType.bip84: - indexKey += "P2WPKH"; - break; } final newChangeIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; @@ -2314,22 +2146,18 @@ class ParticlWallet extends CoinServiceAPI { } Future _fetchTransactionData() async { - final List allAddresses = await _fetchAllOwnAddresses(); + List allAddressesOld = await _fetchAllOwnAddresses(); + List allAddresses = []; + for (String address in allAddressesOld) { + allAddresses.add(address); + } - final changeAddresses = DB.instance.get( - boxName: walletId, key: 'changeAddressesP2WPKH') as List; - final changeAddressesP2PKH = + var changeAddressesP2PKHOld = 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); + List changeAddressesP2PKH = []; + for (var address in changeAddressesP2PKHOld) { + changeAddressesP2PKH.add(address); } final List> allTxHashes = @@ -2348,23 +2176,27 @@ class ParticlWallet extends CoinServiceAPI { 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); + print(cachedTransactions.findTransaction(tx["tx_hash"] as String)); + print(unconfirmedCachedTransactions[tx["tx_hash"] as String]); + final cachedTx = + cachedTransactions.findTransaction(tx["tx_hash"] as String); + if (!(cachedTx != null && + addressType(address: cachedTx.address) == + DerivePathType.bip44)) { + 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) { @@ -2394,16 +2226,6 @@ class ParticlWallet extends CoinServiceAPI { 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 = []; @@ -2417,18 +2239,16 @@ class ParticlWallet extends CoinServiceAPI { Map midSortedTx = {}; for (int i = 0; i < (txObject["vin"] as List).length; i++) { - final input = txObject["vin"]![i] as Map; + 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, - ); + txHash: prevTxid, coin: coin); for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { - final address = out["scriptPubKey"]["address"] as String?; + final address = out["scriptPubKey"]["addresses"][0] as String?; if (address != null) { sendersArray.add(address); } @@ -2439,7 +2259,7 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance.log("sendersArray: $sendersArray", level: LogLevel.Info); for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["address"] as String?; + final address = output["scriptPubKey"]["addresses"][0] as String?; if (address != null) { recipientsArray.add(address); } @@ -2449,7 +2269,8 @@ class ParticlWallet extends CoinServiceAPI { .log("recipientsArray: $recipientsArray", level: LogLevel.Info); final foundInSenders = - allAddresses.any((element) => sendersArray.contains(element)); + allAddresses.any((element) => sendersArray.contains(element)) || + allAddressesOld.any((element) => sendersArray.contains(element)); Logging.instance .log("foundInSenders: $foundInSenders", level: LogLevel.Info); @@ -2457,7 +2278,7 @@ class ParticlWallet extends CoinServiceAPI { if (foundInSenders) { int totalInput = 0; for (int i = 0; i < (txObject["vin"] as List).length; i++) { - final input = txObject["vin"]![i] as Map; + final input = txObject["vin"][i] as Map; final prevTxid = input["txid"] as String; final prevOut = input["vout"] as int; final tx = await _cachedElectrumXClient.getTransaction( @@ -2468,7 +2289,7 @@ class ParticlWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { inputAmtSentFromWallet += - (Decimal.parse(out["value"]!.toString()) * + (Decimal.parse(out["value"].toString()) * Decimal.fromInt(Constants.satsPerCoin)) .toBigInt() .toInt(); @@ -2479,14 +2300,14 @@ class ParticlWallet extends CoinServiceAPI { int totalOutput = 0; for (final output in txObject["vout"] as List) { - final String address = output["scriptPubKey"]!["address"] as String; - final value = output["value"]!; + 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)) { + if (changeAddressesP2PKH.contains(address)) { inputAmtSentFromWallet -= _value; } else { // change address from 'sent from' to the 'sent to' address @@ -2504,14 +2325,15 @@ class ParticlWallet extends CoinServiceAPI { // add up received tx value for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["address"]; + 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)) { + if (allAddresses.contains(address) || + allAddressesOld.contains(address)) { outputAmtAddressedToWallet += value; } } @@ -2789,7 +2611,7 @@ class ParticlWallet extends CoinServiceAPI { utxoSigningData: utxoSigningData, recipients: [ _recipientAddress, - await _getCurrentAddressForChain(1, DerivePathType.bip84), + await _getCurrentAddressForChain(1, DerivePathType.bip44), ], satoshiAmounts: [ satoshiAmountToSend, @@ -2827,9 +2649,9 @@ class ParticlWallet extends CoinServiceAPI { satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used - await _checkChangeAddressForTransactions(DerivePathType.bip84); + await _checkChangeAddressForTransactions(DerivePathType.bip44); final String newChangeAddress = - await _getCurrentAddressForChain(1, DerivePathType.bip84); + await _getCurrentAddressForChain(1, DerivePathType.bip44); int feeBeingPaid = satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; @@ -3006,7 +2828,6 @@ class ParticlWallet extends CoinServiceAPI { // addresses to check List addressesP2PKH = []; List addressesP2SH = []; - List addressesP2WPKH = []; try { // Populating the addresses to check @@ -3032,9 +2853,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: addressesP2SH.add(address); break; - case DerivePathType.bip84: - addressesP2WPKH.add(address); - break; } } } @@ -3169,65 +2987,6 @@ class ParticlWallet extends CoinServiceAPI { } } - // 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, - ).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, - ).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 @@ -3411,40 +3170,6 @@ class ParticlWallet extends CoinServiceAPI { 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"); @@ -3478,24 +3203,6 @@ class ParticlWallet extends CoinServiceAPI { 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'); @@ -3585,43 +3292,6 @@ class ParticlWallet extends CoinServiceAPI { 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"); @@ -3654,22 +3324,6 @@ class ParticlWallet extends CoinServiceAPI { 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'); @@ -3754,21 +3408,21 @@ class ParticlWallet extends CoinServiceAPI { Future generateNewAddress() async { try { await _incrementAddressIndexForChain( - 0, DerivePathType.bip84); // First increment the receiving index + 0, DerivePathType.bip44); // First increment the receiving index final newReceivingIndex = DB.instance.get( boxName: walletId, - key: 'receivingIndexP2WPKH') as int; // Check the new receiving index + key: 'receivingIndexP2PKH') as int; // Check the new receiving index final newReceivingAddress = await _generateAddressForChain( 0, newReceivingIndex, DerivePathType - .bip84); // Use new index to derive a new receiving address + .bip44); // 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(() => + .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; From 401d4b568ca6c1abbbe3c3c8abb602bf4b408c81 Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 26 Oct 2022 19:55:32 +0200 Subject: [PATCH 005/103] WIP: add particl --- .../coins/particl/particl_wallet.dart | 22 ++++++++++++++----- lib/services/price.dart | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index b939828ab..51c1a2c9b 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -46,7 +46,7 @@ const int MINIMUM_CONFIRMATIONS = 1; const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = - "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"; + "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"; const String GENESIS_HASH_TESTNET = "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; @@ -883,6 +883,8 @@ class ParticlWallet extends CoinServiceAPI { if (currentHeight != storedHeight) { if (currentHeight != -1) { + Logging.instance + .log("Can update chain: $currentHeight", level: LogLevel.Info); // -1 failed to fetch current height unawaited(updateStoredChainHeight(newHeight: currentHeight)); } @@ -2221,6 +2223,9 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance.log("allTransactions length: ${allTransactions.length}", level: LogLevel.Info); + Logging.instance + .log("allTransactions is: $allTransactions", level: LogLevel.Info); + final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; @@ -2246,9 +2251,12 @@ class ParticlWallet extends CoinServiceAPI { final tx = await _cachedElectrumXClient.getTransaction( txHash: prevTxid, coin: coin); + Logging.instance + .log("RECEIVED TX IS : ${tx["vout"]}", level: LogLevel.Info); + for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { - final address = out["scriptPubKey"]["addresses"][0] as String?; + final address = out["scriptPubKey"]["address"] as String?; if (address != null) { sendersArray.add(address); } @@ -2259,7 +2267,7 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance.log("sendersArray: $sendersArray", level: LogLevel.Info); for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["addresses"][0] as String?; + final address = output["scriptPubKey"]["address"] as String?; if (address != null) { recipientsArray.add(address); } @@ -2300,8 +2308,9 @@ class ParticlWallet extends CoinServiceAPI { int totalOutput = 0; for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["addresses"][0]; + final address = output["scriptPubKey"]["address"]; final value = output["value"]; + final _value = (Decimal.parse(value.toString()) * Decimal.fromInt(Constants.satsPerCoin)) .toBigInt() @@ -2325,7 +2334,8 @@ class ParticlWallet extends CoinServiceAPI { // add up received tx value for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["addresses"][0]; + final address = output["scriptPubKey"]["address"]; + if (address != null) { final value = (Decimal.parse(output["value"].toString()) * Decimal.fromInt(Constants.satsPerCoin)) @@ -3006,7 +3016,7 @@ class ParticlWallet extends CoinServiceAPI { .log("Starting buildTransaction ----------", level: LogLevel.Info); final txb = TransactionBuilder(network: _network); - txb.setVersion(1); + txb.setVersion(160); // Add transaction inputs for (var i = 0; i < utxosToUse.length; i++) { diff --git a/lib/services/price.dart b/lib/services/price.dart index b44e055d5..da0c9e68d 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -87,7 +87,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,bitcoin-cash,namecoin,wownero&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,wownero,particl&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"); From 4cac7645c8819092ec827450eff17c81f7e5e87c Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 1 Nov 2022 12:46:29 +0200 Subject: [PATCH 006/103] WIP: Fix send --- .../coins/particl/particl_wallet.dart | 57 +++++++++---------- lib/utilities/default_nodes.dart | 6 +- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 51c1a2c9b..dc92b9da6 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -430,7 +430,8 @@ class ParticlWallet extends CoinServiceAPI { data: PaymentData( redeem: P2WPKH( data: PaymentData(pubkey: node.publicKey), - network: _network) + network: _network, + overridePrefix: particl.bech32!) .data), network: _network) .data @@ -1200,7 +1201,7 @@ class ParticlWallet extends CoinServiceAPI { @override bool validateAddress(String address) { - return Address.validateAddress(address, _network); + return Address.validateAddress(address, _network, particl.bech32!); } @override @@ -1480,7 +1481,11 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: address = P2SH( data: PaymentData( - redeem: P2WPKH(data: data, network: _network).data), + redeem: P2WPKH( + data: data, + network: _network, + overridePrefix: particl.bech32!) + .data), network: _network) .data .address!; @@ -2223,9 +2228,6 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance.log("allTransactions length: ${allTransactions.length}", level: LogLevel.Info); - Logging.instance - .log("allTransactions is: $allTransactions", level: LogLevel.Info); - final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; @@ -2516,14 +2518,6 @@ class ParticlWallet extends CoinServiceAPI { 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) { @@ -2558,13 +2552,6 @@ class ParticlWallet extends CoinServiceAPI { 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]; @@ -2851,7 +2838,9 @@ class ParticlWallet extends CoinServiceAPI { for (final output in tx["vout"] as List) { final n = output["n"]; if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["address"] as String; + Logging.instance.log("THIS OUTPUT IS ${output["scriptPubKey"]}", + level: LogLevel.Info, printFullLength: true); + final address = output["scriptPubKey"]["addresses"][0] as String; if (!addressTxid.containsKey(address)) { addressTxid[address] = []; } @@ -2870,6 +2859,9 @@ class ParticlWallet extends CoinServiceAPI { // p2pkh / bip44 final p2pkhLength = addressesP2PKH.length; + Logging.instance + .log("THE BIP44 LENGTH IS $p2pkhLength", level: LogLevel.Info); + if (p2pkhLength > 0) { final receiveDerivations = await _fetchDerivations( chain: 0, @@ -2928,6 +2920,8 @@ class ParticlWallet extends CoinServiceAPI { // p2sh / bip49 final p2shLength = addressesP2SH.length; + Logging.instance + .log("THE BIP49 LENGTH IS $p2pkhLength", level: LogLevel.Info); if (p2shLength > 0) { final receiveDerivations = await _fetchDerivations( chain: 0, @@ -2946,7 +2940,8 @@ class ParticlWallet extends CoinServiceAPI { data: PaymentData( pubkey: Format.stringToUint8List( receiveDerivation["pubKey"] as String)), - network: _network) + network: _network, + overridePrefix: particl.bech32!) .data; final redeemScript = p2wpkh.output; @@ -3027,19 +3022,20 @@ class ParticlWallet extends CoinServiceAPI { // Add transaction output for (var i = 0; i < recipients.length; i++) { - txb.addOutput(recipients[i], satoshiAmounts[i]); + txb.addOutput(recipients[i], satoshiAmounts[i], particl.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?, - ); + vin: i, + keyPair: utxoSigningData[txid]["keyPair"] as ECPair, + witnessValue: utxosToUse[i].value, + redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + overridePrefix: particl.bech32!); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", @@ -3047,7 +3043,8 @@ class ParticlWallet extends CoinServiceAPI { rethrow; } - final builtTx = txb.build(); + final builtTx = txb.build(particl.bech32!); + print("BUILT TX IS $builtTx"); final vSize = builtTx.virtualSize(); return {"hex": builtTx.toHex(), "vSize": vSize}; diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 92f1e6f8c..e6cf4fc3e 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -119,15 +119,15 @@ abstract class DefaultNodes { ); static NodeModel get particl => NodeModel( - host: "164.92.93.20", - port: 50002, + host: "particl.stackwallet.com", + port: 58002, name: defaultName, id: _nodeId(Coin.particl), useSSL: true, enabled: true, coinName: Coin.particl.name, isFailover: true, - isDown: false); //TODO - UPDATE WITH CORRECT DETAILS + isDown: false); static NodeModel get bitcoinTestnet => NodeModel( host: "electrumx-testnet.cypherstack.com", From 8f157ccfc4dcc5e4ed3b1313b70e61b40746aa93 Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Sat, 26 Nov 2022 13:12:38 -0700 Subject: [PATCH 007/103] Bump version. Mm! --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2397fbfbc..ef1b495a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.19+91 +version: 1.5.20+92 environment: sdk: ">=2.17.0 <3.0.0" From d0b2a5a3fed1a41382ce109c85d9e55b45c37e01 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 28 Nov 2022 08:59:23 -0800 Subject: [PATCH 008/103] Add tomli python lib to build script comment. --- scripts/linux/build_secure_storage_deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/linux/build_secure_storage_deps.sh b/scripts/linux/build_secure_storage_deps.sh index 7a725d65c..69b452e2d 100755 --- a/scripts/linux/build_secure_storage_deps.sh +++ b/scripts/linux/build_secure_storage_deps.sh @@ -20,7 +20,7 @@ cd "$LINUX_DIRECTORY" || exit # Build libSecret # sudo apt install meson libgirepository1.0-dev valac xsltproc gi-docgen docbook-xsl # sudo apt install python3-pip -#pip3 install --user meson markdown --upgrade +#pip3 install --user meson markdown tomli --upgrade # pip3 install --user gi-docgen cd build || exit git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret From df4592214930849b033e921fb8e6bf275e3ce475 Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Mon, 28 Nov 2022 17:02:26 -0700 Subject: [PATCH 009/103] Bump version. 1.5.21, build 93. --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ef1b495a5..8cfce39cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.20+92 +version: 1.5.21+93 environment: sdk: ">=2.17.0 <3.0.0" From de0e2cb02115ac4a657b885c8669741926365b1d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 11:06:01 -0600 Subject: [PATCH 010/103] Revert "Fix address error, remove bip84" This reverts commit b0cee75b76a9ce493ea4c3d9d9e4f7d5ac9344bc. --- .../coins/particl/particl_wallet.dart | 442 ++++++++++++++++-- 1 file changed, 393 insertions(+), 49 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index dc92b9da6..1ad195a04 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -50,7 +50,7 @@ const String GENESIS_HASH_MAINNET = const String GENESIS_HASH_TESTNET = "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; -enum DerivePathType { bip44, bip49 } +enum DerivePathType { bip44, bip49, bip84 } bip32.BIP32 getBip32Node( int chain, @@ -97,6 +97,8 @@ bip32.BIP32 getBip32NodeFromRoot( 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."); } @@ -224,7 +226,11 @@ class ParticlWallet extends CoinServiceAPI { } @override - Future get currentReceivingAddress => + Future get currentReceivingAddress => _currentReceivingAddress ??= + _getCurrentAddressForChain(0, DerivePathType.bip84); + Future? _currentReceivingAddress; + + Future get currentLegacyReceivingAddress => _currentReceivingAddressP2PKH ??= _getCurrentAddressForChain(0, DerivePathType.bip44); Future? _currentReceivingAddressP2PKH; @@ -313,7 +319,8 @@ class ParticlWallet extends CoinServiceAPI { if (decodeBech32.version != 0) { throw ArgumentError('Invalid address version'); } - throw ArgumentError('$address has no matching Script'); + // P2WPKH + return DerivePathType.bip84; } } @@ -437,6 +444,13 @@ class ParticlWallet extends CoinServiceAPI { .data .address!; break; + case DerivePathType.bip84: + address = P2WPKH( + network: _network, + data: PaymentData(pubkey: node.publicKey)) + .data + .address!; + break; default: throw Exception("No Path type $type exists"); } @@ -518,24 +532,28 @@ class ParticlWallet extends CoinServiceAPI { 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 24 due to p2pkh, p2sh so 12x2 + // actual size is 36 due to p2pkh, p2sh, and p2wpkh so 12x3 const txCountBatchSize = 12; try { @@ -548,6 +566,9 @@ class ParticlWallet extends CoinServiceAPI { 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 @@ -557,11 +578,16 @@ class ParticlWallet extends CoinServiceAPI { 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 = @@ -576,6 +602,12 @@ class ParticlWallet extends CoinServiceAPI { 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; @@ -588,6 +620,12 @@ class ParticlWallet extends CoinServiceAPI { 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( @@ -601,7 +639,12 @@ class ParticlWallet extends CoinServiceAPI { derivePathType: DerivePathType.bip49, derivationsToAdd: p2shReceiveDerivations); } - + if (p2wpkhReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip84, + derivationsToAdd: p2wpkhReceiveDerivations); + } if (p2pkhChangeDerivations.isNotEmpty) { await addDerivations( chain: 1, @@ -614,6 +657,12 @@ class ParticlWallet extends CoinServiceAPI { 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 @@ -629,6 +678,12 @@ class ParticlWallet extends CoinServiceAPI { 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. @@ -644,7 +699,21 @@ class ParticlWallet extends CoinServiceAPI { 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', @@ -661,6 +730,14 @@ class ParticlWallet extends CoinServiceAPI { 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( @@ -892,7 +969,7 @@ class ParticlWallet extends CoinServiceAPI { GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); final changeAddressForTransactions = - _checkChangeAddressForTransactions(DerivePathType.bip44); + _checkChangeAddressForTransactions(DerivePathType.bip84); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); final currentReceivingAddressesForTransactions = @@ -1304,6 +1381,10 @@ class ParticlWallet extends CoinServiceAPI { 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 = @@ -1315,6 +1396,16 @@ class ParticlWallet extends CoinServiceAPI { 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); @@ -1393,6 +1484,10 @@ class ParticlWallet extends CoinServiceAPI { 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 @@ -1414,6 +1509,23 @@ class ParticlWallet extends CoinServiceAPI { // 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) { @@ -1449,10 +1561,42 @@ class ParticlWallet extends CoinServiceAPI { ), ]); + // // 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 BIP44, or BIP49 derivation path. + /// 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( @@ -1490,6 +1634,9 @@ class ParticlWallet extends CoinServiceAPI { .data .address!; break; + case DerivePathType.bip84: + address = P2WPKH(network: _network, data: data).data.address!; + break; } // add generated address & info to derivations @@ -1517,6 +1664,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: indexKey += "P2SH"; break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; } final newIndex = @@ -1543,6 +1693,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: chainArray += "P2SH"; break; + case DerivePathType.bip84: + chainArray += "P2WPKH"; + break; } final addressArray = @@ -1580,6 +1733,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: arrayKey += "P2SH"; break; + case DerivePathType.bip84: + arrayKey += "P2WPKH"; + break; } final internalChainArray = DB.instance.get(boxName: walletId, key: arrayKey); @@ -1599,6 +1755,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: key = "${walletId}_${chainId}DerivationsP2SH"; break; + case DerivePathType.bip84: + key = "${walletId}_${chainId}DerivationsP2WPKH"; + break; } return key; } @@ -1913,6 +2072,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: indexKey += "P2SH"; break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; } final newReceivingIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; @@ -1934,6 +2096,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: _currentReceivingAddressP2SH = Future(() => newReceivingAddress); break; + case DerivePathType.bip84: + _currentReceivingAddress = Future(() => newReceivingAddress); + break; } } } catch (e, s) { @@ -1967,6 +2132,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: indexKey += "P2SH"; break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; } final newChangeIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; @@ -2153,18 +2321,22 @@ class ParticlWallet extends CoinServiceAPI { } Future _fetchTransactionData() async { - List allAddressesOld = await _fetchAllOwnAddresses(); - List allAddresses = []; - for (String address in allAddressesOld) { - allAddresses.add(address); - } + final List allAddresses = await _fetchAllOwnAddresses(); - var changeAddressesP2PKHOld = + final changeAddresses = DB.instance.get( + boxName: walletId, key: 'changeAddressesP2WPKH') as List; + final changeAddressesP2PKH = DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') as List; - List changeAddressesP2PKH = []; - for (var address in changeAddressesP2PKHOld) { - changeAddressesP2PKH.add(address); + 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 = @@ -2183,27 +2355,23 @@ class ParticlWallet extends CoinServiceAPI { 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) { - print(cachedTransactions.findTransaction(tx["tx_hash"] as String)); - print(unconfirmedCachedTransactions[tx["tx_hash"] as String]); - final cachedTx = - cachedTransactions.findTransaction(tx["tx_hash"] as String); - if (!(cachedTx != null && - addressType(address: cachedTx.address) == - DerivePathType.bip44)) { - allTxHashes.remove(tx); - } + 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) { @@ -2233,6 +2401,16 @@ class ParticlWallet extends CoinServiceAPI { 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 = []; @@ -2246,12 +2424,14 @@ class ParticlWallet extends CoinServiceAPI { Map midSortedTx = {}; for (int i = 0; i < (txObject["vin"] as List).length; i++) { - final input = txObject["vin"][i] as Map; + 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); + txHash: prevTxid, + coin: coin, + ); Logging.instance .log("RECEIVED TX IS : ${tx["vout"]}", level: LogLevel.Info); @@ -2279,8 +2459,7 @@ class ParticlWallet extends CoinServiceAPI { .log("recipientsArray: $recipientsArray", level: LogLevel.Info); final foundInSenders = - allAddresses.any((element) => sendersArray.contains(element)) || - allAddressesOld.any((element) => sendersArray.contains(element)); + allAddresses.any((element) => sendersArray.contains(element)); Logging.instance .log("foundInSenders: $foundInSenders", level: LogLevel.Info); @@ -2288,7 +2467,7 @@ class ParticlWallet extends CoinServiceAPI { if (foundInSenders) { int totalInput = 0; for (int i = 0; i < (txObject["vin"] as List).length; i++) { - final input = txObject["vin"][i] as Map; + final input = txObject["vin"]![i] as Map; final prevTxid = input["txid"] as String; final prevOut = input["vout"] as int; final tx = await _cachedElectrumXClient.getTransaction( @@ -2299,7 +2478,7 @@ class ParticlWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { inputAmtSentFromWallet += - (Decimal.parse(out["value"].toString()) * + (Decimal.parse(out["value"]!.toString()) * Decimal.fromInt(Constants.satsPerCoin)) .toBigInt() .toInt(); @@ -2310,15 +2489,14 @@ class ParticlWallet extends CoinServiceAPI { int totalOutput = 0; for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["address"]; - final value = output["value"]; - + final String address = output["scriptPubKey"]!["address"] as String; + final value = output["value"]!; final _value = (Decimal.parse(value.toString()) * Decimal.fromInt(Constants.satsPerCoin)) .toBigInt() .toInt(); totalOutput += _value; - if (changeAddressesP2PKH.contains(address)) { + if (changeAddresses.contains(address)) { inputAmtSentFromWallet -= _value; } else { // change address from 'sent from' to the 'sent to' address @@ -2337,15 +2515,13 @@ class ParticlWallet extends CoinServiceAPI { // add up received tx value for (final output in txObject["vout"] as List) { final address = output["scriptPubKey"]["address"]; - if (address != null) { final value = (Decimal.parse(output["value"].toString()) * Decimal.fromInt(Constants.satsPerCoin)) .toBigInt() .toInt(); totalOut += value; - if (allAddresses.contains(address) || - allAddressesOld.contains(address)) { + if (allAddresses.contains(address)) { outputAmtAddressedToWallet += value; } } @@ -2608,7 +2784,7 @@ class ParticlWallet extends CoinServiceAPI { utxoSigningData: utxoSigningData, recipients: [ _recipientAddress, - await _getCurrentAddressForChain(1, DerivePathType.bip44), + await _getCurrentAddressForChain(1, DerivePathType.bip84), ], satoshiAmounts: [ satoshiAmountToSend, @@ -2646,9 +2822,9 @@ class ParticlWallet extends CoinServiceAPI { satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used - await _checkChangeAddressForTransactions(DerivePathType.bip44); + await _checkChangeAddressForTransactions(DerivePathType.bip84); final String newChangeAddress = - await _getCurrentAddressForChain(1, DerivePathType.bip44); + await _getCurrentAddressForChain(1, DerivePathType.bip84); int feeBeingPaid = satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; @@ -2825,6 +3001,7 @@ class ParticlWallet extends CoinServiceAPI { // addresses to check List addressesP2PKH = []; List addressesP2SH = []; + List addressesP2WPKH = []; try { // Populating the addresses to check @@ -2852,6 +3029,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: addressesP2SH.add(address); break; + case DerivePathType.bip84: + addressesP2WPKH.add(address); + break; } } } @@ -2992,6 +3172,65 @@ class ParticlWallet extends CoinServiceAPI { } } + // 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, + ).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, + ).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 @@ -3177,6 +3416,40 @@ class ParticlWallet extends CoinServiceAPI { 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"); @@ -3210,6 +3483,24 @@ class ParticlWallet extends CoinServiceAPI { 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'); @@ -3299,6 +3590,43 @@ class ParticlWallet extends CoinServiceAPI { 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"); @@ -3331,6 +3659,22 @@ class ParticlWallet extends CoinServiceAPI { 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'); @@ -3415,21 +3759,21 @@ class ParticlWallet extends CoinServiceAPI { Future generateNewAddress() async { try { await _incrementAddressIndexForChain( - 0, DerivePathType.bip44); // First increment the receiving index + 0, DerivePathType.bip84); // First increment the receiving index final newReceivingIndex = DB.instance.get( boxName: walletId, - key: 'receivingIndexP2PKH') as int; // Check the new receiving index + key: 'receivingIndexP2WPKH') as int; // Check the new receiving index final newReceivingAddress = await _generateAddressForChain( 0, newReceivingIndex, DerivePathType - .bip44); // Use new index to derive a new receiving address + .bip84); // 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(() => + .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; From c8b87b9ea64116438b2f2a490ea09c57edbe5a8b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 11:25:39 -0600 Subject: [PATCH 011/103] WIP particl tests see out of band particl_wallet_test_parameters.dart --- .gitignore | 6 +- .../particl/particl_history_sample_data.dart | 188 ++ .../particl_transaction_data_samples.dart | 357 ++++ .../particl/particl_utxo_sample_data.dart | 60 + .../coins/particl/particl_wallet_test.dart | 1743 +++++++++++++++++ .../particl/particl_wallet_test.mocks.dart | 629 ++++++ 6 files changed, 2980 insertions(+), 3 deletions(-) create mode 100644 test/services/coins/particl/particl_history_sample_data.dart create mode 100644 test/services/coins/particl/particl_transaction_data_samples.dart create mode 100644 test/services/coins/particl/particl_utxo_sample_data.dart create mode 100644 test/services/coins/particl/particl_wallet_test.dart create mode 100644 test/services/coins/particl/particl_wallet_test.mocks.dart diff --git a/.gitignore b/.gitignore index 323aac218..d456eb159 100644 --- a/.gitignore +++ b/.gitignore @@ -37,8 +37,10 @@ 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/namecoin/namecoin_wallet_test_parameters.dart # Legacy +test/services/coins/namecoin/namecoin_wallet_test_parameters.txt # Legacy test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart +test/services/coins/particl/particl_wallet_test_parameters.dart /integration_test/private.dart # Exceptions to above rules. @@ -48,5 +50,3 @@ test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart coverage scripts/**/build /lib/external_api_keys.dart -/test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart -/test/services/coins/namecoin/namecoin_wallet_test_parameters.dart.txt diff --git a/test/services/coins/particl/particl_history_sample_data.dart b/test/services/coins/particl/particl_history_sample_data.dart new file mode 100644 index 000000000..53a082778 --- /dev/null +++ b/test/services/coins/particl/particl_history_sample_data.dart @@ -0,0 +1,188 @@ +// TODO these test vectors are valid for Namecoin: update for Particl + +final Map> historyBatchArgs0 = { + "k_0_0": ["d17132f41b2d55c730db5b27db721020abbd4a5087c15edcccbaa106eef8cbf3"], + "k_0_1": ["cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9"], + "k_0_2": ["82a12031d679c9dd3124047742dc22c2c7c03afa9644bddf55d4c95da41bca1c"], + "k_0_3": ["bbe10c5d3c102fd805770ed2d6c5438dce42c04d3f87e3260056d04245b17ddd"], + "k_0_4": ["d9ca5255516f963d8f348911451e2c69489a70dec7f34a4810ee8b0e32fcb04d"], + "k_0_5": ["2284461fd01b17e7443775e39b19f4378a063ff148938d2e4191cea3fd80368d"], + "k_0_6": ["cd3c32fddbf265410c34a58fefcc849b02fc16978d75e501f88f9effcbecd8fe"], + "k_0_7": ["a3bcc0c3c4a140fbcc4c4f4dff18790d8a2d5f868821f47460f68f0426291b57"], + "k_0_8": ["e400f9431798c87ea35ea19b265d9e56a73fd44c239957d9947ae79e16718fb4"], + "k_0_9": ["1fe8bb16b49725bf3703274e205a4695c398e664284cc68d92d15087a54da299"], + "k_0_10": [ + "2fabf8d61308c8b2d914489a9f02f669ed9fa68047666815cf1f3cd1bb5d8819" + ], + "k_0_11": ["42a567d344189430afe7d45d6854ef6e9d256d9ef4186afd31a1a5ff90a6a0dd"] +}; +final Map> historyBatchArgs1 = { + "k_0_0": ["bcf7aec7c10dfba33ce80149300a7c4fe66460c1dd05503b5df5780884498186"], + "k_0_1": ["587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e"], + "k_0_2": ["fe6ad514f7427782f964b25995a90a3233589904b88f66a2d0e73e2560c9af7c"], + "k_0_3": ["6b962c5f9b4cfc004c74c5ab849304c405b02fc0e2f34ee17c185984f13c9da4"], + "k_0_4": ["720b79fab9a163ce6534828e8a673c5bf600161eba92c2b81555e79add59994c"], + "k_0_5": ["a10f4cf239abd4bcdb03dbe40b5c1d57ae3a7982adf8f177d481feb0ad3a52cd"], + "k_0_6": ["061f28e17ba1a56404b08a5899163011c7d6317e534ccd8e4d38911574f574b0"], + "k_0_7": ["ffc6297d487a13cb80689c448a3aef16cbd367a503d236d0aebd7218cc568e88"], + "k_0_8": ["f4a6c41fc432300509f97ca68db3b9d802d29f90c35a429e3886c480cdce44a2"], + "k_0_9": ["52f3bf96d02cd7e8c631b8ef36262994a3ec658427b930197ed004c8599cd7fd"], + "k_0_10": [ + "7993aef51bebe79bae2d29b775c499f390e99fdb6b9accb8034f657b6e72927a" + ], + "k_0_11": ["430214c9805d90c6a8c4489187db08157a93e60509e96b518dc8b5ba3d567206"] +}; +final Map> historyBatchArgs2 = { + "k_0_0": ["afe5085dd514032810d5b266007557ba8a0f4bee84535cb10754c6d46ab8689b"], + "k_0_1": ["dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c"], + "k_0_2": ["e65d4274e8edc5cc1e7b676652e2e13b0b00648d15cf13caa982ecd6a67731ba"], + "k_0_3": ["6c69ca274f7d7f2fae882a87bcee93d9429328995c5bc6b326b548b4cefcaa9f"], + "k_0_4": ["86f1a5e17dc42c27cdb0dff8a41c2434575ab05ed2f3689fd7b674677e5ea446"], + "k_0_5": ["a5d9b8df5b80c56e6053497a8c89a37267010926e80e0d225a019b78673a7aa7"], + "k_0_6": ["a0030024518874720b82b38d965fb5b3083d9f42fab40e6be4797c789eeb06f2"], + "k_0_7": ["f20077f7c6a6b92a1f75bbbad8dbece9ae4609cfdfc85e40ccac7d463bdfd6e0"], + "k_0_8": ["07b7bb4020c377e0741587efe9c0b3931e2e45f667bc6f1fa81a8f15fbe86ce4"], + "k_0_9": ["ca0322fc293f6e4d8c8adac178ed4aaedbd9acd2ec84acaaf1529f9ab7bda6d2"], + "k_0_10": [ + "06df1d13aa43375775d7d2838595a0c4c642f8af15b06a99d5115d9236e9a79e" + ], + "k_0_11": ["1a146c5a8dd5bf49faca3c6f65c9d955d361c6c00893c48d69cf7ff42c7b387b"] +}; + +final Map> historyBatchArgs3 = { + "k_0_0": ["5c2c77a3671417c5831c336805770344b81e6c7ef0d873c986ba65a7bacd5f68"], + "k_0_1": ["c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b"], + "k_0_2": ["f430c440e90c48b9e4c7e5356083e7c1495b7cad53f39ebba64cca9fb3d05c82"], + "k_0_3": ["30a7ac6789383f7f6def9a927f3b6fb661cf9406fec71a1d118c7d86052382fb"], + "k_0_4": ["a797225a9155417ab18e16b9d7ce9bf4962ae5c05df572a33c60b36a0523f257"], + "k_0_5": ["24d1e3ac9e53727d943688e67eb5c000d993e9c3cf9585d630624197fb73bed3"], + "k_0_6": ["d667a44404519649cb65632d6a3be948a1f0971025c96cb4211943d301fe0d3e"], + "k_0_7": ["be8da400f004546b528fb989c14a88324b8b0c2d5680cf080ae1e1dac4401f68"], + "k_0_8": ["addfa7682c0a2461ab0e82b3c9302b38986b442a1a76c3c839b6c2f0eaa805fe"], + "k_0_9": ["98bb3aab55f4f305fd9994334b8dd3321eda50b25fad2ef3e636714b650d0bb0"], + "k_0_10": [ + "bee1eee20d7169d03ce68d340a17f4598f589920513ec19c458db45399639a9f" + ], + "k_0_11": ["928a988dd65d100d1677a0478abfcd4d2a70aabb0812c58a2b1b4b51c395ed54"] +}; + +final Map> historyBatchArgs4 = { + "k_0_0": ["6bbfd9c1c28d6984646db4736196f67f2d1075894bb1d8990294ca7d663bece6"], + "k_0_1": ["42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3"], + "k_0_2": ["191c977174dc50a57628aea6684c428d3a5e90bbe16c4e412be51b0cfc589d38"], + "k_0_3": ["0daaf61564fd07a25ef106d958216992896f931f5bed4fbf56cc3f94443dc164"], + "k_0_4": ["ac5aca40fed2903def31c9ef1d60874247cdcc5b85238c7a1d83c67d2924d6b9"], + "k_0_5": ["c4102ff0556d863b4bab9d8232fe1f0c0fde4b6e4fe23064b4ecd0958f9726cc"], + "k_0_6": ["1c4bd1554e4992e5914dcd8f3e13927ffd46302dfdcbd2dca0cfd47c040c4256"], + "k_0_7": ["eaf5562ebef7cafa58e2c1fc4ae023e5ae8dd71ee637b08c4bc7e274e401a9a4"], + "k_0_8": ["06f7f55c221fee1b36284b5360155b8380cb9d7172b7e28eb37c61b7ebb6f227"], + "k_0_9": ["7e7ca801131ec1c5797f2c4aa46908ee50e9958cf1cbf53c2481d110800c3d6d"], + "k_0_10": [ + "3895e073aa034add7d2589bfdd1e54f6b9a8d7688d63fff0c3aac7950c6f9697" + ], + "k_0_11": ["ec17dd7c4fe8fbcfce94e9237d3c7ed7f5c91a45b1a060406e206df7e814b006"] +}; + +final Map> historyBatchArgs5 = { + "k_0_0": ["83b744ccb88827d544081c1a03ea782a7d00d6224ff9fddb7d0fbad399e1cae7"], + "k_0_1": ["86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874"], + "k_0_2": ["5baba32b1899d5e740838559ef39b7d8e9ba302bd24b732eeedd4c0e6ec65b51"], + "k_0_3": ["9892eb48394b0e155f63879fb89c3b068fcc071fed2e5cb11fe0729b85b53d67"], + "k_0_4": ["64192782cdaecb5e2a871a2d0fb3f541873e4750cd4e7d28e4d858ab40664a36"], + "k_0_5": ["4047ff48e96d25628acfeaec6ca75c1a668c54fd70a14414827cb59976a3b666"], + "k_0_6": ["299e8bc634ef6438c5bf99c12c2340c77c56ab974ffd767e77c17994e5cfaef8"], + "k_0_7": ["ab649fa14452563b385eb025e0b4cf2dd869c02fcdf2ec0f72725bbe2adaa3bd"], + "k_0_8": ["6be1ca4f8ee923e32137b6cdae324b841a0a60afbee4f4ae457fe31f29e001a6"], + "k_0_9": ["2a99ceea87df667135cc1801682d2c5dc7b95b7efadc48e156345ba46f4c0dc6"], + "k_0_10": [ + "9304094916a19040d3c8f10df90dae1144d1f09ac9e676e66bb76341c70388ac" + ], + "k_0_11": ["01b12fb2ea2533226471dfa863133ce390e3e13a804734e8af995a45aa7c7582"] +}; + +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 = [ + "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c", + "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e", + "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9", + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874", + "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b", + "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" +]; diff --git a/test/services/coins/particl/particl_transaction_data_samples.dart b/test/services/coins/particl/particl_transaction_data_samples.dart new file mode 100644 index 000000000..fb2a2a932 --- /dev/null +++ b/test/services/coins/particl/particl_transaction_data_samples.dart @@ -0,0 +1,357 @@ +// TODO these test vectors are valid for Namecoin: update for Particl + +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/particl/particl_utxo_sample_data.dart b/test/services/coins/particl/particl_utxo_sample_data.dart new file mode 100644 index 000000000..5a0dff492 --- /dev/null +++ b/test/services/coins/particl/particl_utxo_sample_data.dart @@ -0,0 +1,60 @@ +// TODO these test vectors are valid for Namecoin: update for Particl + +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/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart new file mode 100644 index 000000000..d867def8e --- /dev/null +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -0,0 +1,1743 @@ +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/particl/particl_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 'particl_history_sample_data.dart'; +import 'particl_transaction_data_samples.dart'; +import 'particl_utxo_sample_data.dart'; +import 'particl_wallet_test.mocks.dart'; +import 'particl_wallet_test_parameters.dart'; + +@GenerateMocks( + [ElectrumX, CachedElectrumX, PriceAPI, TransactionNotificationTracker]) +void main() { + group("particl constants", () { + test("particl minimum confirmations", () async { + expect(MINIMUM_CONFIRMATIONS, 2); + }); + test("particl dust limit", () async { + expect(DUST_LIMIT, 546); + }); + test("particl mainnet genesis block hash", () async { + expect(GENESIS_HASH_MAINNET, + "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"); + }); + test("particl testnet genesis block hash", () async { + expect(GENESIS_HASH_TESTNET, + "00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008"); + }); + }); + + test("particl 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 mainnet namecoin addresses", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late 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.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("valid mainnet legacy/p2pkh address type", () { + expect( + mainnetWallet?.addressType( + address: "N673DDbjPcrNgJmrhJ1xQXF9LLizQzvjEs"), + DerivePathType.bip44); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet bech32 p2wpkh address type", () { + expect( + mainnetWallet?.addressType( + address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v"), + DerivePathType.bip84); + 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); + }); + }); + + group("testNetworkConnection", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; + + NamecoinWallet? nmc; + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + nmc = NamecoinWallet( + walletId: "testNetworkConnection", + walletName: "testNetworkConnection", + coin: Coin.particl, + 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 nmc?.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 nmc?.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 nmc?.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 = "NMCtestWalletID"; + final testWalletName = "NMCWallet"; + + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; + + NamecoinWallet? nmc; + + setUp(() async { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + nmc = NamecoinWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("get networkType main", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get networkType test", () async { + nmc = NamecoinWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get cryptoCurrency", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get coinName", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get coinTicker", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get and set walletName", () async { + expect(Coin.particl, Coin.particl); + nmc?.walletName = "new name"; + expect(nmc?.walletName, "new name"); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("estimateTxFee", () async { + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); + expect(nmc?.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_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)) + .thenAnswer((realInvocation) async => Decimal.ten); + + final fees = await nmc?.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 nmc?.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 nmc?.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("Particl service class functions that depend on shared storage", () { + final testWalletId = "NMCtestWalletID"; + final testWalletName = "NMCWallet"; + + bool hiveAdaptersRegistered = false; + + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; + + NamecoinWallet? nmc; + + 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(); + + nmc = NamecoinWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + // test("initializeWallet no network", () async { + // when(client?.ping()).thenAnswer((_) async => false); + // expect(await nmc?.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 nmc?.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 nmc?.initializeNew(); + final wallets = await Hive.openBox(testWalletId); + + expectLater(() => nmc?.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(() => nmc?.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( + "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 nmc?.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 nmc?.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 nmc?.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 nmc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + expect(await nmc?.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 nmc?.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.particl)) + .thenAnswer((realInvocation) async {}); + + when(client?.getBatchHistory(args: { + "0": [ + "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + when(client?.getBatchHistory(args: { + "0": [ + "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + when(client?.getBatchHistory(args: { + "0": [ + "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + final wallet = await Hive.openBox(testWalletId); + + // restore so we have something to rescan + await nmc?.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 nmc?.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(client?.getBatchHistory(args: { + "0": [ + "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + ] + })).called(2); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .called(1); + + // for (final arg in dynamicArgValues) { + // final map = Map>.from(arg as Map); + // Map argCount = {}; + // + // // verify(client?.getBatchHistory(args: map)).called(1); + // // expect(activeScriptHashes.contains(map.values.first.first as String), + // // true); + // } + + // Map argCount = {}; + // + // for (final arg in dynamicArgValues) { + // final map = Map>.from(arg as Map); + // + // final str = jsonEncode(map); + // + // if (argCount[str] == null) { + // argCount[str] = 1; + // } else { + // argCount[str] = argCount[str]! + 1; + // } + // } + // + // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); + + 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(client?.getBatchHistory(args: { + "0": [ + "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .thenAnswer((realInvocation) async {}); + + final wallet = await Hive.openBox(testWalletId); + + // restore so we have something to rescan + await nmc?.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 nmc?.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(client?.getBatchHistory(args: { + "0": [ + "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + ] + })).called(1); + verify(client?.getBatchHistory(args: { + "0": [ + "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + ] + })).called(2); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .called(1); + + expect(secureStore.writes, 19); + expect(secureStore.reads, 32); + expect(secureStore.deletes, 12); + + 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: + "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + coin: Coin.particl)) + .thenAnswer((_) async => tx2Raw); + when(cachedClient?.getTransaction( + txHash: + "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + coin: Coin.particl)) + .thenAnswer((_) async => tx3Raw); + when(cachedClient?.getTransaction( + txHash: + "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", + coin: Coin.particl, + )).thenAnswer((_) async => tx4Raw); + + // recover to fill data + await nmc?.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")); + + nmc?.outputsList = utxoList; + + bool didThrow = false; + try { + await nmc?.prepareSend( + address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v", + 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.particl, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coin: Coin.particl, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coin: Coin.particl, + // 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 nmc?.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 nmc?.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 nmc?.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 nmc + ?.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 nmc + ?.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 { + // // nmc = 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 = nmc.cachedElectrumXClient; + // // + // // // return when refresh is called on node changed trigger + // // nmc.longMutex = true; + // // + // // GlobalEventBus.instance + // // .fire(NodesChangedEvent(NodesChangedEventType.updatedCurrentNode)); + // // + // // // make sure event has processed before continuing + // // await Future.delayed(Duration(seconds: 5)); + // // + // // final b = nmc.cachedElectrumXClient; + // // + // // expect(identical(a, b), false); + // // + // // await nmc.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 nmc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + nmc?.refreshMutex = true; + + await nmc?.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.particl: 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 nmc?.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 nmc?.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/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart new file mode 100644 index 000000000..91c3e5bfa --- /dev/null +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -0,0 +1,629 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in stackwallet/test/services/coins/namecoin/namecoin_wallet_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +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 +// ignore_for_file: subtype_of_sealed_class + +class _FakeDecimal_0 extends _i1.SmartFake implements _i2.Decimal { + _FakeDecimal_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePrefs_1 extends _i1.SmartFake implements _i3.Prefs { + _FakePrefs_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeClient_2 extends _i1.SmartFake implements _i4.Client { + _FakeClient_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// 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({ + required 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: _i6.Future.value(), + ) as _i6.Future); + @override + _i6.Future>> batchRequest({ + required String? command, + required 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: _i6.Future>>.value( + >[]), + ) as _i6.Future>>); + @override + _i6.Future ping({ + String? requestID, + int? retryCount = 1, + }) => + (super.noSuchMethod( + Invocation.method( + #ping, + [], + { + #requestID: requestID, + #retryCount: retryCount, + }, + ), + returnValue: _i6.Future.value(false), + ) as _i6.Future); + @override + _i6.Future> getBlockHeadTip({String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getBlockHeadTip, + [], + {#requestID: requestID}, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future> getServerFeatures({String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getServerFeatures, + [], + {#requestID: requestID}, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future broadcastTransaction({ + required String? rawTx, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #broadcastTransaction, + [], + { + #rawTx: rawTx, + #requestID: requestID, + }, + ), + returnValue: _i6.Future.value(''), + ) as _i6.Future); + @override + _i6.Future> getBalance({ + required String? scripthash, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getBalance, + [], + { + #scripthash: scripthash, + #requestID: requestID, + }, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future>> getHistory({ + required String? scripthash, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getHistory, + [], + { + #scripthash: scripthash, + #requestID: requestID, + }, + ), + returnValue: _i6.Future>>.value( + >[]), + ) as _i6.Future>>); + @override + _i6.Future>>> getBatchHistory( + {required Map>? args}) => + (super.noSuchMethod( + Invocation.method( + #getBatchHistory, + [], + {#args: args}, + ), + returnValue: _i6.Future>>>.value( + >>{}), + ) as _i6.Future>>>); + @override + _i6.Future>> getUTXOs({ + required String? scripthash, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getUTXOs, + [], + { + #scripthash: scripthash, + #requestID: requestID, + }, + ), + returnValue: _i6.Future>>.value( + >[]), + ) as _i6.Future>>); + @override + _i6.Future>>> getBatchUTXOs( + {required Map>? args}) => + (super.noSuchMethod( + Invocation.method( + #getBatchUTXOs, + [], + {#args: args}, + ), + returnValue: _i6.Future>>>.value( + >>{}), + ) as _i6.Future>>>); + @override + _i6.Future> getTransaction({ + required String? txHash, + bool? verbose = true, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getTransaction, + [], + { + #txHash: txHash, + #verbose: verbose, + #requestID: requestID, + }, + ), + returnValue: + _i6.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: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future getMintData({ + dynamic mints, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getMintData, + [], + { + #mints: mints, + #requestID: requestID, + }, + ), + returnValue: _i6.Future.value(), + ) as _i6.Future); + @override + _i6.Future> getUsedCoinSerials({ + String? requestID, + required int? startNumber, + }) => + (super.noSuchMethod( + Invocation.method( + #getUsedCoinSerials, + [], + { + #requestID: requestID, + #startNumber: startNumber, + }, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future getLatestCoinId({String? requestID}) => (super.noSuchMethod( + Invocation.method( + #getLatestCoinId, + [], + {#requestID: requestID}, + ), + returnValue: _i6.Future.value(0), + ) as _i6.Future); + @override + _i6.Future> getFeeRate({String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getFeeRate, + [], + {#requestID: requestID}, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future<_i2.Decimal> estimateFee({ + String? requestID, + required int? blocks, + }) => + (super.noSuchMethod( + Invocation.method( + #estimateFee, + [], + { + #requestID: requestID, + #blocks: blocks, + }, + ), + returnValue: _i6.Future<_i2.Decimal>.value(_FakeDecimal_0( + this, + Invocation.method( + #estimateFee, + [], + { + #requestID: requestID, + #blocks: blocks, + }, + ), + )), + ) as _i6.Future<_i2.Decimal>); + @override + _i6.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + Invocation.method( + #relayFee, + [], + {#requestID: requestID}, + ), + returnValue: _i6.Future<_i2.Decimal>.value(_FakeDecimal_0( + this, + Invocation.method( + #relayFee, + [], + {#requestID: requestID}, + ), + )), + ) 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( + this, + Invocation.getter(#prefs), + ), + ) 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({ + required String? groupId, + String? blockhash = r'', + required _i8.Coin? coin, + }) => + (super.noSuchMethod( + Invocation.method( + #getAnonymitySet, + [], + { + #groupId: groupId, + #blockhash: blockhash, + #coin: coin, + }, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override + _i6.Future> getTransaction({ + required String? txHash, + required _i8.Coin? coin, + bool? verbose = true, + }) => + (super.noSuchMethod( + Invocation.method( + #getTransaction, + [], + { + #txHash: txHash, + #coin: coin, + #verbose: verbose, + }, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future> getUsedCoinSerials({ + required _i8.Coin? coin, + int? startNumber = 0, + }) => + (super.noSuchMethod( + Invocation.method( + #getUsedCoinSerials, + [], + { + #coin: coin, + #startNumber: startNumber, + }, + ), + returnValue: _i6.Future>.value([]), + ) as _i6.Future>); + @override + _i6.Future clearSharedTransactionCache({required _i8.Coin? coin}) => + (super.noSuchMethod( + Invocation.method( + #clearSharedTransactionCache, + [], + {#coin: coin}, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.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( + this, + Invocation.getter(#client), + ), + ) as _i4.Client); + @override + void resetLastCalledToForceNextCallToUpdateCache() => super.noSuchMethod( + Invocation.method( + #resetLastCalledToForceNextCallToUpdateCache, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i6.Future< + Map<_i8.Coin, _i10.Tuple2<_i2.Decimal, double>>> getPricesAnd24hChange( + {required String? baseCurrency}) => + (super.noSuchMethod( + Invocation.method( + #getPricesAnd24hChange, + [], + {#baseCurrency: baseCurrency}, + ), + returnValue: + _i6.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: _i6.Future.value(), + returnValueForMissingStub: _i6.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: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); +} From f507baa3679daac148fc783e7adde40f5d858bc1 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 12:54:29 -0600 Subject: [PATCH 012/103] update genesis hash test vector --- test/services/coins/particl/particl_wallet_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index d867def8e..2a85e7c2b 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -35,7 +35,7 @@ void main() { }); test("particl mainnet genesis block hash", () async { expect(GENESIS_HASH_MAINNET, - "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"); + "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"); // Was 000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770 }); test("particl testnet genesis block hash", () async { expect(GENESIS_HASH_TESTNET, From be14e39d8b6816e2349dfec0ca4523ef0599fde3 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 12:54:54 -0600 Subject: [PATCH 013/103] update secure storage interface --- lib/services/coins/particl/particl_wallet.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 1ad195a04..1981c4896 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -1301,7 +1301,7 @@ class ParticlWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1313,7 +1313,7 @@ class ParticlWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + SecureStorageInterface? secureStore, }) { txTracker = tracker; _walletId = walletId; From 2d5beca8a298f2078600896a1b884f27560d37d6 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 12:55:24 -0600 Subject: [PATCH 014/103] update bip32 root test TODO update ROOT_WIF ... is that in the .gitignored wallet test parameters? --- test/services/coins/particl/particl_wallet_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 2a85e7c2b..4c155bf6a 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -51,12 +51,12 @@ void main() { group("bip32 node/root", () { test("getBip32Root", () { - final root = getBip32Root(TEST_MNEMONIC, namecoin); + final root = getBip32Root(TEST_MNEMONIC, particl); expect(root.toWIF(), ROOT_WIF); }); // test("getBip32NodeFromRoot", () { - // final root = getBip32Root(TEST_MNEMONIC, namecoin); + // final root = getBip32Root(TEST_MNEMONIC, particl); // // two mainnet // final node44 = getBip32NodeFromRoot(0, 0, root, DerivePathType.bip44); // expect(node44.toWIF(), NODE_WIF_44); From 18f0629da27748b280faf3b6f380100af64976f8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 12:57:11 -0600 Subject: [PATCH 015/103] add in old .gitignore in case anyone has an old .dart.txt laying around might as well make sure the old gitignored file is still gitginored ... although it isn't referenced, is it? could probably cut this out --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d456eb159..e4bc4a75a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +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 # Legacy +test/services/coins/namecoin/namecoin_wallet_test_parameters.dart +test/services/coins/namecoin/namecoin_wallet_test_parameters.dart.txt # Legacy test/services/coins/namecoin/namecoin_wallet_test_parameters.txt # Legacy test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart test/services/coins/particl/particl_wallet_test_parameters.dart From 3e7b5fbda1add1b22099f3628f1c7ab3a813edfe Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 12:57:50 -0600 Subject: [PATCH 016/103] disable all but 2-3 particl tests TODO re-enable as we implement up to these --- .../coins/particl/particl_wallet_test.dart | 3136 ++++++++--------- 1 file changed, 1568 insertions(+), 1568 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 4c155bf6a..7fcd880a1 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -37,10 +37,10 @@ void main() { expect(GENESIS_HASH_MAINNET, "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"); // Was 000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770 }); - test("particl testnet genesis block hash", () async { - expect(GENESIS_HASH_TESTNET, - "00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008"); - }); + // test("particl testnet genesis block hash", () async { + // expect(GENESIS_HASH_TESTNET, + // "00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008"); + // }); }); test("particl DerivePathType enum", () { @@ -176,1568 +176,1568 @@ void main() { }); }); - group("testNetworkConnection", () { - MockElectrumX? client; - MockCachedElectrumX? cachedClient; - MockPriceAPI? priceAPI; - late FakeSecureStorage secureStore; - MockTransactionNotificationTracker? tracker; - - NamecoinWallet? nmc; - - setUp(() { - client = MockElectrumX(); - cachedClient = MockCachedElectrumX(); - priceAPI = MockPriceAPI(); - secureStore = FakeSecureStorage(); - tracker = MockTransactionNotificationTracker(); - - nmc = NamecoinWallet( - walletId: "testNetworkConnection", - walletName: "testNetworkConnection", - coin: Coin.particl, - 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 nmc?.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 nmc?.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 nmc?.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 = "NMCtestWalletID"; - final testWalletName = "NMCWallet"; - - MockElectrumX? client; - MockCachedElectrumX? cachedClient; - MockPriceAPI? priceAPI; - late FakeSecureStorage secureStore; - MockTransactionNotificationTracker? tracker; - - NamecoinWallet? nmc; - - setUp(() async { - client = MockElectrumX(); - cachedClient = MockCachedElectrumX(); - priceAPI = MockPriceAPI(); - secureStore = FakeSecureStorage(); - tracker = MockTransactionNotificationTracker(); - - nmc = NamecoinWallet( - walletId: testWalletId, - walletName: testWalletName, - coin: Coin.particl, - client: client!, - cachedClient: cachedClient!, - tracker: tracker!, - priceAPI: priceAPI, - secureStore: secureStore, - ); - }); - - test("get networkType main", () async { - expect(Coin.particl, Coin.particl); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("get networkType test", () async { - nmc = NamecoinWallet( - walletId: testWalletId, - walletName: testWalletName, - coin: Coin.particl, - client: client!, - cachedClient: cachedClient!, - tracker: tracker!, - priceAPI: priceAPI, - secureStore: secureStore, - ); - expect(Coin.particl, Coin.particl); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("get cryptoCurrency", () async { - expect(Coin.particl, Coin.particl); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("get coinName", () async { - expect(Coin.particl, Coin.particl); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("get coinTicker", () async { - expect(Coin.particl, Coin.particl); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("get and set walletName", () async { - expect(Coin.particl, Coin.particl); - nmc?.walletName = "new name"; - expect(nmc?.walletName, "new name"); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("estimateTxFee", () async { - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); - expect(nmc?.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_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)) - .thenAnswer((realInvocation) async => Decimal.ten); - - final fees = await nmc?.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 nmc?.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 nmc?.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("Particl service class functions that depend on shared storage", () { - final testWalletId = "NMCtestWalletID"; - final testWalletName = "NMCWallet"; - - bool hiveAdaptersRegistered = false; - - MockElectrumX? client; - MockCachedElectrumX? cachedClient; - MockPriceAPI? priceAPI; - late FakeSecureStorage secureStore; - MockTransactionNotificationTracker? tracker; - - NamecoinWallet? nmc; - - 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(); - - nmc = NamecoinWallet( - walletId: testWalletId, - walletName: testWalletName, - coin: Coin.particl, - client: client!, - cachedClient: cachedClient!, - tracker: tracker!, - priceAPI: priceAPI, - secureStore: secureStore, - ); - }); - - // test("initializeWallet no network", () async { - // when(client?.ping()).thenAnswer((_) async => false); - // expect(await nmc?.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 nmc?.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 nmc?.initializeNew(); - final wallets = await Hive.openBox(testWalletId); - - expectLater(() => nmc?.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(() => nmc?.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( - "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 nmc?.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 nmc?.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 nmc?.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 nmc?.recoverFromMnemonic( - mnemonic: TEST_MNEMONIC, - maxUnusedAddressGap: 2, - maxNumberOfIndexesToCheck: 1000, - height: 4000); - - expect(await nmc?.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 nmc?.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.particl)) - .thenAnswer((realInvocation) async {}); - - when(client?.getBatchHistory(args: { - "0": [ - "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - when(client?.getBatchHistory(args: { - "0": [ - "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - when(client?.getBatchHistory(args: { - "0": [ - "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - final wallet = await Hive.openBox(testWalletId); - - // restore so we have something to rescan - await nmc?.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 nmc?.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(client?.getBatchHistory(args: { - "0": [ - "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - ] - })).called(2); - - verify(client?.getBatchHistory(args: { - "0": [ - "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - ] - })).called(2); - - verify(client?.getBatchHistory(args: { - "0": [ - "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - ] - })).called(2); - - verify(client?.getBatchHistory(args: { - "0": [ - "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - ] - })).called(2); - - verify(client?.getBatchHistory(args: { - "0": [ - "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - ] - })).called(2); - verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - .called(1); - - // for (final arg in dynamicArgValues) { - // final map = Map>.from(arg as Map); - // Map argCount = {}; - // - // // verify(client?.getBatchHistory(args: map)).called(1); - // // expect(activeScriptHashes.contains(map.values.first.first as String), - // // true); - // } - - // Map argCount = {}; - // - // for (final arg in dynamicArgValues) { - // final map = Map>.from(arg as Map); - // - // final str = jsonEncode(map); - // - // if (argCount[str] == null) { - // argCount[str] = 1; - // } else { - // argCount[str] = argCount[str]! + 1; - // } - // } - // - // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); - - 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(client?.getBatchHistory(args: { - "0": [ - "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - .thenAnswer((realInvocation) async {}); - - final wallet = await Hive.openBox(testWalletId); - - // restore so we have something to rescan - await nmc?.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 nmc?.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(client?.getBatchHistory(args: { - "0": [ - "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - ] - })).called(1); - verify(client?.getBatchHistory(args: { - "0": [ - "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - ] - })).called(2); - verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - .called(1); - - expect(secureStore.writes, 19); - expect(secureStore.reads, 32); - expect(secureStore.deletes, 12); - - 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: - "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", - coin: Coin.particl)) - .thenAnswer((_) async => tx2Raw); - when(cachedClient?.getTransaction( - txHash: - "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", - coin: Coin.particl)) - .thenAnswer((_) async => tx3Raw); - when(cachedClient?.getTransaction( - txHash: - "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", - coin: Coin.particl, - )).thenAnswer((_) async => tx4Raw); - - // recover to fill data - await nmc?.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")); - - nmc?.outputsList = utxoList; - - bool didThrow = false; - try { - await nmc?.prepareSend( - address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v", - 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.particl, - // callOutSideMainIsolate: false)) - // .called(1); - // verify(cachedClient?.getTransaction( - // txHash: - // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", - // coin: Coin.particl, - // callOutSideMainIsolate: false)) - // .called(1); - // verify(cachedClient?.getTransaction( - // txHash: - // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", - // coin: Coin.particl, - // 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 nmc?.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 nmc?.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 nmc?.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 nmc - ?.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 nmc - ?.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 { - // // nmc = 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 = nmc.cachedElectrumXClient; - // // - // // // return when refresh is called on node changed trigger - // // nmc.longMutex = true; - // // - // // GlobalEventBus.instance - // // .fire(NodesChangedEvent(NodesChangedEventType.updatedCurrentNode)); - // // - // // // make sure event has processed before continuing - // // await Future.delayed(Duration(seconds: 5)); - // // - // // final b = nmc.cachedElectrumXClient; - // // - // // expect(identical(a, b), false); - // // - // // await nmc.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 nmc?.recoverFromMnemonic( - mnemonic: TEST_MNEMONIC, - maxUnusedAddressGap: 2, - maxNumberOfIndexesToCheck: 1000, - height: 4000); - - nmc?.refreshMutex = true; - - await nmc?.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.particl: 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 nmc?.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 nmc?.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(); - }); - }); + // group("testNetworkConnection", () { + // MockElectrumX? client; + // MockCachedElectrumX? cachedClient; + // MockPriceAPI? priceAPI; + // late FakeSecureStorage secureStore; + // MockTransactionNotificationTracker? tracker; + + // NamecoinWallet? nmc; + + // setUp(() { + // client = MockElectrumX(); + // cachedClient = MockCachedElectrumX(); + // priceAPI = MockPriceAPI(); + // secureStore = FakeSecureStorage(); + // tracker = MockTransactionNotificationTracker(); + + // nmc = NamecoinWallet( + // walletId: "testNetworkConnection", + // walletName: "testNetworkConnection", + // coin: Coin.particl, + // 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 nmc?.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 nmc?.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 nmc?.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 = "NMCtestWalletID"; + // final testWalletName = "NMCWallet"; + + // MockElectrumX? client; + // MockCachedElectrumX? cachedClient; + // MockPriceAPI? priceAPI; + // late FakeSecureStorage secureStore; + // MockTransactionNotificationTracker? tracker; + + // NamecoinWallet? nmc; + + // setUp(() async { + // client = MockElectrumX(); + // cachedClient = MockCachedElectrumX(); + // priceAPI = MockPriceAPI(); + // secureStore = FakeSecureStorage(); + // tracker = MockTransactionNotificationTracker(); + + // nmc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.particl, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // }); + + // test("get networkType main", () async { + // expect(Coin.particl, Coin.particl); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get networkType test", () async { + // nmc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.particl, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // expect(Coin.particl, Coin.particl); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get cryptoCurrency", () async { + // expect(Coin.particl, Coin.particl); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get coinName", () async { + // expect(Coin.particl, Coin.particl); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get coinTicker", () async { + // expect(Coin.particl, Coin.particl); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get and set walletName", () async { + // expect(Coin.particl, Coin.particl); + // nmc?.walletName = "new name"; + // expect(nmc?.walletName, "new name"); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("estimateTxFee", () async { + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); + // expect(nmc?.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_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)) + // .thenAnswer((realInvocation) async => Decimal.ten); + + // final fees = await nmc?.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 nmc?.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 nmc?.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("Particl service class functions that depend on shared storage", () { + // final testWalletId = "NMCtestWalletID"; + // final testWalletName = "NMCWallet"; + + // bool hiveAdaptersRegistered = false; + + // MockElectrumX? client; + // MockCachedElectrumX? cachedClient; + // MockPriceAPI? priceAPI; + // late FakeSecureStorage secureStore; + // MockTransactionNotificationTracker? tracker; + + // NamecoinWallet? nmc; + + // 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(); + + // nmc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.particl, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // }); + + // // test("initializeWallet no network", () async { + // // when(client?.ping()).thenAnswer((_) async => false); + // // expect(await nmc?.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 nmc?.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 nmc?.initializeNew(); + // final wallets = await Hive.openBox(testWalletId); + + // expectLater(() => nmc?.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(() => nmc?.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( + // "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 nmc?.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 nmc?.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 nmc?.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 nmc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000, + // height: 4000); + + // expect(await nmc?.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 nmc?.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.particl)) + // .thenAnswer((realInvocation) async {}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // when(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // when(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // final wallet = await Hive.openBox(testWalletId); + + // // restore so we have something to rescan + // await nmc?.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 nmc?.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(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).called(2); + + // verify(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).called(2); + + // verify(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).called(2); + + // verify(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).called(2); + + // verify(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).called(2); + // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + // .called(1); + + // // for (final arg in dynamicArgValues) { + // // final map = Map>.from(arg as Map); + // // Map argCount = {}; + // // + // // // verify(client?.getBatchHistory(args: map)).called(1); + // // // expect(activeScriptHashes.contains(map.values.first.first as String), + // // // true); + // // } + + // // Map argCount = {}; + // // + // // for (final arg in dynamicArgValues) { + // // final map = Map>.from(arg as Map); + // // + // // final str = jsonEncode(map); + // // + // // if (argCount[str] == null) { + // // argCount[str] = 1; + // // } else { + // // argCount[str] = argCount[str]! + 1; + // // } + // // } + // // + // // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); + + // 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(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + // .thenAnswer((realInvocation) async {}); + + // final wallet = await Hive.openBox(testWalletId); + + // // restore so we have something to rescan + // await nmc?.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 nmc?.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(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).called(1); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).called(2); + // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + // .called(1); + + // expect(secureStore.writes, 19); + // expect(secureStore.reads, 32); + // expect(secureStore.deletes, 12); + + // 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: + // "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + // coin: Coin.particl)) + // .thenAnswer((_) async => tx2Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + // coin: Coin.particl)) + // .thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", + // coin: Coin.particl, + // )).thenAnswer((_) async => tx4Raw); + + // // recover to fill data + // await nmc?.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")); + + // nmc?.outputsList = utxoList; + + // bool didThrow = false; + // try { + // await nmc?.prepareSend( + // address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v", + // 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.particl, + // // callOutSideMainIsolate: false)) + // // .called(1); + // // verify(cachedClient?.getTransaction( + // // txHash: + // // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // // coin: Coin.particl, + // // callOutSideMainIsolate: false)) + // // .called(1); + // // verify(cachedClient?.getTransaction( + // // txHash: + // // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // // coin: Coin.particl, + // // 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 nmc?.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 nmc?.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 nmc?.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 nmc + // ?.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 nmc + // ?.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 { + // // // nmc = 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 = nmc.cachedElectrumXClient; + // // // + // // // // return when refresh is called on node changed trigger + // // // nmc.longMutex = true; + // // // + // // // GlobalEventBus.instance + // // // .fire(NodesChangedEvent(NodesChangedEventType.updatedCurrentNode)); + // // // + // // // // make sure event has processed before continuing + // // // await Future.delayed(Duration(seconds: 5)); + // // // + // // // final b = nmc.cachedElectrumXClient; + // // // + // // // expect(identical(a, b), false); + // // // + // // // await nmc.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 nmc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000, + // height: 4000); + + // nmc?.refreshMutex = true; + + // await nmc?.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.particl: 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 nmc?.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 nmc?.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(); + // }); + // }); } From c7abf3a7e8e6f82c2b8610b0226f532e60417bd7 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 13:11:30 -0600 Subject: [PATCH 017/103] WIP: Add particle --- .../add_edit_node_view.dart | 4 + lib/services/coins/coin_service.dart | 19 + .../coins/particl/particl_wallet.dart | 3813 +++++++++++++++++ lib/utilities/address_utils.dart | 5 + lib/utilities/assets.dart | 12 + lib/utilities/block_explorers.dart | 4 + lib/utilities/constants.dart | 6 + lib/utilities/default_nodes.dart | 29 + lib/utilities/enums/coin_enum.dart | 31 + lib/utilities/theme/color_theme.dart | 4 + lib/utilities/theme/stack_colors.dart | 3 + 11 files changed, 3930 insertions(+) create mode 100644 lib/services/coins/particl/particl_wallet.dart diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 32fa3974a..0cf616013 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 @@ -142,11 +142,13 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.dogecoin: case Coin.firo: case Coin.namecoin: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: + case Coin.particlTestNet: final client = ElectrumX( host: formData.host!, port: formData.port!, @@ -687,11 +689,13 @@ class _NodeFormState extends ConsumerState { case Coin.firo: case Coin.namecoin: case Coin.bitcoincash: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: + case Coin.particlTestNet: return false; case Coin.epicCash: diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 16015ea0c..e0031d781 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -10,6 +10,7 @@ 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/namecoin/namecoin_wallet.dart'; +import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -181,6 +182,24 @@ abstract class CoinServiceAPI { // tracker: tracker, ); + case Coin.particl: + return ParticlWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + client: client, + cachedClient: cachedClient, + tracker: tracker); + + case Coin.particlTestNet: + return ParticlWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + client: client, + cachedClient: cachedClient, + tracker: tracker); + case Coin.wownero: return WowneroWallet( walletId: walletId, diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart new file mode 100644 index 000000000..77d0fa7db --- /dev/null +++ b/lib/services/coins/particl/particl_wallet.dart @@ -0,0 +1,3813 @@ +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 = 1; +const int DUST_LIMIT = 294; + +const String GENESIS_HASH_MAINNET = + "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"; +const String GENESIS_HASH_TESTNET = + "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; + +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 0x6c: // PART mainnet wif + coinType = "44"; // PART mainnet + break; + case 0x2e: // PART testnet wif + coinType = "1"; // PART testnet + break; + default: + throw Exception("Invalid Particl 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 ParticlWallet 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.particl: + return particl; + case Coin.particlTestNet: + return particltestnet; + 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); + } 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.particl: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.particlTestNet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a ParticlWallet using a non particl 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) + .data), + network: _network) + .data + .address!; + break; + case DerivePathType.bip84: + address = P2WPKH( + network: _network, + data: PaymentData(pubkey: node.publicKey)) + .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); + } + + @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; + + ParticlWallet({ + 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.particl: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.particlTestNet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a ParticlWallet using a non particl 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).data), + network: _network) + .data + .address!; + break; + case DerivePathType.bip84: + address = P2WPKH(network: _network, data: data).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); + 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 particl address + String _convertToScriptHash(String particlAddress, NetworkType network) { + try { + final output = Address.addressToOutputScript(particlAddress, 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 = 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"]["address"] 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"]["address"] 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 String address = output["scriptPubKey"]!["address"] as String; + 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"]["address"]; + 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 = []; + + 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"]["address"] 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) + .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) + .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, + ).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, + ).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(1); + + // Add transaction inputs + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + txb.addInput(txid, utxosToUse[i].vout, null, + utxoSigningData[txid]["output"] as Uint8List); + } + + // Add transaction output + for (var i = 0; i < recipients.length; i++) { + txb.addOutput(recipients[i], satoshiAmounts[i]); + } + + try { + // Sign the transaction accordingly + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + txb.sign( + vin: i, + keyPair: utxoSigningData[txid]["keyPair"] as ECPair, + witnessValue: utxosToUse[i].value, + redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + ); + } + } catch (e, s) { + Logging.instance.log("Caught exception while signing transaction: $e\n$s", + level: LogLevel.Error); + rethrow; + } + + final builtTx = txb.build(); + final vSize = builtTx.virtualSize(); + + return {"hex": builtTx.toHex(), "vSize": vSize}; + } + + @override + Future 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; + } + } + + 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; + } + } +} + +// Particl Network +final particl = NetworkType( + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'pw', + bip32: Bip32Type(public: 0x696e82d1, private: 0x8f1daeb8), + pubKeyHash: 0x38, + scriptHash: 0x3c, + wif: 0x6c); + +final particltestnet = NetworkType( + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'tpw', + bip32: Bip32Type(public: 0xe1427800, private: 0x04889478), + pubKeyHash: 0x76, + scriptHash: 0x7a, + wif: 0x2e); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 65303b24d..376471c45 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; +import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -61,6 +62,8 @@ class AddressUtils { RegExp("[a-zA-Z0-9]{106}").hasMatch(address); case Coin.namecoin: return Address.validateAddress(address, namecoin, namecoin.bech32!); + case Coin.particl: + return Address.validateAddress(address, particl); case Coin.bitcoinTestNet: return Address.validateAddress(address, testnet); case Coin.litecoinTestNet: @@ -71,6 +74,8 @@ class AddressUtils { return Address.validateAddress(address, firoTestNetwork); case Coin.dogecoinTestNet: return Address.validateAddress(address, dogecointestnet); + case Coin.particlTestNet: + return Address.validateAddress(address, particltestnet); } } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 27c8fe3b4..74510418c 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -180,6 +180,8 @@ class _SVG { String get monero => "assets/svg/coin_icons/Monero.svg"; String get wownero => "assets/svg/coin_icons/Wownero.svg"; String get namecoin => "assets/svg/coin_icons/Namecoin.svg"; + String get particl => + "assets/svg/coin_icons/Namecoin.svg"; //TODO - Update icon to particl String get chevronRight => "assets/svg/chevron-right.svg"; String get minimize => "assets/svg/minimize.svg"; @@ -192,6 +194,8 @@ class _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"; + String get particlTestnet => + "assets/svg/coin_icons/Dogecoin.svg"; //TODO - Update icon to particl String iconFor({required Coin coin}) { switch (coin) { @@ -214,6 +218,8 @@ class _SVG { return wownero; case Coin.namecoin: return namecoin; + case Coin.particl: + return particl; case Coin.bitcoinTestNet: return bitcoinTestnet; case Coin.bitcoincashTestnet: @@ -222,6 +228,8 @@ class _SVG { return firoTestnet; case Coin.dogecoinTestNet: return dogecoinTestnet; + case Coin.particlTestNet: + return particlTestnet; } } } @@ -241,6 +249,7 @@ class _PNG { String get epicCash => "assets/images/epic-cash.png"; String get bitcoincash => "assets/images/bitcoincash.png"; String get namecoin => "assets/images/namecoin.png"; + String get particl => "assets/images/namecoin.png"; //TODO - use particl png String get glasses => "assets/images/glasses.png"; String get glassesHidden => "assets/images/glasses-hidden.png"; @@ -271,6 +280,9 @@ class _PNG { return wownero; case Coin.namecoin: return namecoin; + case Coin.particl: + case Coin.particlTestNet: + return particl; } } } diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index 69afb4a83..d259524d5 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -35,5 +35,9 @@ Uri getBlockExplorerTransactionUrlFor({ "https://blockexplorer.one/bitcoin-cash/testnet/tx/$txid"); case Coin.namecoin: return Uri.parse("https://chainz.cryptoid.info/nmc/tx.dws?$txid.htm"); + case Coin.particl: + return Uri.parse("https://chainz.cryptoid.info/part/tx.dws?$txid.htm"); + case Coin.particlTestNet: + return Uri.parse(""); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 3263d526e..a6fc902d5 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -103,6 +103,8 @@ abstract class Constants { case Coin.firoTestNet: case Coin.epicCash: case Coin.namecoin: + case Coin.particl: + case Coin.particlTestNet: values.addAll([24, 21, 18, 15, 12]); break; @@ -150,6 +152,10 @@ abstract class Constants { case Coin.namecoin: return 600; + + case Coin.particl: + case Coin.particlTestNet: + return 600; } } diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index c9e96fbac..618aeb7d4 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -145,6 +145,17 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get particl => NodeModel( + host: "host", + port: 123, + name: defaultName, + id: _nodeId(Coin.particl), + useSSL: true, + enabled: true, + coinName: Coin.particl.name, + isFailover: true, + isDown: false); //TODO - UPDATE WITH CORRECT DETAILS + static NodeModel get bitcoinTestnet => NodeModel( host: "electrumx-testnet.cypherstack.com", port: 51002, @@ -193,6 +204,18 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get particlTestnet => NodeModel( + host: "host", + port: 60002, + name: defaultName, + id: _nodeId(Coin.particlTestNet), + useSSL: true, + enabled: true, + coinName: Coin.particlTestNet.name, + isFailover: true, + isDown: false, + ); + static NodeModel getNodeFor(Coin coin) { switch (coin) { case Coin.bitcoin: @@ -222,6 +245,9 @@ abstract class DefaultNodes { case Coin.namecoin: return namecoin; + case Coin.particl: + return namecoin; + case Coin.bitcoinTestNet: return bitcoinTestnet; @@ -236,6 +262,9 @@ abstract class DefaultNodes { case Coin.dogecoinTestNet: return dogecoinTestnet; + + case Coin.particlTestNet: + return particlTestnet; } } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 543a193ee..4e45342bf 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -13,6 +13,8 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/services/coins/particl/particl_wallet.dart' + as particl; enum Coin { bitcoin, @@ -23,6 +25,7 @@ enum Coin { litecoin, monero, namecoin, + particl, wownero, /// @@ -35,6 +38,7 @@ enum Coin { bitcoincashTestnet, dogecoinTestNet, firoTestNet, + particlTestNet } final int kTestNetCoinCount = Util.isDesktop ? 5 : 4; @@ -56,6 +60,8 @@ extension CoinExt on Coin { return "Firo"; case Coin.monero: return "Monero"; + case Coin.particl: + return "Particl"; case Coin.wownero: return "Wownero"; case Coin.namecoin: @@ -70,6 +76,8 @@ extension CoinExt on Coin { return "tFiro"; case Coin.dogecoinTestNet: return "tDogecoin"; + case Coin.particlTestNet: + return "tParticl"; } } @@ -89,6 +97,8 @@ extension CoinExt on Coin { return "FIRO"; case Coin.monero: return "XMR"; + case Coin.particl: + return "PART"; case Coin.wownero: return "WOW"; case Coin.namecoin: @@ -103,6 +113,8 @@ extension CoinExt on Coin { return "tFIRO"; case Coin.dogecoinTestNet: return "tDOGE"; + case Coin.particlTestNet: + return "tPART"; } } @@ -123,6 +135,8 @@ extension CoinExt on Coin { return "firo"; case Coin.monero: return "monero"; + case Coin.particl: + return "particl"; case Coin.wownero: return "wownero"; case Coin.namecoin: @@ -137,6 +151,8 @@ extension CoinExt on Coin { return "firo"; case Coin.dogecoinTestNet: return "dogecoin"; + case Coin.particlTestNet: + return "particl"; } } @@ -148,11 +164,13 @@ extension CoinExt on Coin { case Coin.dogecoin: case Coin.firo: case Coin.namecoin: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: + case Coin.particlTestNet: return true; case Coin.epicCash: @@ -190,6 +208,10 @@ extension CoinExt on Coin { case Coin.monero: return xmr.MINIMUM_CONFIRMATIONS; + case Coin.particl: + case Coin.particlTestNet: + return particl.MINIMUM_CONFIRMATIONS; + case Coin.wownero: return wow.MINIMUM_CONFIRMATIONS; @@ -230,6 +252,11 @@ Coin coinFromPrettyName(String name) { case "monero": return Coin.monero; + case "Particl": + case "particl": + case "particlTestNet": + return Coin.particl; + case "Namecoin": case "namecoin": return Coin.namecoin; @@ -295,6 +322,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.namecoin; case "tltc": return Coin.litecoinTestNet; + case "part": + return Coin.particl; case "tbtc": return Coin.bitcoinTestNet; case "tbch": @@ -303,6 +332,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.firoTestNet; case "tdoge": return Coin.dogecoinTestNet; + case "tparticl": + return Coin.particlTestNet; case "wow": return Coin.wownero; default: diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 4a480491c..8a73e7010 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -193,6 +193,7 @@ class CoinThemeColor { Color get monero => const Color(0xFFFF9E6B); Color get namecoin => const Color(0xFF91B1E1); Color get wownero => const Color(0xFFED80C1); + Color get particl => const Color(0xFFED80C1); //TODO - Use part colors Color forCoin(Coin coin) { switch (coin) { @@ -219,6 +220,9 @@ class CoinThemeColor { return namecoin; case Coin.wownero: return wownero; + case Coin.particl: + case Coin.particlTestNet: + return particl; } } } diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 935fa03ae..66ce107f1 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -1443,6 +1443,9 @@ class StackColors extends ThemeExtension { return _coin.namecoin; case Coin.wownero: return _coin.wownero; + case Coin.particl: + case Coin.particlTestNet: + return _coin.particl; } } From d9338d34f276c6ac54c1267528c9cb2d246e4781 Mon Sep 17 00:00:00 2001 From: Likho Date: Tue, 25 Oct 2022 17:43:58 +0200 Subject: [PATCH 018/103] Fix particl node --- lib/utilities/default_nodes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 618aeb7d4..1cbb4e07e 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -246,7 +246,7 @@ abstract class DefaultNodes { return namecoin; case Coin.particl: - return namecoin; + return particl; case Coin.bitcoinTestNet: return bitcoinTestnet; From ec399ade0aef1d9ab2dd78876a2d20819dae4ba0 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 13:14:11 -0600 Subject: [PATCH 019/103] Remove testnet --- .../add_edit_node_view.dart | 2 -- lib/services/coins/coin_service.dart | 9 ------- .../coins/particl/particl_wallet.dart | 24 +------------------ lib/utilities/address_utils.dart | 2 -- lib/utilities/assets.dart | 3 --- lib/utilities/block_explorers.dart | 2 -- lib/utilities/constants.dart | 2 -- lib/utilities/default_nodes.dart | 19 ++------------- lib/utilities/enums/coin_enum.dart | 12 ---------- lib/utilities/theme/color_theme.dart | 1 - lib/utilities/theme/stack_colors.dart | 1 - 11 files changed, 3 insertions(+), 74 deletions(-) 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 0cf616013..6c515b468 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 @@ -148,7 +148,6 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: - case Coin.particlTestNet: final client = ElectrumX( host: formData.host!, port: formData.port!, @@ -695,7 +694,6 @@ class _NodeFormState extends ConsumerState { case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: - case Coin.particlTestNet: return false; case Coin.epicCash: diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index e0031d781..c9c6c0138 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -191,15 +191,6 @@ abstract class CoinServiceAPI { cachedClient: cachedClient, tracker: tracker); - case Coin.particlTestNet: - return ParticlWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - client: client, - cachedClient: cachedClient, - tracker: tracker); - case Coin.wownero: return WowneroWallet( walletId: walletId, diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 77d0fa7db..5aaedfd23 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -46,7 +46,7 @@ const int MINIMUM_CONFIRMATIONS = 1; const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = - "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"; + "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"; const String GENESIS_HASH_TESTNET = "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; @@ -89,9 +89,6 @@ bip32.BIP32 getBip32NodeFromRoot( case 0x6c: // PART mainnet wif coinType = "44"; // PART mainnet break; - case 0x2e: // PART testnet wif - coinType = "1"; // PART testnet - break; default: throw Exception("Invalid Particl network type used!"); } @@ -153,8 +150,6 @@ class ParticlWallet extends CoinServiceAPI { switch (coin) { case Coin.particl: return particl; - case Coin.particlTestNet: - return particltestnet; default: throw Exception("Invalid network type!"); } @@ -352,10 +347,6 @@ class ParticlWallet extends CoinServiceAPI { throw Exception("genesis hash does not match main net!"); } break; - case Coin.particlTestNet: - if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - throw Exception("genesis hash does not match test net!"); - } break; default: throw Exception( @@ -1474,11 +1465,6 @@ class ParticlWallet extends CoinServiceAPI { throw Exception("genesis hash does not match main net!"); } break; - case Coin.particlTestNet: - if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - throw Exception("genesis hash does not match test net!"); - } - break; default: throw Exception( "Attempted to generate a ParticlWallet using a non particl coin type: ${coin.name}"); @@ -3803,11 +3789,3 @@ final particl = NetworkType( pubKeyHash: 0x38, scriptHash: 0x3c, wif: 0x6c); - -final particltestnet = NetworkType( - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'tpw', - bip32: Bip32Type(public: 0xe1427800, private: 0x04889478), - pubKeyHash: 0x76, - scriptHash: 0x7a, - wif: 0x2e); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 376471c45..a6cbb8b58 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -74,8 +74,6 @@ class AddressUtils { return Address.validateAddress(address, firoTestNetwork); case Coin.dogecoinTestNet: return Address.validateAddress(address, dogecointestnet); - case Coin.particlTestNet: - return Address.validateAddress(address, particltestnet); } } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 74510418c..5e7e15f0b 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -228,8 +228,6 @@ class _SVG { return firoTestnet; case Coin.dogecoinTestNet: return dogecoinTestnet; - case Coin.particlTestNet: - return particlTestnet; } } } @@ -281,7 +279,6 @@ class _PNG { case Coin.namecoin: return namecoin; case Coin.particl: - case Coin.particlTestNet: return particl; } } diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index d259524d5..4b406b704 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -37,7 +37,5 @@ Uri getBlockExplorerTransactionUrlFor({ return Uri.parse("https://chainz.cryptoid.info/nmc/tx.dws?$txid.htm"); case Coin.particl: return Uri.parse("https://chainz.cryptoid.info/part/tx.dws?$txid.htm"); - case Coin.particlTestNet: - return Uri.parse(""); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index a6fc902d5..25105d011 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -104,7 +104,6 @@ abstract class Constants { case Coin.epicCash: case Coin.namecoin: case Coin.particl: - case Coin.particlTestNet: values.addAll([24, 21, 18, 15, 12]); break; @@ -154,7 +153,6 @@ abstract class Constants { return 600; case Coin.particl: - case Coin.particlTestNet: return 600; } } diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 1cbb4e07e..bab113cde 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -146,8 +146,8 @@ abstract class DefaultNodes { ); static NodeModel get particl => NodeModel( - host: "host", - port: 123, + host: "164.92.93.20", + port: 50002, name: defaultName, id: _nodeId(Coin.particl), useSSL: true, @@ -204,18 +204,6 @@ abstract class DefaultNodes { isDown: false, ); - static NodeModel get particlTestnet => NodeModel( - host: "host", - port: 60002, - name: defaultName, - id: _nodeId(Coin.particlTestNet), - useSSL: true, - enabled: true, - coinName: Coin.particlTestNet.name, - isFailover: true, - isDown: false, - ); - static NodeModel getNodeFor(Coin coin) { switch (coin) { case Coin.bitcoin: @@ -262,9 +250,6 @@ abstract class DefaultNodes { case Coin.dogecoinTestNet: return dogecoinTestnet; - - case Coin.particlTestNet: - return particlTestnet; } } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 4e45342bf..8c0c20f6e 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -38,7 +38,6 @@ enum Coin { bitcoincashTestnet, dogecoinTestNet, firoTestNet, - particlTestNet } final int kTestNetCoinCount = Util.isDesktop ? 5 : 4; @@ -76,8 +75,6 @@ extension CoinExt on Coin { return "tFiro"; case Coin.dogecoinTestNet: return "tDogecoin"; - case Coin.particlTestNet: - return "tParticl"; } } @@ -113,8 +110,6 @@ extension CoinExt on Coin { return "tFIRO"; case Coin.dogecoinTestNet: return "tDOGE"; - case Coin.particlTestNet: - return "tPART"; } } @@ -151,8 +146,6 @@ extension CoinExt on Coin { return "firo"; case Coin.dogecoinTestNet: return "dogecoin"; - case Coin.particlTestNet: - return "particl"; } } @@ -170,7 +163,6 @@ extension CoinExt on Coin { case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: - case Coin.particlTestNet: return true; case Coin.epicCash: @@ -209,7 +201,6 @@ extension CoinExt on Coin { return xmr.MINIMUM_CONFIRMATIONS; case Coin.particl: - case Coin.particlTestNet: return particl.MINIMUM_CONFIRMATIONS; case Coin.wownero: @@ -254,7 +245,6 @@ Coin coinFromPrettyName(String name) { case "Particl": case "particl": - case "particlTestNet": return Coin.particl; case "Namecoin": @@ -332,8 +322,6 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.firoTestNet; case "tdoge": return Coin.dogecoinTestNet; - case "tparticl": - return Coin.particlTestNet; case "wow": return Coin.wownero; default: diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 8a73e7010..8de0954e8 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -221,7 +221,6 @@ class CoinThemeColor { case Coin.wownero: return wownero; case Coin.particl: - case Coin.particlTestNet: return particl; } } diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 66ce107f1..9764128e4 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -1444,7 +1444,6 @@ class StackColors extends ThemeExtension { case Coin.wownero: return _coin.wownero; case Coin.particl: - case Coin.particlTestNet: return _coin.particl; } } From 67764778973ae1b588d44012d0ddcbd29b01d2ca Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 11:25:39 -0600 Subject: [PATCH 020/103] WIP particl tests see out of band particl_wallet_test_parameters.dart --- .gitignore | 6 +- .../particl/particl_history_sample_data.dart | 188 ++ .../particl_transaction_data_samples.dart | 357 ++++ .../particl/particl_utxo_sample_data.dart | 60 + .../coins/particl/particl_wallet_test.dart | 1743 +++++++++++++++++ .../particl/particl_wallet_test.mocks.dart | 629 ++++++ 6 files changed, 2980 insertions(+), 3 deletions(-) create mode 100644 test/services/coins/particl/particl_history_sample_data.dart create mode 100644 test/services/coins/particl/particl_transaction_data_samples.dart create mode 100644 test/services/coins/particl/particl_utxo_sample_data.dart create mode 100644 test/services/coins/particl/particl_wallet_test.dart create mode 100644 test/services/coins/particl/particl_wallet_test.mocks.dart diff --git a/.gitignore b/.gitignore index 323aac218..d456eb159 100644 --- a/.gitignore +++ b/.gitignore @@ -37,8 +37,10 @@ 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/namecoin/namecoin_wallet_test_parameters.dart # Legacy +test/services/coins/namecoin/namecoin_wallet_test_parameters.txt # Legacy test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart +test/services/coins/particl/particl_wallet_test_parameters.dart /integration_test/private.dart # Exceptions to above rules. @@ -48,5 +50,3 @@ test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart coverage scripts/**/build /lib/external_api_keys.dart -/test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart -/test/services/coins/namecoin/namecoin_wallet_test_parameters.dart.txt diff --git a/test/services/coins/particl/particl_history_sample_data.dart b/test/services/coins/particl/particl_history_sample_data.dart new file mode 100644 index 000000000..53a082778 --- /dev/null +++ b/test/services/coins/particl/particl_history_sample_data.dart @@ -0,0 +1,188 @@ +// TODO these test vectors are valid for Namecoin: update for Particl + +final Map> historyBatchArgs0 = { + "k_0_0": ["d17132f41b2d55c730db5b27db721020abbd4a5087c15edcccbaa106eef8cbf3"], + "k_0_1": ["cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9"], + "k_0_2": ["82a12031d679c9dd3124047742dc22c2c7c03afa9644bddf55d4c95da41bca1c"], + "k_0_3": ["bbe10c5d3c102fd805770ed2d6c5438dce42c04d3f87e3260056d04245b17ddd"], + "k_0_4": ["d9ca5255516f963d8f348911451e2c69489a70dec7f34a4810ee8b0e32fcb04d"], + "k_0_5": ["2284461fd01b17e7443775e39b19f4378a063ff148938d2e4191cea3fd80368d"], + "k_0_6": ["cd3c32fddbf265410c34a58fefcc849b02fc16978d75e501f88f9effcbecd8fe"], + "k_0_7": ["a3bcc0c3c4a140fbcc4c4f4dff18790d8a2d5f868821f47460f68f0426291b57"], + "k_0_8": ["e400f9431798c87ea35ea19b265d9e56a73fd44c239957d9947ae79e16718fb4"], + "k_0_9": ["1fe8bb16b49725bf3703274e205a4695c398e664284cc68d92d15087a54da299"], + "k_0_10": [ + "2fabf8d61308c8b2d914489a9f02f669ed9fa68047666815cf1f3cd1bb5d8819" + ], + "k_0_11": ["42a567d344189430afe7d45d6854ef6e9d256d9ef4186afd31a1a5ff90a6a0dd"] +}; +final Map> historyBatchArgs1 = { + "k_0_0": ["bcf7aec7c10dfba33ce80149300a7c4fe66460c1dd05503b5df5780884498186"], + "k_0_1": ["587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e"], + "k_0_2": ["fe6ad514f7427782f964b25995a90a3233589904b88f66a2d0e73e2560c9af7c"], + "k_0_3": ["6b962c5f9b4cfc004c74c5ab849304c405b02fc0e2f34ee17c185984f13c9da4"], + "k_0_4": ["720b79fab9a163ce6534828e8a673c5bf600161eba92c2b81555e79add59994c"], + "k_0_5": ["a10f4cf239abd4bcdb03dbe40b5c1d57ae3a7982adf8f177d481feb0ad3a52cd"], + "k_0_6": ["061f28e17ba1a56404b08a5899163011c7d6317e534ccd8e4d38911574f574b0"], + "k_0_7": ["ffc6297d487a13cb80689c448a3aef16cbd367a503d236d0aebd7218cc568e88"], + "k_0_8": ["f4a6c41fc432300509f97ca68db3b9d802d29f90c35a429e3886c480cdce44a2"], + "k_0_9": ["52f3bf96d02cd7e8c631b8ef36262994a3ec658427b930197ed004c8599cd7fd"], + "k_0_10": [ + "7993aef51bebe79bae2d29b775c499f390e99fdb6b9accb8034f657b6e72927a" + ], + "k_0_11": ["430214c9805d90c6a8c4489187db08157a93e60509e96b518dc8b5ba3d567206"] +}; +final Map> historyBatchArgs2 = { + "k_0_0": ["afe5085dd514032810d5b266007557ba8a0f4bee84535cb10754c6d46ab8689b"], + "k_0_1": ["dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c"], + "k_0_2": ["e65d4274e8edc5cc1e7b676652e2e13b0b00648d15cf13caa982ecd6a67731ba"], + "k_0_3": ["6c69ca274f7d7f2fae882a87bcee93d9429328995c5bc6b326b548b4cefcaa9f"], + "k_0_4": ["86f1a5e17dc42c27cdb0dff8a41c2434575ab05ed2f3689fd7b674677e5ea446"], + "k_0_5": ["a5d9b8df5b80c56e6053497a8c89a37267010926e80e0d225a019b78673a7aa7"], + "k_0_6": ["a0030024518874720b82b38d965fb5b3083d9f42fab40e6be4797c789eeb06f2"], + "k_0_7": ["f20077f7c6a6b92a1f75bbbad8dbece9ae4609cfdfc85e40ccac7d463bdfd6e0"], + "k_0_8": ["07b7bb4020c377e0741587efe9c0b3931e2e45f667bc6f1fa81a8f15fbe86ce4"], + "k_0_9": ["ca0322fc293f6e4d8c8adac178ed4aaedbd9acd2ec84acaaf1529f9ab7bda6d2"], + "k_0_10": [ + "06df1d13aa43375775d7d2838595a0c4c642f8af15b06a99d5115d9236e9a79e" + ], + "k_0_11": ["1a146c5a8dd5bf49faca3c6f65c9d955d361c6c00893c48d69cf7ff42c7b387b"] +}; + +final Map> historyBatchArgs3 = { + "k_0_0": ["5c2c77a3671417c5831c336805770344b81e6c7ef0d873c986ba65a7bacd5f68"], + "k_0_1": ["c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b"], + "k_0_2": ["f430c440e90c48b9e4c7e5356083e7c1495b7cad53f39ebba64cca9fb3d05c82"], + "k_0_3": ["30a7ac6789383f7f6def9a927f3b6fb661cf9406fec71a1d118c7d86052382fb"], + "k_0_4": ["a797225a9155417ab18e16b9d7ce9bf4962ae5c05df572a33c60b36a0523f257"], + "k_0_5": ["24d1e3ac9e53727d943688e67eb5c000d993e9c3cf9585d630624197fb73bed3"], + "k_0_6": ["d667a44404519649cb65632d6a3be948a1f0971025c96cb4211943d301fe0d3e"], + "k_0_7": ["be8da400f004546b528fb989c14a88324b8b0c2d5680cf080ae1e1dac4401f68"], + "k_0_8": ["addfa7682c0a2461ab0e82b3c9302b38986b442a1a76c3c839b6c2f0eaa805fe"], + "k_0_9": ["98bb3aab55f4f305fd9994334b8dd3321eda50b25fad2ef3e636714b650d0bb0"], + "k_0_10": [ + "bee1eee20d7169d03ce68d340a17f4598f589920513ec19c458db45399639a9f" + ], + "k_0_11": ["928a988dd65d100d1677a0478abfcd4d2a70aabb0812c58a2b1b4b51c395ed54"] +}; + +final Map> historyBatchArgs4 = { + "k_0_0": ["6bbfd9c1c28d6984646db4736196f67f2d1075894bb1d8990294ca7d663bece6"], + "k_0_1": ["42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3"], + "k_0_2": ["191c977174dc50a57628aea6684c428d3a5e90bbe16c4e412be51b0cfc589d38"], + "k_0_3": ["0daaf61564fd07a25ef106d958216992896f931f5bed4fbf56cc3f94443dc164"], + "k_0_4": ["ac5aca40fed2903def31c9ef1d60874247cdcc5b85238c7a1d83c67d2924d6b9"], + "k_0_5": ["c4102ff0556d863b4bab9d8232fe1f0c0fde4b6e4fe23064b4ecd0958f9726cc"], + "k_0_6": ["1c4bd1554e4992e5914dcd8f3e13927ffd46302dfdcbd2dca0cfd47c040c4256"], + "k_0_7": ["eaf5562ebef7cafa58e2c1fc4ae023e5ae8dd71ee637b08c4bc7e274e401a9a4"], + "k_0_8": ["06f7f55c221fee1b36284b5360155b8380cb9d7172b7e28eb37c61b7ebb6f227"], + "k_0_9": ["7e7ca801131ec1c5797f2c4aa46908ee50e9958cf1cbf53c2481d110800c3d6d"], + "k_0_10": [ + "3895e073aa034add7d2589bfdd1e54f6b9a8d7688d63fff0c3aac7950c6f9697" + ], + "k_0_11": ["ec17dd7c4fe8fbcfce94e9237d3c7ed7f5c91a45b1a060406e206df7e814b006"] +}; + +final Map> historyBatchArgs5 = { + "k_0_0": ["83b744ccb88827d544081c1a03ea782a7d00d6224ff9fddb7d0fbad399e1cae7"], + "k_0_1": ["86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874"], + "k_0_2": ["5baba32b1899d5e740838559ef39b7d8e9ba302bd24b732eeedd4c0e6ec65b51"], + "k_0_3": ["9892eb48394b0e155f63879fb89c3b068fcc071fed2e5cb11fe0729b85b53d67"], + "k_0_4": ["64192782cdaecb5e2a871a2d0fb3f541873e4750cd4e7d28e4d858ab40664a36"], + "k_0_5": ["4047ff48e96d25628acfeaec6ca75c1a668c54fd70a14414827cb59976a3b666"], + "k_0_6": ["299e8bc634ef6438c5bf99c12c2340c77c56ab974ffd767e77c17994e5cfaef8"], + "k_0_7": ["ab649fa14452563b385eb025e0b4cf2dd869c02fcdf2ec0f72725bbe2adaa3bd"], + "k_0_8": ["6be1ca4f8ee923e32137b6cdae324b841a0a60afbee4f4ae457fe31f29e001a6"], + "k_0_9": ["2a99ceea87df667135cc1801682d2c5dc7b95b7efadc48e156345ba46f4c0dc6"], + "k_0_10": [ + "9304094916a19040d3c8f10df90dae1144d1f09ac9e676e66bb76341c70388ac" + ], + "k_0_11": ["01b12fb2ea2533226471dfa863133ce390e3e13a804734e8af995a45aa7c7582"] +}; + +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 = [ + "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c", + "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e", + "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9", + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874", + "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b", + "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" +]; diff --git a/test/services/coins/particl/particl_transaction_data_samples.dart b/test/services/coins/particl/particl_transaction_data_samples.dart new file mode 100644 index 000000000..fb2a2a932 --- /dev/null +++ b/test/services/coins/particl/particl_transaction_data_samples.dart @@ -0,0 +1,357 @@ +// TODO these test vectors are valid for Namecoin: update for Particl + +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/particl/particl_utxo_sample_data.dart b/test/services/coins/particl/particl_utxo_sample_data.dart new file mode 100644 index 000000000..5a0dff492 --- /dev/null +++ b/test/services/coins/particl/particl_utxo_sample_data.dart @@ -0,0 +1,60 @@ +// TODO these test vectors are valid for Namecoin: update for Particl + +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/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart new file mode 100644 index 000000000..d867def8e --- /dev/null +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -0,0 +1,1743 @@ +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/particl/particl_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 'particl_history_sample_data.dart'; +import 'particl_transaction_data_samples.dart'; +import 'particl_utxo_sample_data.dart'; +import 'particl_wallet_test.mocks.dart'; +import 'particl_wallet_test_parameters.dart'; + +@GenerateMocks( + [ElectrumX, CachedElectrumX, PriceAPI, TransactionNotificationTracker]) +void main() { + group("particl constants", () { + test("particl minimum confirmations", () async { + expect(MINIMUM_CONFIRMATIONS, 2); + }); + test("particl dust limit", () async { + expect(DUST_LIMIT, 546); + }); + test("particl mainnet genesis block hash", () async { + expect(GENESIS_HASH_MAINNET, + "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"); + }); + test("particl testnet genesis block hash", () async { + expect(GENESIS_HASH_TESTNET, + "00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008"); + }); + }); + + test("particl 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 mainnet namecoin addresses", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late 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.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("valid mainnet legacy/p2pkh address type", () { + expect( + mainnetWallet?.addressType( + address: "N673DDbjPcrNgJmrhJ1xQXF9LLizQzvjEs"), + DerivePathType.bip44); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet bech32 p2wpkh address type", () { + expect( + mainnetWallet?.addressType( + address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v"), + DerivePathType.bip84); + 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); + }); + }); + + group("testNetworkConnection", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; + + NamecoinWallet? nmc; + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + nmc = NamecoinWallet( + walletId: "testNetworkConnection", + walletName: "testNetworkConnection", + coin: Coin.particl, + 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 nmc?.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 nmc?.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 nmc?.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 = "NMCtestWalletID"; + final testWalletName = "NMCWallet"; + + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; + + NamecoinWallet? nmc; + + setUp(() async { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + nmc = NamecoinWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("get networkType main", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get networkType test", () async { + nmc = NamecoinWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get cryptoCurrency", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get coinName", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get coinTicker", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("get and set walletName", () async { + expect(Coin.particl, Coin.particl); + nmc?.walletName = "new name"; + expect(nmc?.walletName, "new name"); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("estimateTxFee", () async { + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); + expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); + expect(nmc?.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_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)) + .thenAnswer((realInvocation) async => Decimal.ten); + + final fees = await nmc?.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 nmc?.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 nmc?.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("Particl service class functions that depend on shared storage", () { + final testWalletId = "NMCtestWalletID"; + final testWalletName = "NMCWallet"; + + bool hiveAdaptersRegistered = false; + + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; + + NamecoinWallet? nmc; + + 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(); + + nmc = NamecoinWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + // test("initializeWallet no network", () async { + // when(client?.ping()).thenAnswer((_) async => false); + // expect(await nmc?.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 nmc?.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 nmc?.initializeNew(); + final wallets = await Hive.openBox(testWalletId); + + expectLater(() => nmc?.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(() => nmc?.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( + "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 nmc?.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 nmc?.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 nmc?.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 nmc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + expect(await nmc?.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 nmc?.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.particl)) + .thenAnswer((realInvocation) async {}); + + when(client?.getBatchHistory(args: { + "0": [ + "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + when(client?.getBatchHistory(args: { + "0": [ + "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + when(client?.getBatchHistory(args: { + "0": [ + "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + final wallet = await Hive.openBox(testWalletId); + + // restore so we have something to rescan + await nmc?.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 nmc?.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(client?.getBatchHistory(args: { + "0": [ + "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + ] + })).called(2); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .called(1); + + // for (final arg in dynamicArgValues) { + // final map = Map>.from(arg as Map); + // Map argCount = {}; + // + // // verify(client?.getBatchHistory(args: map)).called(1); + // // expect(activeScriptHashes.contains(map.values.first.first as String), + // // true); + // } + + // Map argCount = {}; + // + // for (final arg in dynamicArgValues) { + // final map = Map>.from(arg as Map); + // + // final str = jsonEncode(map); + // + // if (argCount[str] == null) { + // argCount[str] = 1; + // } else { + // argCount[str] = argCount[str]! + 1; + // } + // } + // + // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); + + 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(client?.getBatchHistory(args: { + "0": [ + "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .thenAnswer((realInvocation) async {}); + + final wallet = await Hive.openBox(testWalletId); + + // restore so we have something to rescan + await nmc?.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 nmc?.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(client?.getBatchHistory(args: { + "0": [ + "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + ] + })).called(1); + verify(client?.getBatchHistory(args: { + "0": [ + "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + ] + })).called(2); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .called(1); + + expect(secureStore.writes, 19); + expect(secureStore.reads, 32); + expect(secureStore.deletes, 12); + + 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: + "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + coin: Coin.particl)) + .thenAnswer((_) async => tx2Raw); + when(cachedClient?.getTransaction( + txHash: + "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + coin: Coin.particl)) + .thenAnswer((_) async => tx3Raw); + when(cachedClient?.getTransaction( + txHash: + "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", + coin: Coin.particl, + )).thenAnswer((_) async => tx4Raw); + + // recover to fill data + await nmc?.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")); + + nmc?.outputsList = utxoList; + + bool didThrow = false; + try { + await nmc?.prepareSend( + address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v", + 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.particl, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coin: Coin.particl, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coin: Coin.particl, + // 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 nmc?.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 nmc?.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 nmc?.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 nmc + ?.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 nmc + ?.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 { + // // nmc = 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 = nmc.cachedElectrumXClient; + // // + // // // return when refresh is called on node changed trigger + // // nmc.longMutex = true; + // // + // // GlobalEventBus.instance + // // .fire(NodesChangedEvent(NodesChangedEventType.updatedCurrentNode)); + // // + // // // make sure event has processed before continuing + // // await Future.delayed(Duration(seconds: 5)); + // // + // // final b = nmc.cachedElectrumXClient; + // // + // // expect(identical(a, b), false); + // // + // // await nmc.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 nmc?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + nmc?.refreshMutex = true; + + await nmc?.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.particl: 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 nmc?.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 nmc?.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/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart new file mode 100644 index 000000000..91c3e5bfa --- /dev/null +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -0,0 +1,629 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in stackwallet/test/services/coins/namecoin/namecoin_wallet_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +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 +// ignore_for_file: subtype_of_sealed_class + +class _FakeDecimal_0 extends _i1.SmartFake implements _i2.Decimal { + _FakeDecimal_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePrefs_1 extends _i1.SmartFake implements _i3.Prefs { + _FakePrefs_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeClient_2 extends _i1.SmartFake implements _i4.Client { + _FakeClient_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// 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({ + required 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: _i6.Future.value(), + ) as _i6.Future); + @override + _i6.Future>> batchRequest({ + required String? command, + required 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: _i6.Future>>.value( + >[]), + ) as _i6.Future>>); + @override + _i6.Future ping({ + String? requestID, + int? retryCount = 1, + }) => + (super.noSuchMethod( + Invocation.method( + #ping, + [], + { + #requestID: requestID, + #retryCount: retryCount, + }, + ), + returnValue: _i6.Future.value(false), + ) as _i6.Future); + @override + _i6.Future> getBlockHeadTip({String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getBlockHeadTip, + [], + {#requestID: requestID}, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future> getServerFeatures({String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getServerFeatures, + [], + {#requestID: requestID}, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future broadcastTransaction({ + required String? rawTx, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #broadcastTransaction, + [], + { + #rawTx: rawTx, + #requestID: requestID, + }, + ), + returnValue: _i6.Future.value(''), + ) as _i6.Future); + @override + _i6.Future> getBalance({ + required String? scripthash, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getBalance, + [], + { + #scripthash: scripthash, + #requestID: requestID, + }, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future>> getHistory({ + required String? scripthash, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getHistory, + [], + { + #scripthash: scripthash, + #requestID: requestID, + }, + ), + returnValue: _i6.Future>>.value( + >[]), + ) as _i6.Future>>); + @override + _i6.Future>>> getBatchHistory( + {required Map>? args}) => + (super.noSuchMethod( + Invocation.method( + #getBatchHistory, + [], + {#args: args}, + ), + returnValue: _i6.Future>>>.value( + >>{}), + ) as _i6.Future>>>); + @override + _i6.Future>> getUTXOs({ + required String? scripthash, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getUTXOs, + [], + { + #scripthash: scripthash, + #requestID: requestID, + }, + ), + returnValue: _i6.Future>>.value( + >[]), + ) as _i6.Future>>); + @override + _i6.Future>>> getBatchUTXOs( + {required Map>? args}) => + (super.noSuchMethod( + Invocation.method( + #getBatchUTXOs, + [], + {#args: args}, + ), + returnValue: _i6.Future>>>.value( + >>{}), + ) as _i6.Future>>>); + @override + _i6.Future> getTransaction({ + required String? txHash, + bool? verbose = true, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getTransaction, + [], + { + #txHash: txHash, + #verbose: verbose, + #requestID: requestID, + }, + ), + returnValue: + _i6.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: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future getMintData({ + dynamic mints, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getMintData, + [], + { + #mints: mints, + #requestID: requestID, + }, + ), + returnValue: _i6.Future.value(), + ) as _i6.Future); + @override + _i6.Future> getUsedCoinSerials({ + String? requestID, + required int? startNumber, + }) => + (super.noSuchMethod( + Invocation.method( + #getUsedCoinSerials, + [], + { + #requestID: requestID, + #startNumber: startNumber, + }, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future getLatestCoinId({String? requestID}) => (super.noSuchMethod( + Invocation.method( + #getLatestCoinId, + [], + {#requestID: requestID}, + ), + returnValue: _i6.Future.value(0), + ) as _i6.Future); + @override + _i6.Future> getFeeRate({String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getFeeRate, + [], + {#requestID: requestID}, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future<_i2.Decimal> estimateFee({ + String? requestID, + required int? blocks, + }) => + (super.noSuchMethod( + Invocation.method( + #estimateFee, + [], + { + #requestID: requestID, + #blocks: blocks, + }, + ), + returnValue: _i6.Future<_i2.Decimal>.value(_FakeDecimal_0( + this, + Invocation.method( + #estimateFee, + [], + { + #requestID: requestID, + #blocks: blocks, + }, + ), + )), + ) as _i6.Future<_i2.Decimal>); + @override + _i6.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + Invocation.method( + #relayFee, + [], + {#requestID: requestID}, + ), + returnValue: _i6.Future<_i2.Decimal>.value(_FakeDecimal_0( + this, + Invocation.method( + #relayFee, + [], + {#requestID: requestID}, + ), + )), + ) 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( + this, + Invocation.getter(#prefs), + ), + ) 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({ + required String? groupId, + String? blockhash = r'', + required _i8.Coin? coin, + }) => + (super.noSuchMethod( + Invocation.method( + #getAnonymitySet, + [], + { + #groupId: groupId, + #blockhash: blockhash, + #coin: coin, + }, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override + _i6.Future> getTransaction({ + required String? txHash, + required _i8.Coin? coin, + bool? verbose = true, + }) => + (super.noSuchMethod( + Invocation.method( + #getTransaction, + [], + { + #txHash: txHash, + #coin: coin, + #verbose: verbose, + }, + ), + returnValue: + _i6.Future>.value({}), + ) as _i6.Future>); + @override + _i6.Future> getUsedCoinSerials({ + required _i8.Coin? coin, + int? startNumber = 0, + }) => + (super.noSuchMethod( + Invocation.method( + #getUsedCoinSerials, + [], + { + #coin: coin, + #startNumber: startNumber, + }, + ), + returnValue: _i6.Future>.value([]), + ) as _i6.Future>); + @override + _i6.Future clearSharedTransactionCache({required _i8.Coin? coin}) => + (super.noSuchMethod( + Invocation.method( + #clearSharedTransactionCache, + [], + {#coin: coin}, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.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( + this, + Invocation.getter(#client), + ), + ) as _i4.Client); + @override + void resetLastCalledToForceNextCallToUpdateCache() => super.noSuchMethod( + Invocation.method( + #resetLastCalledToForceNextCallToUpdateCache, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i6.Future< + Map<_i8.Coin, _i10.Tuple2<_i2.Decimal, double>>> getPricesAnd24hChange( + {required String? baseCurrency}) => + (super.noSuchMethod( + Invocation.method( + #getPricesAnd24hChange, + [], + {#baseCurrency: baseCurrency}, + ), + returnValue: + _i6.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: _i6.Future.value(), + returnValueForMissingStub: _i6.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: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); +} From 7a0cb3669ace7756eac59ab93313c361369736e3 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 12:54:29 -0600 Subject: [PATCH 021/103] update genesis hash test vector --- test/services/coins/particl/particl_wallet_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index d867def8e..2a85e7c2b 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -35,7 +35,7 @@ void main() { }); test("particl mainnet genesis block hash", () async { expect(GENESIS_HASH_MAINNET, - "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"); + "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"); // Was 000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770 }); test("particl testnet genesis block hash", () async { expect(GENESIS_HASH_TESTNET, From 1d7147d3300166e4fbbb1077f2fa150e526954f4 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 12:54:54 -0600 Subject: [PATCH 022/103] update secure storage interface --- lib/services/coins/particl/particl_wallet.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 5aaedfd23..f35c13049 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -1298,7 +1298,7 @@ class ParticlWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1310,7 +1310,7 @@ class ParticlWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + SecureStorageInterface? secureStore, }) { txTracker = tracker; _walletId = walletId; From e82a4d95fa65fc45ab1f14f7398489b91a413da4 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 12:55:24 -0600 Subject: [PATCH 023/103] update bip32 root test TODO update ROOT_WIF ... is that in the .gitignored wallet test parameters? --- test/services/coins/particl/particl_wallet_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 2a85e7c2b..4c155bf6a 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -51,12 +51,12 @@ void main() { group("bip32 node/root", () { test("getBip32Root", () { - final root = getBip32Root(TEST_MNEMONIC, namecoin); + final root = getBip32Root(TEST_MNEMONIC, particl); expect(root.toWIF(), ROOT_WIF); }); // test("getBip32NodeFromRoot", () { - // final root = getBip32Root(TEST_MNEMONIC, namecoin); + // final root = getBip32Root(TEST_MNEMONIC, particl); // // two mainnet // final node44 = getBip32NodeFromRoot(0, 0, root, DerivePathType.bip44); // expect(node44.toWIF(), NODE_WIF_44); From 485c6d55e5a39223e5de22b2d3129b869e2393f0 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 12:57:11 -0600 Subject: [PATCH 024/103] add in old .gitignore in case anyone has an old .dart.txt laying around might as well make sure the old gitignored file is still gitginored ... although it isn't referenced, is it? could probably cut this out --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d456eb159..e4bc4a75a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +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 # Legacy +test/services/coins/namecoin/namecoin_wallet_test_parameters.dart +test/services/coins/namecoin/namecoin_wallet_test_parameters.dart.txt # Legacy test/services/coins/namecoin/namecoin_wallet_test_parameters.txt # Legacy test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart test/services/coins/particl/particl_wallet_test_parameters.dart From fa03c5de74d249ca9e4820e555b9b95f30639e51 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 12:57:50 -0600 Subject: [PATCH 025/103] disable all but 2-3 particl tests TODO re-enable as we implement up to these --- .../coins/particl/particl_wallet_test.dart | 3136 ++++++++--------- 1 file changed, 1568 insertions(+), 1568 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 4c155bf6a..7fcd880a1 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -37,10 +37,10 @@ void main() { expect(GENESIS_HASH_MAINNET, "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"); // Was 000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770 }); - test("particl testnet genesis block hash", () async { - expect(GENESIS_HASH_TESTNET, - "00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008"); - }); + // test("particl testnet genesis block hash", () async { + // expect(GENESIS_HASH_TESTNET, + // "00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008"); + // }); }); test("particl DerivePathType enum", () { @@ -176,1568 +176,1568 @@ void main() { }); }); - group("testNetworkConnection", () { - MockElectrumX? client; - MockCachedElectrumX? cachedClient; - MockPriceAPI? priceAPI; - late FakeSecureStorage secureStore; - MockTransactionNotificationTracker? tracker; - - NamecoinWallet? nmc; - - setUp(() { - client = MockElectrumX(); - cachedClient = MockCachedElectrumX(); - priceAPI = MockPriceAPI(); - secureStore = FakeSecureStorage(); - tracker = MockTransactionNotificationTracker(); - - nmc = NamecoinWallet( - walletId: "testNetworkConnection", - walletName: "testNetworkConnection", - coin: Coin.particl, - 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 nmc?.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 nmc?.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 nmc?.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 = "NMCtestWalletID"; - final testWalletName = "NMCWallet"; - - MockElectrumX? client; - MockCachedElectrumX? cachedClient; - MockPriceAPI? priceAPI; - late FakeSecureStorage secureStore; - MockTransactionNotificationTracker? tracker; - - NamecoinWallet? nmc; - - setUp(() async { - client = MockElectrumX(); - cachedClient = MockCachedElectrumX(); - priceAPI = MockPriceAPI(); - secureStore = FakeSecureStorage(); - tracker = MockTransactionNotificationTracker(); - - nmc = NamecoinWallet( - walletId: testWalletId, - walletName: testWalletName, - coin: Coin.particl, - client: client!, - cachedClient: cachedClient!, - tracker: tracker!, - priceAPI: priceAPI, - secureStore: secureStore, - ); - }); - - test("get networkType main", () async { - expect(Coin.particl, Coin.particl); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("get networkType test", () async { - nmc = NamecoinWallet( - walletId: testWalletId, - walletName: testWalletName, - coin: Coin.particl, - client: client!, - cachedClient: cachedClient!, - tracker: tracker!, - priceAPI: priceAPI, - secureStore: secureStore, - ); - expect(Coin.particl, Coin.particl); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("get cryptoCurrency", () async { - expect(Coin.particl, Coin.particl); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("get coinName", () async { - expect(Coin.particl, Coin.particl); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("get coinTicker", () async { - expect(Coin.particl, Coin.particl); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("get and set walletName", () async { - expect(Coin.particl, Coin.particl); - nmc?.walletName = "new name"; - expect(nmc?.walletName, "new name"); - expect(secureStore.interactions, 0); - verifyNoMoreInteractions(client); - verifyNoMoreInteractions(cachedClient); - verifyNoMoreInteractions(priceAPI); - }); - - test("estimateTxFee", () async { - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); - expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); - expect(nmc?.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_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)) - .thenAnswer((realInvocation) async => Decimal.ten); - - final fees = await nmc?.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 nmc?.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 nmc?.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("Particl service class functions that depend on shared storage", () { - final testWalletId = "NMCtestWalletID"; - final testWalletName = "NMCWallet"; - - bool hiveAdaptersRegistered = false; - - MockElectrumX? client; - MockCachedElectrumX? cachedClient; - MockPriceAPI? priceAPI; - late FakeSecureStorage secureStore; - MockTransactionNotificationTracker? tracker; - - NamecoinWallet? nmc; - - 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(); - - nmc = NamecoinWallet( - walletId: testWalletId, - walletName: testWalletName, - coin: Coin.particl, - client: client!, - cachedClient: cachedClient!, - tracker: tracker!, - priceAPI: priceAPI, - secureStore: secureStore, - ); - }); - - // test("initializeWallet no network", () async { - // when(client?.ping()).thenAnswer((_) async => false); - // expect(await nmc?.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 nmc?.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 nmc?.initializeNew(); - final wallets = await Hive.openBox(testWalletId); - - expectLater(() => nmc?.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(() => nmc?.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( - "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 nmc?.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 nmc?.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 nmc?.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 nmc?.recoverFromMnemonic( - mnemonic: TEST_MNEMONIC, - maxUnusedAddressGap: 2, - maxNumberOfIndexesToCheck: 1000, - height: 4000); - - expect(await nmc?.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 nmc?.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.particl)) - .thenAnswer((realInvocation) async {}); - - when(client?.getBatchHistory(args: { - "0": [ - "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - when(client?.getBatchHistory(args: { - "0": [ - "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - when(client?.getBatchHistory(args: { - "0": [ - "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - final wallet = await Hive.openBox(testWalletId); - - // restore so we have something to rescan - await nmc?.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 nmc?.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(client?.getBatchHistory(args: { - "0": [ - "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - ] - })).called(2); - - verify(client?.getBatchHistory(args: { - "0": [ - "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - ] - })).called(2); - - verify(client?.getBatchHistory(args: { - "0": [ - "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - ] - })).called(2); - - verify(client?.getBatchHistory(args: { - "0": [ - "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - ] - })).called(2); - - verify(client?.getBatchHistory(args: { - "0": [ - "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - ] - })).called(2); - verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - .called(1); - - // for (final arg in dynamicArgValues) { - // final map = Map>.from(arg as Map); - // Map argCount = {}; - // - // // verify(client?.getBatchHistory(args: map)).called(1); - // // expect(activeScriptHashes.contains(map.values.first.first as String), - // // true); - // } - - // Map argCount = {}; - // - // for (final arg in dynamicArgValues) { - // final map = Map>.from(arg as Map); - // - // final str = jsonEncode(map); - // - // if (argCount[str] == null) { - // argCount[str] = 1; - // } else { - // argCount[str] = argCount[str]! + 1; - // } - // } - // - // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); - - 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(client?.getBatchHistory(args: { - "0": [ - "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(client?.getBatchHistory(args: { - "0": [ - "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - ] - })).thenAnswer((realInvocation) async => {"0": []}); - - when(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - .thenAnswer((realInvocation) async {}); - - final wallet = await Hive.openBox(testWalletId); - - // restore so we have something to rescan - await nmc?.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 nmc?.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(client?.getBatchHistory(args: { - "0": [ - "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - ] - })).called(1); - verify(client?.getBatchHistory(args: { - "0": [ - "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - ] - })).called(2); - verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - .called(1); - - expect(secureStore.writes, 19); - expect(secureStore.reads, 32); - expect(secureStore.deletes, 12); - - 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: - "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", - coin: Coin.particl)) - .thenAnswer((_) async => tx2Raw); - when(cachedClient?.getTransaction( - txHash: - "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", - coin: Coin.particl)) - .thenAnswer((_) async => tx3Raw); - when(cachedClient?.getTransaction( - txHash: - "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", - coin: Coin.particl, - )).thenAnswer((_) async => tx4Raw); - - // recover to fill data - await nmc?.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")); - - nmc?.outputsList = utxoList; - - bool didThrow = false; - try { - await nmc?.prepareSend( - address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v", - 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.particl, - // callOutSideMainIsolate: false)) - // .called(1); - // verify(cachedClient?.getTransaction( - // txHash: - // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", - // coin: Coin.particl, - // callOutSideMainIsolate: false)) - // .called(1); - // verify(cachedClient?.getTransaction( - // txHash: - // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", - // coin: Coin.particl, - // 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 nmc?.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 nmc?.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 nmc?.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 nmc - ?.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 nmc - ?.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 { - // // nmc = 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 = nmc.cachedElectrumXClient; - // // - // // // return when refresh is called on node changed trigger - // // nmc.longMutex = true; - // // - // // GlobalEventBus.instance - // // .fire(NodesChangedEvent(NodesChangedEventType.updatedCurrentNode)); - // // - // // // make sure event has processed before continuing - // // await Future.delayed(Duration(seconds: 5)); - // // - // // final b = nmc.cachedElectrumXClient; - // // - // // expect(identical(a, b), false); - // // - // // await nmc.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 nmc?.recoverFromMnemonic( - mnemonic: TEST_MNEMONIC, - maxUnusedAddressGap: 2, - maxNumberOfIndexesToCheck: 1000, - height: 4000); - - nmc?.refreshMutex = true; - - await nmc?.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.particl: 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 nmc?.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 nmc?.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(); - }); - }); + // group("testNetworkConnection", () { + // MockElectrumX? client; + // MockCachedElectrumX? cachedClient; + // MockPriceAPI? priceAPI; + // late FakeSecureStorage secureStore; + // MockTransactionNotificationTracker? tracker; + + // NamecoinWallet? nmc; + + // setUp(() { + // client = MockElectrumX(); + // cachedClient = MockCachedElectrumX(); + // priceAPI = MockPriceAPI(); + // secureStore = FakeSecureStorage(); + // tracker = MockTransactionNotificationTracker(); + + // nmc = NamecoinWallet( + // walletId: "testNetworkConnection", + // walletName: "testNetworkConnection", + // coin: Coin.particl, + // 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 nmc?.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 nmc?.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 nmc?.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 = "NMCtestWalletID"; + // final testWalletName = "NMCWallet"; + + // MockElectrumX? client; + // MockCachedElectrumX? cachedClient; + // MockPriceAPI? priceAPI; + // late FakeSecureStorage secureStore; + // MockTransactionNotificationTracker? tracker; + + // NamecoinWallet? nmc; + + // setUp(() async { + // client = MockElectrumX(); + // cachedClient = MockCachedElectrumX(); + // priceAPI = MockPriceAPI(); + // secureStore = FakeSecureStorage(); + // tracker = MockTransactionNotificationTracker(); + + // nmc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.particl, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // }); + + // test("get networkType main", () async { + // expect(Coin.particl, Coin.particl); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get networkType test", () async { + // nmc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.particl, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // expect(Coin.particl, Coin.particl); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get cryptoCurrency", () async { + // expect(Coin.particl, Coin.particl); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get coinName", () async { + // expect(Coin.particl, Coin.particl); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get coinTicker", () async { + // expect(Coin.particl, Coin.particl); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("get and set walletName", () async { + // expect(Coin.particl, Coin.particl); + // nmc?.walletName = "new name"; + // expect(nmc?.walletName, "new name"); + // expect(secureStore.interactions, 0); + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); + + // test("estimateTxFee", () async { + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); + // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); + // expect(nmc?.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_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)) + // .thenAnswer((realInvocation) async => Decimal.ten); + + // final fees = await nmc?.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 nmc?.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 nmc?.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("Particl service class functions that depend on shared storage", () { + // final testWalletId = "NMCtestWalletID"; + // final testWalletName = "NMCWallet"; + + // bool hiveAdaptersRegistered = false; + + // MockElectrumX? client; + // MockCachedElectrumX? cachedClient; + // MockPriceAPI? priceAPI; + // late FakeSecureStorage secureStore; + // MockTransactionNotificationTracker? tracker; + + // NamecoinWallet? nmc; + + // 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(); + + // nmc = NamecoinWallet( + // walletId: testWalletId, + // walletName: testWalletName, + // coin: Coin.particl, + // client: client!, + // cachedClient: cachedClient!, + // tracker: tracker!, + // priceAPI: priceAPI, + // secureStore: secureStore, + // ); + // }); + + // // test("initializeWallet no network", () async { + // // when(client?.ping()).thenAnswer((_) async => false); + // // expect(await nmc?.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 nmc?.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 nmc?.initializeNew(); + // final wallets = await Hive.openBox(testWalletId); + + // expectLater(() => nmc?.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(() => nmc?.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( + // "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 nmc?.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 nmc?.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 nmc?.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 nmc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000, + // height: 4000); + + // expect(await nmc?.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 nmc?.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.particl)) + // .thenAnswer((realInvocation) async {}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // when(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // when(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // final wallet = await Hive.openBox(testWalletId); + + // // restore so we have something to rescan + // await nmc?.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 nmc?.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(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).called(2); + + // verify(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).called(2); + + // verify(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).called(2); + + // verify(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).called(2); + + // verify(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).called(2); + // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + // .called(1); + + // // for (final arg in dynamicArgValues) { + // // final map = Map>.from(arg as Map); + // // Map argCount = {}; + // // + // // // verify(client?.getBatchHistory(args: map)).called(1); + // // // expect(activeScriptHashes.contains(map.values.first.first as String), + // // // true); + // // } + + // // Map argCount = {}; + // // + // // for (final arg in dynamicArgValues) { + // // final map = Map>.from(arg as Map); + // // + // // final str = jsonEncode(map); + // // + // // if (argCount[str] == null) { + // // argCount[str] = 1; + // // } else { + // // argCount[str] = argCount[str]! + 1; + // // } + // // } + // // + // // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); + + // 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(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + + // when(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + // .thenAnswer((realInvocation) async {}); + + // final wallet = await Hive.openBox(testWalletId); + + // // restore so we have something to rescan + // await nmc?.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 nmc?.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(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).called(1); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).called(2); + // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + // .called(1); + + // expect(secureStore.writes, 19); + // expect(secureStore.reads, 32); + // expect(secureStore.deletes, 12); + + // 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: + // "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + // coin: Coin.particl)) + // .thenAnswer((_) async => tx2Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + // coin: Coin.particl)) + // .thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", + // coin: Coin.particl, + // )).thenAnswer((_) async => tx4Raw); + + // // recover to fill data + // await nmc?.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")); + + // nmc?.outputsList = utxoList; + + // bool didThrow = false; + // try { + // await nmc?.prepareSend( + // address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v", + // 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.particl, + // // callOutSideMainIsolate: false)) + // // .called(1); + // // verify(cachedClient?.getTransaction( + // // txHash: + // // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // // coin: Coin.particl, + // // callOutSideMainIsolate: false)) + // // .called(1); + // // verify(cachedClient?.getTransaction( + // // txHash: + // // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // // coin: Coin.particl, + // // 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 nmc?.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 nmc?.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 nmc?.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 nmc + // ?.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 nmc + // ?.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 { + // // // nmc = 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 = nmc.cachedElectrumXClient; + // // // + // // // // return when refresh is called on node changed trigger + // // // nmc.longMutex = true; + // // // + // // // GlobalEventBus.instance + // // // .fire(NodesChangedEvent(NodesChangedEventType.updatedCurrentNode)); + // // // + // // // // make sure event has processed before continuing + // // // await Future.delayed(Duration(seconds: 5)); + // // // + // // // final b = nmc.cachedElectrumXClient; + // // // + // // // expect(identical(a, b), false); + // // // + // // // await nmc.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 nmc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000, + // height: 4000); + + // nmc?.refreshMutex = true; + + // await nmc?.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.particl: 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 nmc?.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 nmc?.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(); + // }); + // }); } From a52f2325108033f1bf5accbd2ad3cf85fa29840e Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 13:40:51 -0600 Subject: [PATCH 026/103] pass secureStorageInterface to coin service --- lib/services/coins/coin_service.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index c9c6c0138..41dbed42b 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -187,6 +187,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker); From 245dba515fb8716ceb949acb0cf8ff3fdd5195d8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 13:41:01 -0600 Subject: [PATCH 027/103] add particl constants --- lib/utilities/constants.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 25105d011..c6fe81d74 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -54,6 +54,7 @@ abstract class Constants { case Coin.firoTestNet: case Coin.epicCash: case Coin.namecoin: + case Coin.particl: return _satsPerCoin; case Coin.wownero: @@ -78,6 +79,7 @@ abstract class Constants { case Coin.firoTestNet: case Coin.epicCash: case Coin.namecoin: + case Coin.particl: return _decimalPlaces; case Coin.wownero: From 6c9690dcf51c7707f900db087bdd018365da088d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 13:41:11 -0600 Subject: [PATCH 028/103] Namecoin->Particl --- test/services/coins/particl/particl_wallet_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 7fcd880a1..dabaabc5c 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -106,7 +106,7 @@ void main() { late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; - NamecoinWallet? mainnetWallet; + ParticlWallet? mainnetWallet; setUp(() { client = MockElectrumX(); @@ -115,7 +115,7 @@ void main() { secureStore = FakeSecureStorage(); tracker = MockTransactionNotificationTracker(); - mainnetWallet = NamecoinWallet( + mainnetWallet = ParticlWallet( walletId: "validateAddressMainNet", walletName: "validateAddressMainNet", coin: Coin.particl, From 61f3135889d26a5fe5399890caa2c0ca27a48671 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 13:41:47 -0600 Subject: [PATCH 029/103] port recent updates from bitcoin_wallet to particl_wallet --- .../coins/particl/particl_wallet.dart | 123 +++++++++++++----- 1 file changed, 92 insertions(+), 31 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index f35c13049..2392d82ad 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -10,8 +10,8 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.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'; @@ -194,20 +194,24 @@ class ParticlWallet extends CoinServiceAPI { Future get availableBalance async { final data = await utxoData; return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed); + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + return Format.satoshisToAmount( + data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(); + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(); @override Future get totalBalance async { @@ -216,13 +220,19 @@ class ParticlWallet extends CoinServiceAPI { .get(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount( + data.satoshiBalance, + coin: coin); } else { - return Format.satoshisToAmount(totalBalance); + return Format.satoshisToAmount( + totalBalance, + coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount( + data.satoshiBalance, + coin: coin); } @override @@ -260,7 +270,8 @@ class ParticlWallet extends CoinServiceAPI { @override Future get maxFee async { final fee = (await fees).fast as String; - final satsFee = Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin); + final satsFee = + Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -1083,7 +1094,8 @@ class ParticlWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; - final balance = Format.decimalAmountToSatoshis(await availableBalance); + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1273,6 +1285,55 @@ class ParticlWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future? _transactionData; + TransactionData? cachedTxData; + + // TODO make sure this copied implementation from bitcoin_wallet.dart applies for particl just as well--or import it + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future updateSentCachedTxData(Map txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network); @@ -1310,7 +1371,7 @@ class ParticlWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - SecureStorageInterface? secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1320,13 +1381,12 @@ class ParticlWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } @override Future updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map((e) => ElectrumXNode( address: e.host, @@ -1364,7 +1424,7 @@ class ParticlWallet extends CoinServiceAPI { } Future getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore).getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( @@ -1439,9 +1499,9 @@ class ParticlWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1905,7 +1965,7 @@ class ParticlWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1914,16 +1974,17 @@ class ParticlWallet extends CoinServiceAPI { } Decimal currencyBalanceRaw = - ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: 2); + ((Decimal.fromInt(satoshiBalance) * currentPrice) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .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) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -2469,7 +2530,7 @@ class ParticlWallet extends CoinServiceAPI { if (prevOut == out["n"]) { inputAmtSentFromWallet += (Decimal.parse(out["value"]!.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2482,7 +2543,7 @@ class ParticlWallet extends CoinServiceAPI { final String address = output["scriptPubKey"]!["address"] as String; final value = output["value"]!; final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOutput += _value; @@ -2507,7 +2568,7 @@ class ParticlWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["address"]; if (address != null) { final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOut += value; @@ -2530,7 +2591,7 @@ class ParticlWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { totalIn += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2552,7 +2613,7 @@ class ParticlWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2562,7 +2623,7 @@ class ParticlWallet extends CoinServiceAPI { midSortedTx["amount"] = outputAmtAddressedToWallet; final worthNow = ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -3689,7 +3750,7 @@ class ParticlWallet extends CoinServiceAPI { @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance); + final available = Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); From 2df3b0f3ed0580d1f822add90c9c400f55af5ec4 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 14:27:15 -0600 Subject: [PATCH 030/103] update particl tests to the point of testing address validity --- .../coins/particl/particl_wallet.dart | 2 +- .../coins/particl/particl_wallet_test.dart | 104 +++++++++++++----- 2 files changed, 77 insertions(+), 29 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 2392d82ad..6c964687c 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -46,7 +46,7 @@ const int MINIMUM_CONFIRMATIONS = 1; const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = - "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"; + "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"; const String GENESIS_HASH_TESTNET = "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index dabaabc5c..fe98c87ff 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -28,19 +28,19 @@ import 'particl_wallet_test_parameters.dart'; void main() { group("particl constants", () { test("particl minimum confirmations", () async { - expect(MINIMUM_CONFIRMATIONS, 2); + expect(MINIMUM_CONFIRMATIONS, 1); }); test("particl dust limit", () async { - expect(DUST_LIMIT, 546); + expect(DUST_LIMIT, 294); }); test("particl mainnet genesis block hash", () async { expect(GENESIS_HASH_MAINNET, - "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"); // Was 000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770 + "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"); + }); + test("particl testnet genesis block hash", () async { + expect(GENESIS_HASH_TESTNET, + "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"); }); - // test("particl testnet genesis block hash", () async { - // expect(GENESIS_HASH_TESTNET, - // "00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008"); - // }); }); test("particl DerivePathType enum", () { @@ -99,14 +99,14 @@ void main() { // }); }); - group("validate mainnet namecoin addresses", () { + group("validate mainnet particl addresses", () { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; - ParticlWallet? mainnetWallet; + ParticlWallet? mainnetWallet; // TODO reimplement testnet, see 9baa30c1a40b422bb5f4746efc1220b52691ace6 and sneurlax/stack_wallet#ec399ade0aef1d9ab2dd78876a2d20819dae4ba0 setUp(() { client = MockElectrumX(); @@ -127,41 +127,89 @@ void main() { ); }); - test("valid mainnet legacy/p2pkh address type", () { + test("valid mainnet particl legacy/p2pkh address", () { + expect( + mainnetWallet?.validateAddress("Pi9W46PhXkNRusar2KVMbXftYpGzEYGcSa"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet particl legacy/p2pkh address type", () { expect( mainnetWallet?.addressType( - address: "N673DDbjPcrNgJmrhJ1xQXF9LLizQzvjEs"), + address: "Pi9W46PhXkNRusar2KVMbXftYpGzEYGcSa"), DerivePathType.bip44); - expect(secureStore.interactions, 0); + expect(secureStore?.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); verifyNoMoreInteractions(priceAPI); }); - test("valid mainnet bech32 p2wpkh address type", () { + test("valid mainnet particl p2wpkh address", () { expect( - mainnetWallet?.addressType( - address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v"), - DerivePathType.bip84); - expect(secureStore.interactions, 0); + mainnetWallet + ?.validateAddress("pw1qj6t0kvsmx8qd95pdh4rwxaz5qp5qtfz0xq2rja"), + true); + expect( + mainnetWallet + ?.validateAddress("bc1qc5ymmsay89r6gr4fy2kklvrkuvzyln4shdvjhf"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet particl legacy/p2pkh address", () { + expect( + mainnetWallet?.validateAddress("PputQYxNxMiYh3sg7vSh25wg3XxHiPHag7"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("invalid mainnet particl legacy/p2pkh address", () { + expect( + mainnetWallet?.validateAddress("PputQYxNxMiYh3sg7vSh25wg3XxHiP0000"), + false); + expect( + mainnetWallet?.validateAddress("16YB85zQHjro7fqjR2hMcwdQWCX8jNVtr5"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + test("invalid mainnet particl p2wpkh address", () { + expect( + mainnetWallet + ?.validateAddress("pw1qce3dhmmle4e0833kssj7ptta3ehydjf0tsa3ju"), + false); + 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("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( From 426217cce34024df988c5e1e3bb85a3dddf927d5 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 14:55:35 -0600 Subject: [PATCH 031/103] update bitcoindart ref to particl branch --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 8cfce39cb..d1f834241 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: bitcoindart: git: url: https://github.com/cypherstack/bitcoindart.git - ref: 65eb920719c8f7895c5402a07497647e7fc4b346 + ref: particl # TODO change to hash ref when merging in particl support stack_wallet_backup: git: From c6f799fab10e0265774753d6d8c8aa46d21f5804 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 14:57:51 -0600 Subject: [PATCH 032/103] track pubspec.lock changes --- pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index f12b25487..9cfa3ea8d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -109,7 +109,7 @@ packages: dependency: "direct main" description: path: "." - ref: "65eb920719c8f7895c5402a07497647e7fc4b346" + ref: particl resolved-ref: "65eb920719c8f7895c5402a07497647e7fc4b346" url: "https://github.com/cypherstack/bitcoindart.git" source: git From 861d362966acd41f8ba6b16539e2bbe26eb250ef Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 15:27:32 -0600 Subject: [PATCH 033/103] linting and TODO AS linted and removed unused imports --- .../coins/particl/particl_wallet_test.dart | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index fe98c87ff..27fdaa9c7 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -1,25 +1,14 @@ -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/particl/particl_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 'particl_history_sample_data.dart'; -import 'particl_transaction_data_samples.dart'; -import 'particl_utxo_sample_data.dart'; import 'particl_wallet_test.mocks.dart'; import 'particl_wallet_test_parameters.dart'; @@ -28,10 +17,11 @@ import 'particl_wallet_test_parameters.dart'; void main() { group("particl constants", () { test("particl minimum confirmations", () async { - expect(MINIMUM_CONFIRMATIONS, 1); + expect(MINIMUM_CONFIRMATIONS, + 1); // TODO confirm particl minimum confirmations }); test("particl dust limit", () async { - expect(DUST_LIMIT, 294); + expect(DUST_LIMIT, 294); // TODO confirm particl dust limit }); test("particl mainnet genesis block hash", () async { expect(GENESIS_HASH_MAINNET, @@ -46,7 +36,7 @@ void main() { test("particl DerivePathType enum", () { expect(DerivePathType.values.length, 3); expect(DerivePathType.values.toString(), - "[DerivePathType.bip44, DerivePathType.bip49, DerivePathType.bip84]"); + "[DerivePathType.bip44, DerivePathType.bip49, DerivePathType.bip84]"); // TODO does particl have BIP49, P2WPKH-P2SH? I'd think no }); group("bip32 node/root", () { @@ -106,7 +96,8 @@ void main() { late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; - ParticlWallet? mainnetWallet; // TODO reimplement testnet, see 9baa30c1a40b422bb5f4746efc1220b52691ace6 and sneurlax/stack_wallet#ec399ade0aef1d9ab2dd78876a2d20819dae4ba0 + ParticlWallet? + mainnetWallet; // TODO reimplement testnet, see 9baa30c1a40b422bb5f4746efc1220b52691ace6 and sneurlax/stack_wallet#ec399ade0aef1d9ab2dd78876a2d20819dae4ba0 setUp(() { client = MockElectrumX(); From f635f474c6b73a2abe1ccdd63e4ba6c818953d03 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 16:48:26 -0600 Subject: [PATCH 034/103] update prebuild.sh to create stub wallet test parameter files --- scripts/prebuild.sh | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh index d24076666..ef3d47ce2 100755 --- a/scripts/prebuild.sh +++ b/scripts/prebuild.sh @@ -2,5 +2,36 @@ KEYS=../lib/external_api_keys.dart if ! test -f "$KEYS"; then echo 'prebuild.sh: creating template lib/external_api_keys.dart file' - printf 'const kChangeNowApiKey = "";\nconst kSimpleSwapApiKey = "";' > $KEYS + printf 'const kChangeNowApiKey = "";\nconst kSimpleSwapApiKey = "";\n' > $KEYS +fi + +# Create template wallet test parameter files if they don't already exist +BWTP=../test/services/coins/bitcoin/bitcoin_wallet_test_parameters.dart +if ! test -f "$BWTP"; then + echo 'prebuild.sh: creating template test/services/coins/bitcoin/bitcoin_wallet_test_parameters.dart file' + printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $BWTP +fi + +BCWTP=../test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart +if ! test -f "$BCWTP"; then + echo 'prebuild.sh: creating template test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart file' + printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $BCWTP +fi + +DWTP=../test/services/coins/dogecoin/dogecoin_wallet_test_parameters.dart +if ! test -f "$DWTP"; then + echo 'prebuild.sh: creating template test/services/coins/dogecoin/dogecoin_wallet_test_parameters.dart file' + printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $DWTP +fi + +NWTP=../test/services/coins/namecoin/namecoin_wallet_test_parameters.dart +if ! test -f "$NWTP"; then + echo 'prebuild.sh: creating template test/services/coins/namecoin/namecoin_wallet_test_parameters.dart file' + printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $NWTP +fi + +FWTP=../test/services/coins/firo/firo_wallet_test_parameters.dart +if ! test -f "$FWTP"; then + echo 'prebuild.sh: creating template test/services/coins/firo/firo_wallet_test_parameters.dart file' + printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $FWTP fi From c2dd01bd1b65ccafd9945ea6287e6c9420ff5a9c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 16:51:01 -0600 Subject: [PATCH 035/103] refactor wallet parameter stub creation --- scripts/prebuild.sh | 37 +++++++++---------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh index ef3d47ce2..60e096232 100755 --- a/scripts/prebuild.sh +++ b/scripts/prebuild.sh @@ -6,32 +6,13 @@ if ! test -f "$KEYS"; then fi # Create template wallet test parameter files if they don't already exist -BWTP=../test/services/coins/bitcoin/bitcoin_wallet_test_parameters.dart -if ! test -f "$BWTP"; then - echo 'prebuild.sh: creating template test/services/coins/bitcoin/bitcoin_wallet_test_parameters.dart file' - printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $BWTP -fi +declare -a coins=("bitcoin" "bitcoincash" "dogecoin" "namecoin" "firo") # TODO add monero and wownero when those tests are updated to use the .gitignored test wallet setup: when doing that, make sure to update the test vectors for a new, private development seed -BCWTP=../test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart -if ! test -f "$BCWTP"; then - echo 'prebuild.sh: creating template test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart file' - printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $BCWTP -fi - -DWTP=../test/services/coins/dogecoin/dogecoin_wallet_test_parameters.dart -if ! test -f "$DWTP"; then - echo 'prebuild.sh: creating template test/services/coins/dogecoin/dogecoin_wallet_test_parameters.dart file' - printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $DWTP -fi - -NWTP=../test/services/coins/namecoin/namecoin_wallet_test_parameters.dart -if ! test -f "$NWTP"; then - echo 'prebuild.sh: creating template test/services/coins/namecoin/namecoin_wallet_test_parameters.dart file' - printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $NWTP -fi - -FWTP=../test/services/coins/firo/firo_wallet_test_parameters.dart -if ! test -f "$FWTP"; then - echo 'prebuild.sh: creating template test/services/coins/firo/firo_wallet_test_parameters.dart file' - printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $FWTP -fi +for coin in "${coins[@]}" +do + WALLETTESTPARAMFILE="../test/services/coins/${coin}/${coin}_wallet_test_parameters.dart" + if ! test -f "$WALLETTESTPARAMFILE"; then + echo "prebuild.sh: creating template test/services/coins/${coin}/${coin}_wallet_test_parameters.dart file" + printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $WALLETTESTPARAMFILE + fi +done From be5e9189f482cd98e838e1f4996f915688cbe1fc Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Nov 2022 16:55:41 -0600 Subject: [PATCH 036/103] add particl to coins list --- scripts/prebuild.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh index 60e096232..b9ab666a9 100755 --- a/scripts/prebuild.sh +++ b/scripts/prebuild.sh @@ -6,7 +6,7 @@ if ! test -f "$KEYS"; then fi # Create template wallet test parameter files if they don't already exist -declare -a coins=("bitcoin" "bitcoincash" "dogecoin" "namecoin" "firo") # TODO add monero and wownero when those tests are updated to use the .gitignored test wallet setup: when doing that, make sure to update the test vectors for a new, private development seed +declare -a coins=("bitcoin" "bitcoincash" "dogecoin" "namecoin" "firo" "particl") # TODO add monero and wownero when those tests are updated to use the .gitignored test wallet setup: when doing that, make sure to update the test vectors for a new, private development seed for coin in "${coins[@]}" do From 80db215a011fac160b380c29f634f2716e8379e0 Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Tue, 29 Nov 2022 18:22:46 -0700 Subject: [PATCH 037/103] Bump version number. 1.5.22 build 95 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 8cfce39cb..02c4bc433 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.21+93 +version: 1.5.22+95 environment: sdk: ">=2.17.0 <3.0.0" From 322870d9e37e9053550f98b7ed7863458b19a3cb Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 11 Nov 2022 15:18:46 -0800 Subject: [PATCH 038/103] Fix check in build script for jsoncpp directory existing. --- scripts/linux/build_all.sh | 2 +- scripts/linux/build_secure_storage_deps.sh | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/linux/build_all.sh b/scripts/linux/build_all.sh index 830198bcd..31edfb872 100755 --- a/scripts/linux/build_all.sh +++ b/scripts/linux/build_all.sh @@ -4,7 +4,7 @@ # flutter-elinux clean # flutter-elinux pub get # flutter-elinux build linux --dart-define="IS_ARM=true" -mkdir build +mkdir -p build ./build_secure_storage_deps.sh & (cd ../../crypto_plugins/flutter_liblelantus/scripts/linux && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh ) & diff --git a/scripts/linux/build_secure_storage_deps.sh b/scripts/linux/build_secure_storage_deps.sh index 69b452e2d..e63e38665 100755 --- a/scripts/linux/build_secure_storage_deps.sh +++ b/scripts/linux/build_secure_storage_deps.sh @@ -1,30 +1,31 @@ #!/bin/bash LINUX_DIRECTORY=$(pwd) +JSONCPP_TAG=1.7.4 mkdir -p build # Build JsonCPP -cd build || exit +cd build || exit 1 if ! [ -x "$(command -v git)" ]; then echo 'Error: git is not installed.' >&2 exit 1 fi -git -C jsoncpp pull || git clone https://github.com/open-source-parsers/jsoncpp.git jsoncpp -cd jsoncpp || exit -git checkout 1.7.4 +git -C jsoncpp pull origin $JSONCPP_TAG || git clone https://github.com/open-source-parsers/jsoncpp.git jsoncpp +cd jsoncpp || exit 1 +git checkout $JSONCPP_TAG mkdir -p build -cd build || exit +cd build || exit 1 cmake -DCMAKE_BUILD_TYPE=release -DBUILD_STATIC_LIBS=ON -DBUILD_SHARED_LIBS=ON -DARCHIVE_INSTALL_DIR=. -G "Unix Makefiles" .. make -j"$(nproc)" -cd "$LINUX_DIRECTORY" || exit +cd "$LINUX_DIRECTORY" || exit 1 # Build libSecret # sudo apt install meson libgirepository1.0-dev valac xsltproc gi-docgen docbook-xsl # sudo apt install python3-pip #pip3 install --user meson markdown tomli --upgrade # pip3 install --user gi-docgen -cd build || exit +cd build || exit 1 git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret -cd libsecret || exit +cd libsecret || exit 1 if ! [ -x "$(command -v meson)" ]; then echo 'Error: meson is not installed.' >&2 exit 1 From 3d8ae35956e2fe7f802f0e8e66ff2fe449a9d58d Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 30 Nov 2022 11:02:52 +0200 Subject: [PATCH 039/103] Fix address has no matching Script error --- .../coins/particl/particl_wallet.dart | 57 +++++++++---------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 6c964687c..d15053415 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -201,17 +201,15 @@ class ParticlWallet extends CoinServiceAPI { @override Future get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount( - data.satoshiBalanceUnconfirmed, - coin: coin); + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); } @override Future get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / + (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(); + .toDecimal(); @override Future get totalBalance async { @@ -220,19 +218,13 @@ class ParticlWallet extends CoinServiceAPI { .get(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount( - data.satoshiBalance, - coin: coin); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } else { - return Format.satoshisToAmount( - totalBalance, - coin: coin); + return Format.satoshisToAmount(totalBalance, coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount( - data.satoshiBalance, - coin: coin); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } @override @@ -320,7 +312,7 @@ class ParticlWallet extends CoinServiceAPI { throw ArgumentError('Invalid version or Network mismatch'); } else { try { - decodeBech32 = segwit.decode(address); + decodeBech32 = segwit.decode(address, particl.bech32!); } catch (err) { // Bech32 decode fail } @@ -1095,7 +1087,7 @@ class ParticlWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; final balance = - Format.decimalAmountToSatoshis(await availableBalance, coin); + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1294,14 +1286,14 @@ class ParticlWallet extends CoinServiceAPI { @override Future updateSentCachedTxData(Map txData) async { final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; final locale = await Devicelocale.currentLocale; final String worthNow = Format.localizedStringAsFixed( value: - ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(scaleOnInfinitePrecision: 2), + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -1336,7 +1328,7 @@ class ParticlWallet extends CoinServiceAPI { @override bool validateAddress(String address) { - return Address.validateAddress(address, _network); + return Address.validateAddress(address, _network, particl.bech32!); } @override @@ -1424,7 +1416,8 @@ class ParticlWallet extends CoinServiceAPI { } Future getCurrentNode() async { - final node = NodeService(secureStorageInterface: _secureStore).getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( @@ -1965,7 +1958,7 @@ class ParticlWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin(coin))) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1974,17 +1967,17 @@ class ParticlWallet extends CoinServiceAPI { } Decimal currencyBalanceRaw = - ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(scaleOnInfinitePrecision: 2); + ((Decimal.fromInt(satoshiBalance) * currentPrice) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2); final Map result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin(coin))) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal( - scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -2266,7 +2259,8 @@ class ParticlWallet extends CoinServiceAPI { /// Returns the scripthash or throws an exception on invalid particl address String _convertToScriptHash(String particlAddress, NetworkType network) { try { - final output = Address.addressToOutputScript(particlAddress, network); + final output = Address.addressToOutputScript( + particlAddress, network, particl.bech32!); final hash = sha256.convert(output.toList(growable: false)).toString(); final chars = hash.split(""); @@ -3319,7 +3313,7 @@ class ParticlWallet extends CoinServiceAPI { // Add transaction output for (var i = 0; i < recipients.length; i++) { - txb.addOutput(recipients[i], satoshiAmounts[i]); + txb.addOutput(recipients[i], satoshiAmounts[i], particl.bech32!); } try { @@ -3750,7 +3744,8 @@ class ParticlWallet extends CoinServiceAPI { @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance, coin); + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); From f984a6a20891068a497eeeca8800aa8bdc1f0a3a Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 30 Nov 2022 15:06:31 +0200 Subject: [PATCH 040/103] Add test history data and WIP change tests to particl --- test/cached_electrumx_test.mocks.dart | 13 + test/electrumx_test.mocks.dart | 13 + .../pages/send_view/send_view_test.mocks.dart | 19 +- .../exchange/exchange_view_test.mocks.dart | 13 + .../lockscreen_view_screen_test.mocks.dart | 6 +- .../create_pin_view_screen_test.mocks.dart | 6 +- ...restore_wallet_view_screen_test.mocks.dart | 9 +- ...dd_custom_node_view_screen_test.mocks.dart | 6 +- .../node_details_view_screen_test.mocks.dart | 6 +- ...twork_settings_view_screen_test.mocks.dart | 6 +- .../particl/particl_history_sample_data.dart | 158 +- .../particl_transaction_data_samples.dart | 323 +- .../particl/particl_utxo_sample_data.dart | 42 +- .../coins/particl/particl_wallet_test.dart | 2601 +++++++++-------- .../particl/particl_wallet_test.mocks.dart | 2 +- .../managed_favorite_test.mocks.dart | 6 +- test/widget_tests/node_card_test.mocks.dart | 6 +- .../node_options_sheet_test.mocks.dart | 22 +- .../transaction_card_test.mocks.dart | 13 + ...et_info_row_balance_future_test.mocks.dart | 6 +- .../wallet_info_row_test.mocks.dart | 6 +- 21 files changed, 1688 insertions(+), 1594 deletions(-) diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index a45cdd402..68d3b695f 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -551,6 +551,19 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); @override + int get familiarity => (super.noSuchMethod( + Invocation.getter(#familiarity), + returnValue: 0, + ) as int); + @override + set familiarity(int? familiarity) => super.noSuchMethod( + Invocation.setter( + #familiarity, + familiarity, + ), + returnValueForMissingStub: null, + ); + @override bool get showTestNetCoins => (super.noSuchMethod( Invocation.getter(#showTestNetCoins), returnValue: false, diff --git a/test/electrumx_test.mocks.dart b/test/electrumx_test.mocks.dart index 9f29ae1e5..c4a9ae9b2 100644 --- a/test/electrumx_test.mocks.dart +++ b/test/electrumx_test.mocks.dart @@ -272,6 +272,19 @@ class MockPrefs extends _i1.Mock implements _i4.Prefs { returnValueForMissingStub: null, ); @override + int get familiarity => (super.noSuchMethod( + Invocation.getter(#familiarity), + returnValue: 0, + ) as int); + @override + set familiarity(int? familiarity) => super.noSuchMethod( + Invocation.setter( + #familiarity, + familiarity, + ), + returnValueForMissingStub: null, + ); + @override bool get showTestNetCoins => (super.noSuchMethod( Invocation.getter(#showTestNetCoins), returnValue: false, diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index d63dafb04..b5a3e4e63 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -85,9 +85,9 @@ class _FakeManager_3 extends _i1.SmartFake implements _i6.Manager { ); } -class _FakeFlutterSecureStorageInterface_4 extends _i1.SmartFake +class _FakeSecureStorageInterface_4 extends _i1.SmartFake implements _i7.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_4( + _FakeSecureStorageInterface_4( Object parent, Invocation parentInvocation, ) : super( @@ -623,7 +623,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override _i7.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_4( + returnValue: _FakeSecureStorageInterface_4( this, Invocation.getter(#secureStorageInterface), ), @@ -1765,6 +1765,19 @@ class MockPrefs extends _i1.Mock implements _i17.Prefs { returnValueForMissingStub: null, ); @override + int get familiarity => (super.noSuchMethod( + Invocation.getter(#familiarity), + returnValue: 0, + ) as int); + @override + set familiarity(int? familiarity) => super.noSuchMethod( + Invocation.setter( + #familiarity, + familiarity, + ), + returnValueForMissingStub: null, + ); + @override bool get showTestNetCoins => (super.noSuchMethod( Invocation.getter(#showTestNetCoins), returnValue: false, diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 0da12dbd0..8c0d72f55 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -223,6 +223,19 @@ class MockPrefs extends _i1.Mock implements _i3.Prefs { returnValueForMissingStub: null, ); @override + int get familiarity => (super.noSuchMethod( + Invocation.getter(#familiarity), + returnValue: 0, + ) as int); + @override + set familiarity(int? familiarity) => super.noSuchMethod( + Invocation.setter( + #familiarity, + familiarity, + ), + returnValueForMissingStub: null, + ); + @override bool get showTestNetCoins => (super.noSuchMethod( Invocation.getter(#showTestNetCoins), returnValue: false, diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index a33c4ef28..2ea9822b6 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -29,9 +29,9 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake +class _FakeSecureStorageInterface_0 extends _i1.SmartFake implements _i2.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_0( + _FakeSecureStorageInterface_0( Object parent, Invocation parentInvocation, ) : super( @@ -311,7 +311,7 @@ class MockNodeService extends _i1.Mock implements _i10.NodeService { @override _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_0( + returnValue: _FakeSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index 3aed1dcb8..c891911ae 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -29,9 +29,9 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake +class _FakeSecureStorageInterface_0 extends _i1.SmartFake implements _i2.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_0( + _FakeSecureStorageInterface_0( Object parent, Invocation parentInvocation, ) : super( @@ -311,7 +311,7 @@ class MockNodeService extends _i1.Mock implements _i10.NodeService { @override _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_0( + returnValue: _FakeSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index cd4986e13..ee326b1d8 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -83,9 +83,9 @@ class _FakeTransactionData_4 extends _i1.SmartFake ); } -class _FakeFlutterSecureStorageInterface_5 extends _i1.SmartFake +class _FakeSecureStorageInterface_5 extends _i1.SmartFake implements _i6.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_5( + _FakeSecureStorageInterface_5( Object parent, Invocation parentInvocation, ) : super( @@ -744,10 +744,9 @@ class MockManager extends _i1.Mock implements _i12.Manager { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i13.NodeService { @override - _i6.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i6.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_5( + returnValue: _FakeSecureStorageInterface_5( this, Invocation.getter(#secureStorageInterface), ), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index 3ac5afcc7..9cdd45a2a 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -28,9 +28,9 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake +class _FakeSecureStorageInterface_0 extends _i1.SmartFake implements _i2.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_0( + _FakeSecureStorageInterface_0( Object parent, Invocation parentInvocation, ) : super( @@ -88,7 +88,7 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { @override _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_0( + returnValue: _FakeSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index 0f6447a9e..984287655 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -28,9 +28,9 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake +class _FakeSecureStorageInterface_0 extends _i1.SmartFake implements _i2.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_0( + _FakeSecureStorageInterface_0( Object parent, Invocation parentInvocation, ) : super( @@ -88,7 +88,7 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { @override _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_0( + returnValue: _FakeSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart index 707da7345..cf3ed4e9a 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart @@ -24,9 +24,9 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake +class _FakeSecureStorageInterface_0 extends _i1.SmartFake implements _i2.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_0( + _FakeSecureStorageInterface_0( Object parent, Invocation parentInvocation, ) : super( @@ -42,7 +42,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_0( + returnValue: _FakeSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), diff --git a/test/services/coins/particl/particl_history_sample_data.dart b/test/services/coins/particl/particl_history_sample_data.dart index 53a082778..b2e5f9fa7 100644 --- a/test/services/coins/particl/particl_history_sample_data.dart +++ b/test/services/coins/particl/particl_history_sample_data.dart @@ -1,103 +1,105 @@ // TODO these test vectors are valid for Namecoin: update for Particl final Map> historyBatchArgs0 = { - "k_0_0": ["d17132f41b2d55c730db5b27db721020abbd4a5087c15edcccbaa106eef8cbf3"], - "k_0_1": ["cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9"], - "k_0_2": ["82a12031d679c9dd3124047742dc22c2c7c03afa9644bddf55d4c95da41bca1c"], - "k_0_3": ["bbe10c5d3c102fd805770ed2d6c5438dce42c04d3f87e3260056d04245b17ddd"], - "k_0_4": ["d9ca5255516f963d8f348911451e2c69489a70dec7f34a4810ee8b0e32fcb04d"], - "k_0_5": ["2284461fd01b17e7443775e39b19f4378a063ff148938d2e4191cea3fd80368d"], - "k_0_6": ["cd3c32fddbf265410c34a58fefcc849b02fc16978d75e501f88f9effcbecd8fe"], - "k_0_7": ["a3bcc0c3c4a140fbcc4c4f4dff18790d8a2d5f868821f47460f68f0426291b57"], - "k_0_8": ["e400f9431798c87ea35ea19b265d9e56a73fd44c239957d9947ae79e16718fb4"], - "k_0_9": ["1fe8bb16b49725bf3703274e205a4695c398e664284cc68d92d15087a54da299"], + "k_0_0": ["29e9e6410954dea9e527a0d2cac5de4dea5fb600b719badff90d6d43518d3ed8"], + "k_0_1": ["9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc"], + "k_0_2": ["016b150fa7948b0841a2e755b3d40a96a5cbc1d2ac96a105d3ff201137f60d31"], + "k_0_3": ["1b668ee6f82a3f10ea0a515760f1165f53e99e979bd3c557cf6e09d793b9296d"], + "k_0_4": ["351da1f29872e3703dd92625f7face416226f54473bd853c80c1dcc2d8849b63"], + "k_0_5": ["2cd81781064f4679d09bf794d322d44903d8f66c00c8b80753eecaa40a993897"], + "k_0_6": ["ed4d0c5b4c13213e0c91805ef212bf25b81ed0ece46a2fa0d647e66cebebe53d"], + "k_0_7": ["ea558eee7a7b00b4a6207eb5ee783afa422410ababd02da879b687327caf9707"], + "k_0_8": ["90a7df71ab0a57abea44d955f78bf665785a2f104fce6348832fd79dc218d87a"], + "k_0_9": ["4b6d05c5ac9651a1e6452955cde697bbdca941b0c21021e30d0bc772c999a79d"], "k_0_10": [ - "2fabf8d61308c8b2d914489a9f02f669ed9fa68047666815cf1f3cd1bb5d8819" + "af8481316f782f8d2696ef6e70a4c23a17658500c2ee1ab5b7ef0eafb9f18112" ], - "k_0_11": ["42a567d344189430afe7d45d6854ef6e9d256d9ef4186afd31a1a5ff90a6a0dd"] + "k_0_11": ["608f1621850396138cf0ece48b20c1ecbd138d74d200c81b3640564083d117da"] }; + final Map> historyBatchArgs1 = { - "k_0_0": ["bcf7aec7c10dfba33ce80149300a7c4fe66460c1dd05503b5df5780884498186"], - "k_0_1": ["587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e"], - "k_0_2": ["fe6ad514f7427782f964b25995a90a3233589904b88f66a2d0e73e2560c9af7c"], - "k_0_3": ["6b962c5f9b4cfc004c74c5ab849304c405b02fc0e2f34ee17c185984f13c9da4"], - "k_0_4": ["720b79fab9a163ce6534828e8a673c5bf600161eba92c2b81555e79add59994c"], - "k_0_5": ["a10f4cf239abd4bcdb03dbe40b5c1d57ae3a7982adf8f177d481feb0ad3a52cd"], - "k_0_6": ["061f28e17ba1a56404b08a5899163011c7d6317e534ccd8e4d38911574f574b0"], - "k_0_7": ["ffc6297d487a13cb80689c448a3aef16cbd367a503d236d0aebd7218cc568e88"], - "k_0_8": ["f4a6c41fc432300509f97ca68db3b9d802d29f90c35a429e3886c480cdce44a2"], - "k_0_9": ["52f3bf96d02cd7e8c631b8ef36262994a3ec658427b930197ed004c8599cd7fd"], + "k_0_0": ["a41a236959ee41de770a0c2d360d62d75e4ba010294415cfb9f44eff0f731a70"], + "k_0_1": ["8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247"], + "k_0_2": ["540939221b59a81810398a1af45c22d1e9599e718f24870a44972fbfe55c0a39"], + "k_0_3": ["0323d2dcc60e8d9b6234be43c942f493f1039d45f47b641cd78d0860a5bc61cb"], + "k_0_4": ["c1c61eaa4e0ffa3c2bed0bb32b54281f743113c5d1fe59a4e595559ed1951a8b"], + "k_0_5": ["08dd301d83e42ded2d4ea1990389b281eaec328039b3469d3f2f25b52571be81"], + "k_0_6": ["7de683cf6ac67680166d82505b15974e4ff13caf544001479a2918157b171ccd"], + "k_0_7": ["b15515c8c6dd3eb6835086031f22fc644b490d291cd09772d414c67b5822e95b"], + "k_0_8": ["ff3ff4bc2f223169fa3317c19d399feeb84bae60f32171b1960c94fd61e72041"], + "k_0_9": ["15a21ca5cf24740944b894a9f0482abf1433ab59f156ff52d241ecc234a0dff4"], "k_0_10": [ - "7993aef51bebe79bae2d29b775c499f390e99fdb6b9accb8034f657b6e72927a" + "2037faeb3a55b1bb4545aa220578f9322ab8af8ac7af100d3c1261d25d1b1135" ], - "k_0_11": ["430214c9805d90c6a8c4489187db08157a93e60509e96b518dc8b5ba3d567206"] + "k_0_11": ["4e2051a980cb463b523df8a765e45ea18a0d08a670c38dd14f7c4457d7c86bff"] }; + final Map> historyBatchArgs2 = { - "k_0_0": ["afe5085dd514032810d5b266007557ba8a0f4bee84535cb10754c6d46ab8689b"], - "k_0_1": ["dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c"], - "k_0_2": ["e65d4274e8edc5cc1e7b676652e2e13b0b00648d15cf13caa982ecd6a67731ba"], - "k_0_3": ["6c69ca274f7d7f2fae882a87bcee93d9429328995c5bc6b326b548b4cefcaa9f"], - "k_0_4": ["86f1a5e17dc42c27cdb0dff8a41c2434575ab05ed2f3689fd7b674677e5ea446"], - "k_0_5": ["a5d9b8df5b80c56e6053497a8c89a37267010926e80e0d225a019b78673a7aa7"], - "k_0_6": ["a0030024518874720b82b38d965fb5b3083d9f42fab40e6be4797c789eeb06f2"], - "k_0_7": ["f20077f7c6a6b92a1f75bbbad8dbece9ae4609cfdfc85e40ccac7d463bdfd6e0"], - "k_0_8": ["07b7bb4020c377e0741587efe9c0b3931e2e45f667bc6f1fa81a8f15fbe86ce4"], - "k_0_9": ["ca0322fc293f6e4d8c8adac178ed4aaedbd9acd2ec84acaaf1529f9ab7bda6d2"], + "k_0_0": ["a48f8ee5dc6ff58b29eddeac1afd808b2edff10d736bdede3a2e6a95e588911c"], + "k_0_1": ["b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3"], + "k_0_2": ["aabdda195909a16141e8a3ab548fc5e8e8ba483762f94e1571cf93572bc6767e"], + "k_0_3": ["17ffed6bf6a9960b696139b6c40a1e4f2ee214a68442abdf57e9040079e62765"], + "k_0_4": ["25b58fdb4a8ea949730e138bddf6ff90c13d09123ba935efefc6f5d0e085885e"], + "k_0_5": ["952c7c54ad41bca032fa752d00a8d07e8ea5ae3e850266b45110bbc2a8969c43"], + "k_0_6": ["6b9a3a156ca83f20533ddc29c84cd1872fce4b612f738f022028ad680b77aaa3"], + "k_0_7": ["b6f0f12cc91bbb21584668146c2bfa7d07a786b8772fdd43e6daba3ff43aadff"], + "k_0_8": ["d478d5ca5e92e3a98c36136bf9712f981e7b1cb93ebe65e25f1e11151047a753"], + "k_0_9": ["f3bfe232ca898d1cb44c23586323b0fedef477208c8b4f203eebdf9ea8a2ceef"], "k_0_10": [ - "06df1d13aa43375775d7d2838595a0c4c642f8af15b06a99d5115d9236e9a79e" + "aedac6f5e8f0e96c7a53d9b0460ba9e9397efbd9d15c46a28d7b0be70ffc6dac" ], - "k_0_11": ["1a146c5a8dd5bf49faca3c6f65c9d955d361c6c00893c48d69cf7ff42c7b387b"] + "k_0_11": ["f66b687065339e2d4d567f9ea473947b8aab74c066bf00cdfdb5f918bbd159dc"] }; final Map> historyBatchArgs3 = { - "k_0_0": ["5c2c77a3671417c5831c336805770344b81e6c7ef0d873c986ba65a7bacd5f68"], - "k_0_1": ["c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b"], - "k_0_2": ["f430c440e90c48b9e4c7e5356083e7c1495b7cad53f39ebba64cca9fb3d05c82"], - "k_0_3": ["30a7ac6789383f7f6def9a927f3b6fb661cf9406fec71a1d118c7d86052382fb"], - "k_0_4": ["a797225a9155417ab18e16b9d7ce9bf4962ae5c05df572a33c60b36a0523f257"], - "k_0_5": ["24d1e3ac9e53727d943688e67eb5c000d993e9c3cf9585d630624197fb73bed3"], - "k_0_6": ["d667a44404519649cb65632d6a3be948a1f0971025c96cb4211943d301fe0d3e"], - "k_0_7": ["be8da400f004546b528fb989c14a88324b8b0c2d5680cf080ae1e1dac4401f68"], - "k_0_8": ["addfa7682c0a2461ab0e82b3c9302b38986b442a1a76c3c839b6c2f0eaa805fe"], - "k_0_9": ["98bb3aab55f4f305fd9994334b8dd3321eda50b25fad2ef3e636714b650d0bb0"], + "k_0_0": ["0664a4e19dd852c7d6fb53824198070e911dae9049aa9a6a940413cb868bbb27"], + "k_0_1": ["c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f"], + "k_0_2": ["aa34a5cd34908ed90f41ce31058552bee864b8894eec3b5b3f2583eb56eca759"], + "k_0_3": ["996cca35eb2c30699c8b28d3fff12a8fb7fbbacfbfe4aafc4e59833134cb37bb"], + "k_0_4": ["f581a49f492a0d2bc18b5c82ef999f03ff795bf5101646bed871b3efa1f34578"], + "k_0_5": ["ce5fe035e2ce47a7d5d1dc65ad4fc2d40d621f0f2fa25eb233e6d717e0b1743a"], + "k_0_6": ["51031f3710836824b48df2f33d1daa2c63b397c3c604577f09c8b4bef19302fb"], + "k_0_7": ["901f355de67d762c5a768ef19624359c8f95bc9f70d381507727a885cb46964b"], + "k_0_8": ["8ac9526d63526f498fc7c609adcb72c23a403cc271c91408288c19318357f059"], + "k_0_9": ["ca7b62c4b069ff2d4fcbc0faac32447b92d519dd726039eb7381ca5fde176e97"], "k_0_10": [ - "bee1eee20d7169d03ce68d340a17f4598f589920513ec19c458db45399639a9f" + "f07285fe3a8eac625b2c5339cf9f068fccb2278f923a38a46a60c94c7179d4aa" ], - "k_0_11": ["928a988dd65d100d1677a0478abfcd4d2a70aabb0812c58a2b1b4b51c395ed54"] + "k_0_11": ["f8f09b8fe23da8435409c3e688002dcaa87c2b9f3707e17bc668db7392039dab"] }; final Map> historyBatchArgs4 = { - "k_0_0": ["6bbfd9c1c28d6984646db4736196f67f2d1075894bb1d8990294ca7d663bece6"], - "k_0_1": ["42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3"], - "k_0_2": ["191c977174dc50a57628aea6684c428d3a5e90bbe16c4e412be51b0cfc589d38"], - "k_0_3": ["0daaf61564fd07a25ef106d958216992896f931f5bed4fbf56cc3f94443dc164"], - "k_0_4": ["ac5aca40fed2903def31c9ef1d60874247cdcc5b85238c7a1d83c67d2924d6b9"], - "k_0_5": ["c4102ff0556d863b4bab9d8232fe1f0c0fde4b6e4fe23064b4ecd0958f9726cc"], - "k_0_6": ["1c4bd1554e4992e5914dcd8f3e13927ffd46302dfdcbd2dca0cfd47c040c4256"], - "k_0_7": ["eaf5562ebef7cafa58e2c1fc4ae023e5ae8dd71ee637b08c4bc7e274e401a9a4"], - "k_0_8": ["06f7f55c221fee1b36284b5360155b8380cb9d7172b7e28eb37c61b7ebb6f227"], - "k_0_9": ["7e7ca801131ec1c5797f2c4aa46908ee50e9958cf1cbf53c2481d110800c3d6d"], + "k_0_0": ["4cff1590918be5d24d130f10627aaacc6d1e3f03872643c3afc742e6c77e3e72"], + "k_0_1": ["3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339"], + "k_0_2": ["68668ef2d53d4cb5bda66ce3adae25dbe7a8466eb3eca64ed816a59cf8362288"], + "k_0_3": ["8bb32cbd5de862d6305813e312b0caec7249692c6f41154b4855003450c58f6d"], + "k_0_4": ["d66592642c8dcbde165c04fd394ed92bcef89bbadbfd8cbe213cea0e811708ce"], + "k_0_5": ["72ac3ef3b722777e5e7cf75eaf8324cb7db0c575a6a8640609757c99a71bca91"], + "k_0_6": ["be8f9884d1655f84993572924729f52ec66b56582adf44b841cebdf42d3dcd5b"], + "k_0_7": ["c5a53feb8be5ea226da3e72bfcb522569f7956d137266e3da16ada99d0c4817b"], + "k_0_8": ["f50124f4371374e623db18f24bf01644018b0a47351dfa5624df9706c5409dca"], + "k_0_9": ["83f0334c6c57164ac6fd9c83b89f1977e2e4bf9144dd25c992e3def16242ae8c"], "k_0_10": [ - "3895e073aa034add7d2589bfdd1e54f6b9a8d7688d63fff0c3aac7950c6f9697" + "e04e7d94a880ccbd8ce473ce5e780fd86003137cef1e879e38971e4216a282e1" ], - "k_0_11": ["ec17dd7c4fe8fbcfce94e9237d3c7ed7f5c91a45b1a060406e206df7e814b006"] + "k_0_11": ["f6a7b80c32f2568bebe37d6615ebfa602ec04207cd9edf304ff7f835b03c27d2"] }; final Map> historyBatchArgs5 = { - "k_0_0": ["83b744ccb88827d544081c1a03ea782a7d00d6224ff9fddb7d0fbad399e1cae7"], - "k_0_1": ["86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874"], - "k_0_2": ["5baba32b1899d5e740838559ef39b7d8e9ba302bd24b732eeedd4c0e6ec65b51"], - "k_0_3": ["9892eb48394b0e155f63879fb89c3b068fcc071fed2e5cb11fe0729b85b53d67"], - "k_0_4": ["64192782cdaecb5e2a871a2d0fb3f541873e4750cd4e7d28e4d858ab40664a36"], - "k_0_5": ["4047ff48e96d25628acfeaec6ca75c1a668c54fd70a14414827cb59976a3b666"], - "k_0_6": ["299e8bc634ef6438c5bf99c12c2340c77c56ab974ffd767e77c17994e5cfaef8"], - "k_0_7": ["ab649fa14452563b385eb025e0b4cf2dd869c02fcdf2ec0f72725bbe2adaa3bd"], - "k_0_8": ["6be1ca4f8ee923e32137b6cdae324b841a0a60afbee4f4ae457fe31f29e001a6"], - "k_0_9": ["2a99ceea87df667135cc1801682d2c5dc7b95b7efadc48e156345ba46f4c0dc6"], + "k_0_0": ["f2547dcbe38adc0fee943dc0b0a543f96b90af587850c9df172c69134a49f4c9"], + "k_0_1": ["0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a"], + "k_0_2": ["099bdff41fbbfc3d90ea5a8510d5588e71a27509592447025ee6dee4278e13ff"], + "k_0_3": ["c1ae51351f1267bf6747c888760f25cc199747a3cc2be7dd6a899223d748508c"], + "k_0_4": ["7eba642b2889d562edc75dc2653caf1d2b864a9db8a0e64e1b6d62e014c6ed5b"], + "k_0_5": ["be2b6216b6effadfa12f4588612396daa781a40b50a7bd73c1bf722b7855d4c3"], + "k_0_6": ["ddf040fca6a4609fcb1045216fc17772b821cf560802be267bd433596c2aa897"], + "k_0_7": ["5c9c6a409240e59d731c7d87c58d701e2d99cc87d073ff07114ebf5db602e87d"], + "k_0_8": ["03b3c0dae8d561f1ab38199b5dfa4930a18fe702b14332b996c93364819aef56"], + "k_0_9": ["98bf1f26ff3e8db88a4506d476122c2b2ff7f8e9e217b351e532fb95d6c9e308"], "k_0_10": [ - "9304094916a19040d3c8f10df90dae1144d1f09ac9e676e66bb76341c70388ac" + "0c6c028ede10b0c3180e9541675c16b72f4443663dd7dbe9b45037b230d55917" ], - "k_0_11": ["01b12fb2ea2533226471dfa863133ce390e3e13a804734e8af995a45aa7c7582"] + "k_0_11": ["eb1ebeefa4bc5f754daabb0f783f3685bd839429ee0a287bd26d8717265c3d27"] }; final Map>> historyBatchResponse = { @@ -179,10 +181,10 @@ final Map>> emptyHistoryBatchResponse = { }; final List activeScriptHashes = [ - "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c", - "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e", - "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9", - "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874", - "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b", - "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247", + "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339", + "b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3", + "0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a", + "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc", + "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" ]; diff --git a/test/services/coins/particl/particl_transaction_data_samples.dart b/test/services/coins/particl/particl_transaction_data_samples.dart index fb2a2a932..e90a90263 100644 --- a/test/services/coins/particl/particl_transaction_data_samples.dart +++ b/test/services/coins/particl/particl_transaction_data_samples.dart @@ -1,251 +1,265 @@ -// TODO these test vectors are valid for Namecoin: update for Particl - import 'package:stackwallet/models/paymint/transactions_model.dart'; final transactionData = TransactionData.fromMap({ - "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6": tx1, - "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7": tx2, - "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d": tx3, - "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9": tx4, + "a51831f09072dc9edb3130f677a484ca03bced8f6d803e8df83a1ed84bc06c0a": tx1, + "39a9c37d54d04f9ac6ed45aaa1a02b058391b5d1fc0e2e1d67e50f36b1d82896": tx2, + "e53ef367a5f9d8493825400a291136870ea24a750f63897f559851ab80ea1020": tx3, + "10e14b1d34c18a563b476c4c36688eb7caebf6658e25753074471d2adef460ba": tx4, }); final tx1 = Transaction( - txid: "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6", + txid: "a51831f09072dc9edb3130f677a484ca03bced8f6d803e8df83a1ed84bc06c0a", confirmedStatus: true, - confirmations: 212, + confirmations: 15447, txType: "Received", - amount: 1000000, - fees: 23896, - height: 629633, - address: "nc1qwfda4s9qmdqpnykgpjf85n09ath983srtuxcqx", - timestamp: 1663093275, + amount: 10000000, + fees: 53600, + height: 1299909, + address: "PtQCgwUx9mLmRDWxB3J7MPnNsWDcce7a5g", + timestamp: 1667814832, worthNow: "0.00", worthAtBlockTimestamp: "0.00", inputSize: 2, outputSize: 2, inputs: [ Input( - txid: "290904699ccbebd0921c4acc4f7a10f41141ee6a07bc64ebca5674c1e5ee8dfa", + txid: "e53ef367a5f9d8493825400a291136870ea24a750f63897f559851ab80ea1020", vout: 1, ), Input( - txid: "bd84ae7e09414b0ccf5dcbf70a1f89f2fd42119a98af35dd4ecc80210fed0487", + txid: "b255bf1b4b2f1a76eab45fd69e589b655261b049f238807b0acbf304d1b8195b", vout: 0, ), ], outputs: [ Output( - scriptpubkeyAddress: "nc1qwfda4s9qmdqpnykgpjf85n09ath983srtuxcqx", - value: 1000000, + scriptpubkeyAddress: "PtQCgwUx9mLmRDWxB3J7MPnNsWDcce7a5g", + value: 10000000, ), Output( - scriptpubkeyAddress: "nc1qp7h7fxcnkqcpul202z6nh8yjy8jpt39jcpeapj", - value: 29853562, + scriptpubkeyAddress: "PsHtVuRCybcTpJQN6ckLFptPB7k9ZkqztA", + value: 9946400, ) ], ); final tx2 = Transaction( - txid: "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + txid: "39a9c37d54d04f9ac6ed45aaa1a02b058391b5d1fc0e2e1d67e50f36b1d82896", confirmedStatus: true, - confirmations: 150, + confirmations: 13927, txType: "Sent", - amount: 988567, - fees: 11433, - height: 629695, - address: "nc1qraffwaq3cxngwp609e03ynwsx8ykgjnjve9f3y", - timestamp: 1663142110, + amount: 50000000, + fees: 49500, + height: 1301433, + address: "PcKLXor8hqb3qSjtoHQThapJSbPapSDt4C", + timestamp: 1668010880, worthNow: "0.00", worthAtBlockTimestamp: "0.00", inputSize: 1, - outputSize: 1, + outputSize: 2, 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", + txid: "909bdf555736c272df0e1df52ca5fcce4f1090b74c0e5d9319bb40e02f4b3add", vout: 1, ), ], outputs: [ Output( - scriptpubkeyAddress: "nc1qw4srwqq2semrxje4x6zcrg53g07q0pr3yqv5kr", - value: 1000000, + scriptpubkeyAddress: "PcKLXor8hqb3qSjtoHQThapJSbPapSDt4C", + value: 50000000, ), Output( - scriptpubkeyAddress: "nc1qsgr7u4hd22rc64r9vlef69en9wzlvmjt8dzyrm", - value: 28805770, + scriptpubkeyAddress: "PjDq9kwadvgKNtQLTdGqcDsFzPmk9LMjT7", + value: 1749802000, + ), + ], +); + +final tx3 = Transaction( + txid: "e53ef367a5f9d8493825400a291136870ea24a750f63897f559851ab80ea1020", + confirmedStatus: true, + confirmations: 23103, + txType: "Received", + amount: 10000000, + fees: 34623, + height: 1292263, + address: "PhDSyHLt7ejdPXGve3HFr93pSdFLHUBwdr", + timestamp: 1666827392, + worthNow: "0.00", + worthAtBlockTimestamp: "0.00", + inputSize: 1, + outputSize: 2, + inputs: [ + Input( + txid: "8a2c6a4c0797d057f20f93b5e3b6e5f306493c67b2341626e0375f30f35a2d47", + vout: 0, + ) + ], + outputs: [ + Output( + scriptpubkeyAddress: "PYv7kk7TKQsSosWLuLveMJqAYxTiDiK5kp", + value: 39915877, + ), + Output( + scriptpubkeyAddress: "PhDSyHLt7ejdPXGve3HFr93pSdFLHUBwdr", + value: 10000000, ), ], ); final tx4 = Transaction( - txid: "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", + txid: "10e14b1d34c18a563b476c4c36688eb7caebf6658e25753074471d2adef460ba", confirmedStatus: true, - confirmations: 130, + confirmations: 493, txType: "Sent", - amount: 988567, - fees: 11433, - height: 629717, - address: "nc1qmdt0fxhpwx7x5ymmm9gvh229adu0kmtukfcsjk", - timestamp: 1663155739, + amount: 9945773, + fees: 27414, + height: 1314873, + address: "PpqgMahyfqfasunUKfkmVfdpyhhrHa2ibY", + timestamp: 1669740960, worthNow: "0.00", worthAtBlockTimestamp: "0.00", inputSize: 1, outputSize: 1, inputs: [ Input( - txid: "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + txid: "aab01876c4db40b35ba00bbfb7c58aaec32cad7dc136214b7344a944606cbe73", vout: 0, ), ], outputs: [ Output( - scriptpubkeyAddress: "nc1qmdt0fxhpwx7x5ymmm9gvh229adu0kmtukfcsjk", - value: 988567, + scriptpubkeyAddress: "PpqgMahyfqfasunUKfkmVfdpyhhrHa2ibY", + value: 9945773, ), ], ); final tx1Raw = { - "txid": "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6", - "hash": "40c8dd876cf111dc00d3aa2fedc93a77c18b391931939d4f99a760226cbff675", - "version": 2, - "size": 394, - "vsize": 232, - "weight": 925, - "locktime": 0, + "txid": "a51831f09072dc9edb3130f677a484ca03bced8f6d803e8df83a1ed84bc06c0a", + "hash": "46b7358ccbc018da4e144188f311657e8b694f056211d7511726c4259dca86b4", + "size": 374, + "vsize": 267, + "version": 160, + "locktime": 1299908, "vin": [ { "txid": - "290904699ccbebd0921c4acc4f7a10f41141ee6a07bc64ebca5674c1e5ee8dfa", + "e53ef367a5f9d8493825400a291136870ea24a750f63897f559851ab80ea1020", "vout": 1, - "scriptSig": { - "asm": "001466d2173325f3d379c6beb0a4949e937308edb152", - "hex": "16001466d2173325f3d379c6beb0a4949e937308edb152" - }, + "scriptSig": {"asm": "", "hex": ""}, "txinwitness": [ - "3044022062d0f32dc051ed1e91889a96070121c77d895f69d2ed5a307d8b320e0352186702206a0c2613e708e5ef8a935aba61b8fa14ddd6ca4e9a80a8b4ded126a879217dd101", - "0303cd92ed121ef22398826af055f3006769210e019f8fb43bd2f5556282d84997" + "30440220336bf0952b543314ba37b1bb8866a65b2482b499c715d778e92e90d7d59c6a39022072cae4341ca8825bee8043ae91f18de5776edd069ed228142eca55a16c887d6b01", + "026b4ca62de9e8f63abd0a6cf176536fe8e6a64d6343b6396aa9fb35232520e4a7" ], - "sequence": 4294967295 + "sequence": 4294967293 }, { "txid": - "bd84ae7e09414b0ccf5dcbf70a1f89f2fd42119a98af35dd4ecc80210fed0487", + "b255bf1b4b2f1a76eab45fd69e589b655261b049f238807b0acbf304d1b8195b", "vout": 0, "scriptSig": {"asm": "", "hex": ""}, "txinwitness": [ - "3045022100e8814706766a2d7588908c51209c3b7095241bbc681febdd6b317b7e9b6ea97502205c33c63e4d8a675c19122bfe0057afce2159e6bd86f2c9aced214de77099dc8b01", - "03c35212e3a4c0734735eccae9219987dc78d9cf6245ab247942d430d0a01d61be" + "304402205b914f31952958d54f0290d47eef6d9042259387c9493993882e24bd9acefe00022066b16f2f41885a85051c9bff4c119ecddc0209520e9a93d75866624f11b4e82d01", + "026b4ca62de9e8f63abd0a6cf176536fe8e6a64d6343b6396aa9fb35232520e4a7" ], - "sequence": 4294967295 + "sequence": 4294967293 } ], "vout": [ { - "value": 0.01, "n": 0, + "type": "standard", + "value": 0.1, + "valueSat": 10000000, "scriptPubKey": { - "asm": "0 725bdac0a0db401992c80c927a4de5eaee53c603", - "hex": "0014725bdac0a0db401992c80c927a4de5eaee53c603", + "asm": + "OP_DUP OP_HASH160 e0923d464a2c30438f0808e4af94868253b63ca0 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914e0923d464a2c30438f0808e4af94868253b63ca088ac", "reqSigs": 1, - "type": "witness_v0_keyhash", - "addresses": ["nc1qwfda4s9qmdqpnykgpjf85n09ath983srtuxcqx"] + "type": "pubkeyhash", + "addresses": ["PtQCgwUx9mLmRDWxB3J7MPnNsWDcce7a5g"] } }, { - "value": 0.29853562, "n": 1, + "type": "standard", + "value": 0.099464, + "valueSat": 9946400, "scriptPubKey": { - "asm": "0 0fafe49b13b0301e7d4f50b53b9c9221e415c4b2", - "hex": "00140fafe49b13b0301e7d4f50b53b9c9221e415c4b2", + "asm": + "OP_DUP OP_HASH160 d4686eee8cd127b50d28869627d61b38cc63fe4a OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914d4686eee8cd127b50d28869627d61b38cc63fe4a88ac", "reqSigs": 1, - "type": "witness_v0_keyhash", - "addresses": ["nc1qp7h7fxcnkqcpul202z6nh8yjy8jpt39jcpeapj"] + "type": "pubkeyhash", + "addresses": ["PsHtVuRCybcTpJQN6ckLFptPB7k9ZkqztA"] } } ], - "hex": - "02000000000102fa8deee5c17456caeb64bc076aee4111f4107a4fcc4a1c92d0ebcb9c69040929010000001716001466d2173325f3d379c6beb0a4949e937308edb152ffffffff8704ed0f2180cc4edd35af989a1142fdf2891f0af7cb5dcf0c4b41097eae84bd0000000000ffffffff0240420f0000000000160014725bdac0a0db401992c80c927a4de5eaee53c6037a87c701000000001600140fafe49b13b0301e7d4f50b53b9c9221e415c4b202473044022062d0f32dc051ed1e91889a96070121c77d895f69d2ed5a307d8b320e0352186702206a0c2613e708e5ef8a935aba61b8fa14ddd6ca4e9a80a8b4ded126a879217dd101210303cd92ed121ef22398826af055f3006769210e019f8fb43bd2f5556282d8499702483045022100e8814706766a2d7588908c51209c3b7095241bbc681febdd6b317b7e9b6ea97502205c33c63e4d8a675c19122bfe0057afce2159e6bd86f2c9aced214de77099dc8b012103c35212e3a4c0734735eccae9219987dc78d9cf6245ab247942d430d0a01d61be00000000", "blockhash": - "c9f53cc7cbf654cbcc400e17b33e03a32706d6e6647ad7085c688540f980a378", - "confirmations": 212, - "time": 1663093275, - "blocktime": 1663093275 + "b7cb29eb9cb4fa73c4da32f5cf8dfd90194eb6b689d4e547fa9b3176a698a741", + "height": 1299909, + "confirmations": 15447, + "time": 1667814832, + "blocktime": 1667814832 }; final tx2Raw = { - "txid": "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", - "hash": "32dbc0d21327e0cb94ec6069a8d235affd99689ffc5f68959bfb720bafc04bcf", - "version": 2, - "size": 192, - "vsize": 110, - "weight": 438, - "locktime": 0, + "txid": "39a9c37d54d04f9ac6ed45aaa1a02b058391b5d1fc0e2e1d67e50f36b1d82896", + "hash": "85130125ec9e37a48670fb5eb0a2780b94ea958cd700a1237ff75775d8a0edb0", + "size": 226, + "vsize": 173, + "version": 160, + "locktime": 1301432, "vin": [ { "txid": - "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6", - "vout": 0, + "909bdf555736c272df0e1df52ca5fcce4f1090b74c0e5d9319bb40e02f4b3add", + "vout": 1, "scriptSig": {"asm": "", "hex": ""}, "txinwitness": [ - "30450221009d58ebfaab8eae297910bca93a7fd48f94ce52a1731cf27fb4c043368fa10e8d02207e88f5d868113d9567999793be0a5b752ad704d04224046839763cefe46463a501", - "02f6ca5274b59dfb014f6a0d690671964290dac7f97fe825f723204e6cb8daf086" + "30440220486c87376122e2d3ca7154f41a45fdafa2865412ec90e4b3db791915eee1d13002204cca8520a655b43c3cddc216725cc8508cd9b326a39ed99ed893be59167289af01", + "03acc7ad6e2e9560db73f7ec7ef2f55a6115d85069cf0eacfe3ab663f33415573c" ], - "sequence": 4294967295 + "sequence": 4294967293 } ], "vout": [ { - "value": 0.00988567, "n": 0, + "type": "standard", + "value": 0.5, + "valueSat": 50000000, "scriptPubKey": { - "asm": "0 1f52977411c1a687074f2e5f124dd031c9644a72", - "hex": "00141f52977411c1a687074f2e5f124dd031c9644a72", + "asm": + "OP_DUP OP_HASH160 3024b192883be45b197b548f71155829af980724 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143024b192883be45b197b548f71155829af98072488ac", "reqSigs": 1, - "type": "witness_v0_keyhash", - "addresses": ["nc1qraffwaq3cxngwp609e03ynwsx8ykgjnjve9f3y"] + "type": "pubkeyhash", + "addresses": ["PcKLXor8hqb3qSjtoHQThapJSbPapSDt4C"] + } + }, + { + "n": 1, + "type": "standard", + "value": 17.49802, + "valueSat": 1749802000, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 7be2f80f6b9f6df740142fb34668c25c4e5c8bd5 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9147be2f80f6b9f6df740142fb34668c25c4e5c8bd588ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["PjDq9kwadvgKNtQLTdGqcDsFzPmk9LMjT7"] } } ], - "hex": - "02000000000101c6ccf4ddc2a21434ed634636378923d01014b2d3b2f124999f3e7c88d043f53e0000000000ffffffff0197150f00000000001600141f52977411c1a687074f2e5f124dd031c9644a72024830450221009d58ebfaab8eae297910bca93a7fd48f94ce52a1731cf27fb4c043368fa10e8d02207e88f5d868113d9567999793be0a5b752ad704d04224046839763cefe46463a5012102f6ca5274b59dfb014f6a0d690671964290dac7f97fe825f723204e6cb8daf08600000000", "blockhash": - "ae1129ee834853c45b9edbb7228497c7fa423d7d1bdec8fd155f9e3c429c84d3", - "confirmations": 150, - "time": 1663142110, - "blocktime": 1663142110 + "065c7328f1a768f3005ab7bfb322806bcc0cf88a96e89830b44991cc434c9955", + "height": 1301433, + "confirmations": 13927, + "time": 1668010880, + "blocktime": 1668010880 }; final tx3Raw = { @@ -314,44 +328,45 @@ final tx3Raw = { }; final tx4Raw = { - "txid": "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", - "hash": "c6b544ddd7d901fcc7218208a6cfc8e1819c403a22cc8a1f1a7029aafa427925", - "version": 2, - "size": 192, - "vsize": 110, - "weight": 438, - "locktime": 0, + "txid": "10e14b1d34c18a563b476c4c36688eb7caebf6658e25753074471d2adef460ba", + "hash": "cb0d83958db55c91fb9cd9cab65ee516e63aea68ae5650a692918779ceb46576", + "size": 191, + "vsize": 138, + "version": 160, + "locktime": 1314871, "vin": [ { "txid": - "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + "aab01876c4db40b35ba00bbfb7c58aaec32cad7dc136214b7344a944606cbe73", "vout": 0, "scriptSig": {"asm": "", "hex": ""}, "txinwitness": [ - "3045022100c664c6ad206999e019954c5206a26c2eca1ae2572288c0f78074c279a4a210ce022017456fdf85f744d694fa2e4638acee782d809268ea4808c04d91da3ac4fe7fd401", - "035456b63e86c0a6235cb3debfb9654966a4c2362ec678ae3b9beec53d31a25eba" + "304402202e33ab9c5bb6a50c24de9ebfd1b2f398b4c9027787fb9620fda515a25b62ffcf02205e8371aeeda3b3765fa1e2a5c7ebce5dffbf18932012670c1f5266992f9ed9c901", + "039ca6c697fed4daf1697f137e7d5b113ff7b6c48ea48d707addd9cfa51889a42a" ], - "sequence": 4294967295 + "sequence": 4294967293 } ], "vout": [ { - "value": 0.00988567, "n": 0, + "type": "standard", + "value": 0.09945773, + "valueSat": 9945773, "scriptPubKey": { - "asm": "0 db56f49ae171bc6a137bd950cba945eb78fb6d7c", - "hex": "0014db56f49ae171bc6a137bd950cba945eb78fb6d7c", + "asm": + "OP_DUP OP_HASH160 b9833ad924ab05567ea2b679a5c523c66a1da6d7 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914b9833ad924ab05567ea2b679a5c523c66a1da6d788ac", "reqSigs": 1, - "type": "witness_v0_keyhash", - "addresses": ["nc1qmdt0fxhpwx7x5ymmm9gvh229adu0kmtukfcsjk"] + "type": "pubkeyhash", + "addresses": ["PpqgMahyfqfasunUKfkmVfdpyhhrHa2ibY"] } } ], - "hex": - "020000000001014da0dde1ee465c062356dd3e2f9d04430753148b0f0dc3d81b32e7e93265b5710000000000ffffffff0197150f0000000000160014db56f49ae171bc6a137bd950cba945eb78fb6d7c02483045022100c664c6ad206999e019954c5206a26c2eca1ae2572288c0f78074c279a4a210ce022017456fdf85f744d694fa2e4638acee782d809268ea4808c04d91da3ac4fe7fd40121035456b63e86c0a6235cb3debfb9654966a4c2362ec678ae3b9beec53d31a25eba00000000", "blockhash": - "6f60029ff3a32ca2d7e7e23c02b9cb35f61e7f9481992f9c3ded2c60c7b1de9b", - "confirmations": 130, - "time": 1663155739, - "blocktime": 1663155739 + "74e2d8acec688645120925c8a10d2fdf9ec61278534c0788d749162a6899ddaf", + "height": 1314873, + "confirmations": 493, + "time": 1669740960, + "blocktime": 1669740960 }; diff --git a/test/services/coins/particl/particl_utxo_sample_data.dart b/test/services/coins/particl/particl_utxo_sample_data.dart index 5a0dff492..57adb0357 100644 --- a/test/services/coins/particl/particl_utxo_sample_data.dart +++ b/test/services/coins/particl/particl_utxo_sample_data.dart @@ -1,22 +1,20 @@ -// TODO these test vectors are valid for Namecoin: update for Particl - import 'package:stackwallet/models/paymint/utxo_model.dart'; final Map>> batchGetUTXOResponse0 = { "some id 0": [ { "tx_pos": 0, - "value": 988567, + "value": 9973187, "tx_hash": - "32dbc0d21327e0cb94ec6069a8d235affd99689ffc5f68959bfb720bafc04bcf", - "height": 629695 + "7b932948c95cf483798011da3fc77b6d53ee26d3d2ba4d90748cd007bdce48e8", + "height": 1314869 }, { "tx_pos": 0, - "value": 1000000, + "value": 50000000, "tx_hash": - "40c8dd876cf111dc00d3aa2fedc93a77c18b391931939d4f99a760226cbff675", - "height": 629633 + "aae9e712e26e5ff77ac2258c47a845ad6e952d580c2ad805e2b5d7667f3d4e42", + "height": 1297229 }, ], "some id 1": [], @@ -24,36 +22,36 @@ final Map>> batchGetUTXOResponse0 = { final utxoList = [ UtxoObject( - txid: "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + txid: "aab01876c4db40b35ba00bbfb7c58aaec32cad7dc136214b7344a944606cbe73", vout: 0, status: Status( confirmed: true, - confirmations: 150, - blockHeight: 629695, - blockTime: 1663142110, + confirmations: 516, + blockHeight: 1314869, + blockTime: 1669740688, blockHash: - "32dbc0d21327e0cb94ec6069a8d235affd99689ffc5f68959bfb720bafc04bcf", + "6146005e4b21b72d0e2afe5b0cce3abd6e9e9e71c6cf6a1e1150d33e33ba81d4", ), - value: 988567, + value: 9973187, fiatWorth: "\$0", - txName: "nc1qraffwaq3cxngwp609e03ynwsx8ykgjnjve9f3y", + txName: "pw1qj6t0kvsmx8qd95pdh4rwxaz5qp5qtfz0xq2rja", blocked: false, isCoinbase: false, ), UtxoObject( - txid: "3ef543d0887c3e9f9924f1b2d3b21410d0238937364663ed3414a2c2ddf4ccc6", + txid: "909bdf555736c272df0e1df52ca5fcce4f1090b74c0e5d9319bb40e02f4b3add", vout: 0, status: Status( confirmed: true, - confirmations: 212, - blockHeight: 629633, - blockTime: 1663093275, + confirmations: 18173, + blockHeight: 1297229, + blockTime: 1667469296, blockHash: - "40c8dd876cf111dc00d3aa2fedc93a77c18b391931939d4f99a760226cbff675", + "5c5c1a4e2d9cc77a1df4337359f901c92bb4907cff85312599b06141fd1d96d9", ), - value: 1000000, + value: 50000000, fiatWorth: "\$0", - txName: "nc1qwfda4s9qmdqpnykgpjf85n09ath983srtuxcqx", + txName: "PhDSyHLt7ejdPXGve3HFr93pSdFLHUBwdr", blocked: false, isCoinbase: false, ), diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 27fdaa9c7..844b61461 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -1,14 +1,18 @@ 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/models.dart'; import 'package:stackwallet/services/coins/particl/particl_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 'particl_history_sample_data.dart'; import 'particl_wallet_test.mocks.dart'; import 'particl_wallet_test_parameters.dart'; @@ -122,7 +126,7 @@ void main() { expect( mainnetWallet?.validateAddress("Pi9W46PhXkNRusar2KVMbXftYpGzEYGcSa"), true); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -133,7 +137,7 @@ void main() { mainnetWallet?.addressType( address: "Pi9W46PhXkNRusar2KVMbXftYpGzEYGcSa"), DerivePathType.bip44); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -149,7 +153,7 @@ void main() { mainnetWallet ?.validateAddress("bc1qc5ymmsay89r6gr4fy2kklvrkuvzyln4shdvjhf"), false); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -159,7 +163,7 @@ void main() { expect( mainnetWallet?.validateAddress("PputQYxNxMiYh3sg7vSh25wg3XxHiPHag7"), true); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -172,7 +176,7 @@ void main() { expect( mainnetWallet?.validateAddress("16YB85zQHjro7fqjR2hMcwdQWCX8jNVtr5"), false); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -181,9 +185,9 @@ void main() { test("invalid mainnet particl p2wpkh address", () { expect( mainnetWallet - ?.validateAddress("pw1qce3dhmmle4e0833kssj7ptta3ehydjf0tsa3ju"), + ?.validateAddress("pw1qce3dhmmle4e0833mssj7ptta3ehydjf0tsa3ju"), false); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -487,1296 +491,1295 @@ void main() { // // }); // }); - // group("Particl service class functions that depend on shared storage", () { - // final testWalletId = "NMCtestWalletID"; - // final testWalletName = "NMCWallet"; - - // bool hiveAdaptersRegistered = false; - - // MockElectrumX? client; - // MockCachedElectrumX? cachedClient; - // MockPriceAPI? priceAPI; - // late FakeSecureStorage secureStore; - // MockTransactionNotificationTracker? tracker; - - // NamecoinWallet? nmc; - - // 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(); - - // nmc = NamecoinWallet( - // walletId: testWalletId, - // walletName: testWalletName, - // coin: Coin.particl, - // client: client!, - // cachedClient: cachedClient!, - // tracker: tracker!, - // priceAPI: priceAPI, - // secureStore: secureStore, - // ); - // }); - - // // test("initializeWallet no network", () async { - // // when(client?.ping()).thenAnswer((_) async => false); - // // expect(await nmc?.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 nmc?.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 nmc?.initializeNew(); - // final wallets = await Hive.openBox(testWalletId); - - // expectLater(() => nmc?.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(() => nmc?.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( - // "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 nmc?.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 nmc?.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 nmc?.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 nmc?.recoverFromMnemonic( - // mnemonic: TEST_MNEMONIC, - // maxUnusedAddressGap: 2, - // maxNumberOfIndexesToCheck: 1000, - // height: 4000); - - // expect(await nmc?.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 nmc?.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.particl)) - // .thenAnswer((realInvocation) async {}); - - // when(client?.getBatchHistory(args: { - // "0": [ - // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - - // when(client?.getBatchHistory(args: { - // "0": [ - // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - - // when(client?.getBatchHistory(args: { - // "0": [ - // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - - // when(client?.getBatchHistory(args: { - // "0": [ - // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // when(client?.getBatchHistory(args: { - // "0": [ - // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // when(client?.getBatchHistory(args: { - // "0": [ - // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - - // final wallet = await Hive.openBox(testWalletId); - - // // restore so we have something to rescan - // await nmc?.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 nmc?.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(client?.getBatchHistory(args: { - // "0": [ - // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - // ] - // })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - // ] - // })).called(2); - - // verify(client?.getBatchHistory(args: { - // "0": [ - // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - // ] - // })).called(2); - - // verify(client?.getBatchHistory(args: { - // "0": [ - // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - // ] - // })).called(2); - - // verify(client?.getBatchHistory(args: { - // "0": [ - // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - // ] - // })).called(2); - - // verify(client?.getBatchHistory(args: { - // "0": [ - // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - // ] - // })).called(2); - // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - // .called(1); - - // // for (final arg in dynamicArgValues) { - // // final map = Map>.from(arg as Map); - // // Map argCount = {}; - // // - // // // verify(client?.getBatchHistory(args: map)).called(1); - // // // expect(activeScriptHashes.contains(map.values.first.first as String), - // // // true); - // // } - - // // Map argCount = {}; - // // - // // for (final arg in dynamicArgValues) { - // // final map = Map>.from(arg as Map); - // // - // // final str = jsonEncode(map); - // // - // // if (argCount[str] == null) { - // // argCount[str] = 1; - // // } else { - // // argCount[str] = argCount[str]! + 1; - // // } - // // } - // // - // // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); - - // 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(client?.getBatchHistory(args: { - // "0": [ - // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - - // when(client?.getBatchHistory(args: { - // "0": [ - // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - - // when(client?.getBatchHistory(args: { - // "0": [ - // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - - // when(client?.getBatchHistory(args: { - // "0": [ - // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - - // when(client?.getBatchHistory(args: { - // "0": [ - // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - - // when(client?.getBatchHistory(args: { - // "0": [ - // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - - // when(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - // .thenAnswer((realInvocation) async {}); - - // final wallet = await Hive.openBox(testWalletId); - - // // restore so we have something to rescan - // await nmc?.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 nmc?.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(client?.getBatchHistory(args: { - // "0": [ - // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - // ] - // })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - // ] - // })).called(1); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - // ] - // })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - // ] - // })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - // ] - // })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - // ] - // })).called(2); - // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - // .called(1); - - // expect(secureStore.writes, 19); - // expect(secureStore.reads, 32); - // expect(secureStore.deletes, 12); - - // 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: - // "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", - // coin: Coin.particl)) - // .thenAnswer((_) async => tx2Raw); - // when(cachedClient?.getTransaction( - // txHash: - // "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", - // coin: Coin.particl)) - // .thenAnswer((_) async => tx3Raw); - // when(cachedClient?.getTransaction( - // txHash: - // "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", - // coin: Coin.particl, - // )).thenAnswer((_) async => tx4Raw); - - // // recover to fill data - // await nmc?.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")); - - // nmc?.outputsList = utxoList; - - // bool didThrow = false; - // try { - // await nmc?.prepareSend( - // address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v", - // 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.particl, - // // callOutSideMainIsolate: false)) - // // .called(1); - // // verify(cachedClient?.getTransaction( - // // txHash: - // // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", - // // coin: Coin.particl, - // // callOutSideMainIsolate: false)) - // // .called(1); - // // verify(cachedClient?.getTransaction( - // // txHash: - // // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", - // // coin: Coin.particl, - // // 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 nmc?.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 nmc?.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 nmc?.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 nmc - // ?.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 nmc - // ?.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 { - // // // nmc = 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 = nmc.cachedElectrumXClient; - // // // - // // // // return when refresh is called on node changed trigger - // // // nmc.longMutex = true; - // // // - // // // GlobalEventBus.instance - // // // .fire(NodesChangedEvent(NodesChangedEventType.updatedCurrentNode)); - // // // - // // // // make sure event has processed before continuing - // // // await Future.delayed(Duration(seconds: 5)); - // // // - // // // final b = nmc.cachedElectrumXClient; - // // // - // // // expect(identical(a, b), false); - // // // - // // // await nmc.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 nmc?.recoverFromMnemonic( - // mnemonic: TEST_MNEMONIC, - // maxUnusedAddressGap: 2, - // maxNumberOfIndexesToCheck: 1000, - // height: 4000); - - // nmc?.refreshMutex = true; - - // await nmc?.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.particl: 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 nmc?.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 nmc?.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(); - // }); - // }); + group("Particl service class functions that depend on shared storage", () { + const testWalletId = "ParticltestWalletID"; + const testWalletName = "ParticlWallet"; + + bool hiveAdaptersRegistered = false; + + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; + + ParticlWallet? part; + + 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(); + + part = ParticlWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + // test("initializeWallet no network", () async { + // when(client?.ping()).thenAnswer((_) async => false); + // expect(await part?.initializeNew(), 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 nmc?.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 nmc?.initializeNew(); + // final wallets = await Hive.openBox(testWalletId); + // + // expectLater(() => nmc?.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(() => nmc?.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( + // "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 nmc?.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 nmc?.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 nmc?.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 nmc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000, + // height: 4000); + // + // expect(await nmc?.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 part?.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.particl)) + // .thenAnswer((realInvocation) async {}); + // + // when(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // + // when(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // + // when(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // + // when(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // when(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // when(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // + // final wallet = await Hive.openBox(testWalletId); + // + // // restore so we have something to rescan + // await nmc?.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 nmc?.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(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).called(2); + // + // verify(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).called(2); + // + // verify(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).called(2); + // + // verify(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).called(2); + // + // verify(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).called(2); + // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + // .called(1); + + // for (final arg in dynamicArgValues) { + // final map = Map>.from(arg as Map); + // Map argCount = {}; + // + // // verify(client?.getBatchHistory(args: map)).called(1); + // // expect(activeScriptHashes.contains(map.values.first.first as String), + // // true); + // } + + // Map argCount = {}; + // + // for (final arg in dynamicArgValues) { + // final map = Map>.from(arg as Map); + // + // final str = jsonEncode(map); + // + // if (argCount[str] == null) { + // argCount[str] = 1; + // } else { + // argCount[str] = argCount[str]! + 1; + // } + // } + // + // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); + + // 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(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // + // when(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // + // when(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // + // when(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // + // when(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // + // when(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).thenAnswer((realInvocation) async => {"0": []}); + // + // when(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + // .thenAnswer((realInvocation) async {}); + // + // final wallet = await Hive.openBox(testWalletId); + // + // // restore so we have something to rescan + // await nmc?.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 nmc?.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(client?.getBatchHistory(args: { + // "0": [ + // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" + // ] + // })).called(1); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" + // ] + // })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" + // ] + // })).called(2); + // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + // .called(1); + // + // expect(secureStore.writes, 19); + // expect(secureStore.reads, 32); + // expect(secureStore.deletes, 12); + // + // 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: + // "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", + // coin: Coin.particl)) + // .thenAnswer((_) async => tx2Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", + // coin: Coin.particl)) + // .thenAnswer((_) async => tx3Raw); + // when(cachedClient?.getTransaction( + // txHash: + // "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", + // coin: Coin.particl, + // )).thenAnswer((_) async => tx4Raw); + // + // // recover to fill data + // await nmc?.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")); + // + // nmc?.outputsList = utxoList; + // + // bool didThrow = false; + // try { + // await nmc?.prepareSend( + // address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v", + // 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.particl, + // // callOutSideMainIsolate: false)) + // // .called(1); + // // verify(cachedClient?.getTransaction( + // // txHash: + // // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // // coin: Coin.particl, + // // callOutSideMainIsolate: false)) + // // .called(1); + // // verify(cachedClient?.getTransaction( + // // txHash: + // // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // // coin: Coin.particl, + // // 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 nmc?.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 nmc?.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 nmc?.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 nmc + // ?.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 nmc + // ?.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 { + // // nmc = 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 = nmc.cachedElectrumXClient; + // // + // // // return when refresh is called on node changed trigger + // // nmc.longMutex = true; + // // + // // GlobalEventBus.instance + // // .fire(NodesChangedEvent(NodesChangedEventType.updatedCurrentNode)); + // // + // // // make sure event has processed before continuing + // // await Future.delayed(Duration(seconds: 5)); + // // + // // final b = nmc.cachedElectrumXClient; + // // + // // expect(identical(a, b), false); + // // + // // await nmc.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 nmc?.recoverFromMnemonic( + // mnemonic: TEST_MNEMONIC, + // maxUnusedAddressGap: 2, + // maxNumberOfIndexesToCheck: 1000, + // height: 4000); + // + // nmc?.refreshMutex = true; + // + // await nmc?.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.particl: 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 nmc?.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 nmc?.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/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index 91c3e5bfa..fb0f50d79 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -1,5 +1,5 @@ // Mocks generated by Mockito 5.3.2 from annotations -// in stackwallet/test/services/coins/namecoin/namecoin_wallet_test.dart. +// in stackwallet/test/services/coins/particl/particl_wallet_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 50f906c3c..117a810cf 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -165,9 +165,9 @@ class _FakeElectrumXNode_11 extends _i1.SmartFake ); } -class _FakeFlutterSecureStorageInterface_12 extends _i1.SmartFake +class _FakeSecureStorageInterface_12 extends _i1.SmartFake implements _i12.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_12( + _FakeSecureStorageInterface_12( Object parent, Invocation parentInvocation, ) : super( @@ -1400,7 +1400,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override _i12.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_12( + returnValue: _FakeSecureStorageInterface_12( this, Invocation.getter(#secureStorageInterface), ), diff --git a/test/widget_tests/node_card_test.mocks.dart b/test/widget_tests/node_card_test.mocks.dart index 2bb32b58d..6b85a4f5d 100644 --- a/test/widget_tests/node_card_test.mocks.dart +++ b/test/widget_tests/node_card_test.mocks.dart @@ -24,9 +24,9 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake +class _FakeSecureStorageInterface_0 extends _i1.SmartFake implements _i2.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_0( + _FakeSecureStorageInterface_0( Object parent, Invocation parentInvocation, ) : super( @@ -46,7 +46,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_0( + returnValue: _FakeSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index c9e4e2bb8..7e3e3a92d 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -76,9 +76,9 @@ class _FakeManager_3 extends _i1.SmartFake implements _i6.Manager { ); } -class _FakeFlutterSecureStorageInterface_4 extends _i1.SmartFake +class _FakeSecureStorageInterface_4 extends _i1.SmartFake implements _i7.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_4( + _FakeSecureStorageInterface_4( Object parent, Invocation parentInvocation, ) : super( @@ -446,6 +446,19 @@ class MockPrefs extends _i1.Mock implements _i11.Prefs { returnValueForMissingStub: null, ); @override + int get familiarity => (super.noSuchMethod( + Invocation.getter(#familiarity), + returnValue: 0, + ) as int); + @override + set familiarity(int? familiarity) => super.noSuchMethod( + Invocation.setter( + #familiarity, + familiarity, + ), + returnValueForMissingStub: null, + ); + @override bool get showTestNetCoins => (super.noSuchMethod( Invocation.getter(#showTestNetCoins), returnValue: false, @@ -623,10 +636,9 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i7.SecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i7.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_4( + returnValue: _FakeSecureStorageInterface_4( this, Invocation.getter(#secureStorageInterface), ), diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index f258a402b..2aa2e1dcb 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -2213,6 +2213,19 @@ class MockPrefs extends _i1.Mock implements _i17.Prefs { returnValueForMissingStub: null, ); @override + int get familiarity => (super.noSuchMethod( + Invocation.getter(#familiarity), + returnValue: 0, + ) as int); + @override + set familiarity(int? familiarity) => super.noSuchMethod( + Invocation.setter( + #familiarity, + familiarity, + ), + returnValueForMissingStub: null, + ); + @override bool get showTestNetCoins => (super.noSuchMethod( Invocation.getter(#showTestNetCoins), returnValue: false, diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index a249c997d..fe5e0e8a2 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -164,9 +164,9 @@ class _FakeElectrumXNode_11 extends _i1.SmartFake ); } -class _FakeFlutterSecureStorageInterface_12 extends _i1.SmartFake +class _FakeSecureStorageInterface_12 extends _i1.SmartFake implements _i12.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_12( + _FakeSecureStorageInterface_12( Object parent, Invocation parentInvocation, ) : super( @@ -1337,7 +1337,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override _i12.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_12( + returnValue: _FakeSecureStorageInterface_12( this, Invocation.getter(#secureStorageInterface), ), diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 820fbd96d..7f370eb9a 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -164,9 +164,9 @@ class _FakeElectrumXNode_11 extends _i1.SmartFake ); } -class _FakeFlutterSecureStorageInterface_12 extends _i1.SmartFake +class _FakeSecureStorageInterface_12 extends _i1.SmartFake implements _i12.SecureStorageInterface { - _FakeFlutterSecureStorageInterface_12( + _FakeSecureStorageInterface_12( Object parent, Invocation parentInvocation, ) : super( @@ -1337,7 +1337,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override _i12.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), - returnValue: _FakeFlutterSecureStorageInterface_12( + returnValue: _FakeSecureStorageInterface_12( this, Invocation.getter(#secureStorageInterface), ), From 4dec3fbf48b5acbe93b17ca99d54cad9ca08314d Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 30 Nov 2022 16:05:56 +0200 Subject: [PATCH 041/103] Update tests that use batchHistory --- .../particl/particl_history_sample_data.dart | 2 - .../coins/particl/particl_wallet_test.dart | 1610 ++++++++--------- 2 files changed, 794 insertions(+), 818 deletions(-) diff --git a/test/services/coins/particl/particl_history_sample_data.dart b/test/services/coins/particl/particl_history_sample_data.dart index b2e5f9fa7..620453178 100644 --- a/test/services/coins/particl/particl_history_sample_data.dart +++ b/test/services/coins/particl/particl_history_sample_data.dart @@ -1,5 +1,3 @@ -// TODO these test vectors are valid for Namecoin: update for Particl - final Map> historyBatchArgs0 = { "k_0_0": ["29e9e6410954dea9e527a0d2cac5de4dea5fb600b719badff90d6d43518d3ed8"], "k_0_1": ["9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc"], diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 844b61461..048c8eeb1 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:decimal/decimal.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; @@ -11,6 +14,7 @@ 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 'particl_history_sample_data.dart'; import 'particl_wallet_test.mocks.dart'; @@ -281,215 +285,215 @@ void main() { // }); // }); - // group("basic getters, setters, and functions", () { - // final testWalletId = "NMCtestWalletID"; - // final testWalletName = "NMCWallet"; + group("basic getters, setters, and functions", () { + final testWalletId = "ParticltestWalletID"; + final testWalletName = "ParticlWallet"; - // MockElectrumX? client; - // MockCachedElectrumX? cachedClient; - // MockPriceAPI? priceAPI; - // late FakeSecureStorage secureStore; - // MockTransactionNotificationTracker? tracker; + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; - // NamecoinWallet? nmc; + ParticlWallet? part; - // setUp(() async { - // client = MockElectrumX(); - // cachedClient = MockCachedElectrumX(); - // priceAPI = MockPriceAPI(); - // secureStore = FakeSecureStorage(); - // tracker = MockTransactionNotificationTracker(); + setUp(() async { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); - // nmc = NamecoinWallet( - // walletId: testWalletId, - // walletName: testWalletName, - // coin: Coin.particl, - // client: client!, - // cachedClient: cachedClient!, - // tracker: tracker!, - // priceAPI: priceAPI, - // secureStore: secureStore, - // ); - // }); + part = ParticlWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); - // test("get networkType main", () async { - // expect(Coin.particl, Coin.particl); - // expect(secureStore.interactions, 0); - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + test("get networkType main", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); - // test("get networkType test", () async { - // nmc = NamecoinWallet( - // walletId: testWalletId, - // walletName: testWalletName, - // coin: Coin.particl, - // client: client!, - // cachedClient: cachedClient!, - // tracker: tracker!, - // priceAPI: priceAPI, - // secureStore: secureStore, - // ); - // expect(Coin.particl, Coin.particl); - // expect(secureStore.interactions, 0); - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + test("get networkType test", () async { + part = ParticlWallet( + walletId: testWalletId, + walletName: testWalletName, + coin: Coin.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); - // test("get cryptoCurrency", () async { - // expect(Coin.particl, Coin.particl); - // expect(secureStore.interactions, 0); - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + test("get cryptoCurrency", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); - // test("get coinName", () async { - // expect(Coin.particl, Coin.particl); - // expect(secureStore.interactions, 0); - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + test("get coinName", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); - // test("get coinTicker", () async { - // expect(Coin.particl, Coin.particl); - // expect(secureStore.interactions, 0); - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + test("get coinTicker", () async { + expect(Coin.particl, Coin.particl); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); - // test("get and set walletName", () async { - // expect(Coin.particl, Coin.particl); - // nmc?.walletName = "new name"; - // expect(nmc?.walletName, "new name"); - // expect(secureStore.interactions, 0); - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + test("get and set walletName", () async { + expect(Coin.particl, Coin.particl); + part?.walletName = "new name"; + expect(part?.walletName, "new name"); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); - // test("estimateTxFee", () async { - // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); - // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); - // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); - // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); - // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); - // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); - // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); - // expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 12345), 4628); - // expect(secureStore.interactions, 0); - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + // test("estimateTxFee", () async { + // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); + // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); + // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); + // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); + // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); + // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); + // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); + // expect(part?.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_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)) - // .thenAnswer((realInvocation) async => Decimal.ten); + // 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_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)) + // .thenAnswer((realInvocation) async => Decimal.ten); + // + // final fees = await part?.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); + // }); - // final fees = await nmc?.fees; - // expect(fees, isA()); - // expect(fees?.slow, 1000000000); - // expect(fees?.medium, 100000000); - // expect(fees?.fast, 0); + // 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 part?.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); + // }); - // 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 nmc?.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 nmc?.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); - // // }); - // }); + // 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 nmc?.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("Particl service class functions that depend on shared storage", () { const testWalletId = "ParticltestWalletID"; @@ -866,486 +870,461 @@ void main() { 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.particl)) - // .thenAnswer((realInvocation) async {}); - // - // when(client?.getBatchHistory(args: { - // "0": [ - // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // - // when(client?.getBatchHistory(args: { - // "0": [ - // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // - // when(client?.getBatchHistory(args: { - // "0": [ - // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // - // when(client?.getBatchHistory(args: { - // "0": [ - // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // when(client?.getBatchHistory(args: { - // "0": [ - // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // when(client?.getBatchHistory(args: { - // "0": [ - // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // - // final wallet = await Hive.openBox(testWalletId); - // - // // restore so we have something to rescan - // await nmc?.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 nmc?.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(client?.getBatchHistory(args: { - // "0": [ - // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - // ] - // })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - // ] - // })).called(2); - // - // verify(client?.getBatchHistory(args: { - // "0": [ - // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - // ] - // })).called(2); - // - // verify(client?.getBatchHistory(args: { - // "0": [ - // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - // ] - // })).called(2); - // - // verify(client?.getBatchHistory(args: { - // "0": [ - // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - // ] - // })).called(2); - // - // verify(client?.getBatchHistory(args: { - // "0": [ - // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - // ] - // })).called(2); - // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - // .called(1); + 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.particl)) + .thenAnswer((realInvocation) async {}); - // for (final arg in dynamicArgValues) { - // final map = Map>.from(arg as Map); - // Map argCount = {}; - // - // // verify(client?.getBatchHistory(args: map)).called(1); - // // expect(activeScriptHashes.contains(map.values.first.first as String), - // // true); - // } + when(client?.getBatchHistory(args: { + "0": [ + "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" + ] + })).thenAnswer((realInvocation) async => {"0": []}); - // Map argCount = {}; - // - // for (final arg in dynamicArgValues) { - // final map = Map>.from(arg as Map); - // - // final str = jsonEncode(map); - // - // if (argCount[str] == null) { - // argCount[str] = 1; - // } else { - // argCount[str] = argCount[str]! + 1; - // } - // } - // - // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); + when(client?.getBatchHistory(args: { + "0": [ + "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339" + ] + })).thenAnswer((realInvocation) async => {"0": []}); - // expect(secureStore.writes, 25); - // expect(secureStore.reads, 32); - // expect(secureStore.deletes, 6); - // - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + when(client?.getBatchHistory(args: { + "0": [ + "b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3" + ] + })).thenAnswer((realInvocation) async => {"0": []}); - // 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(client?.getBatchHistory(args: { - // "0": [ - // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // - // when(client?.getBatchHistory(args: { - // "0": [ - // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // - // when(client?.getBatchHistory(args: { - // "0": [ - // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // - // when(client?.getBatchHistory(args: { - // "0": [ - // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // - // when(client?.getBatchHistory(args: { - // "0": [ - // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // - // when(client?.getBatchHistory(args: { - // "0": [ - // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - // ] - // })).thenAnswer((realInvocation) async => {"0": []}); - // - // when(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - // .thenAnswer((realInvocation) async {}); - // - // final wallet = await Hive.openBox(testWalletId); - // - // // restore so we have something to rescan - // await nmc?.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 nmc?.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(client?.getBatchHistory(args: { - // "0": [ - // "dd63fc12f5e6c1ada2cf3c941d1648e6d561ce4024747bb2117d72112d83287c" - // ] - // })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "cd3dd4abe4f9efc7149ba334d2d6790020331805b0bd5c7ed89a3ac6a22f10b9" - // ] - // })).called(1); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "42d6e40636f4740f9c7f95ef0bbc2a4c17f54da2bc98a32a622e2bf73eb675c3" - // ] - // })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "587943864cefed4f1643a5ee2ce2b3c13a0c6ad7c435373f0ac328e144a15c1e" - // ] - // })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "86906979fc9107d06d560275d7de8305b69d7189c3206ac9070ad76e6abff874" - // ] - // })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "c068e7fa4aa0b8a63114f6d11c047ca4be6a8fa333eb0dac48506e8f150af73b" - // ] - // })).called(2); - // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) - // .called(1); - // - // expect(secureStore.writes, 19); - // expect(secureStore.reads, 32); - // expect(secureStore.deletes, 12); - // - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + when(client?.getBatchHistory(args: { + "0": [ + "0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + when(client?.getBatchHistory(args: { + "0": [ + "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + when(client?.getBatchHistory(args: { + "0": [ + "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + final wallet = await Hive.openBox(testWalletId); + + // restore so we have something to rescan + await part?.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 part?.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(client?.getBatchHistory(args: { + "0": [ + "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc" + ] + })).called(2); + + verify(client?.getBatchHistory(args: { + "0": [ + "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" + ] + })).called(2); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .called(1); + + 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(client?.getBatchHistory(args: { + "0": [ + "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(client?.getBatchHistory(args: { + "0": [ + "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" + ] + })).thenAnswer((realInvocation) async => {"0": []}); + + when(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .thenAnswer((realInvocation) async {}); + + final wallet = await Hive.openBox(testWalletId); + + // restore so we have something to rescan + await part?.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 part?.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(client?.getBatchHistory(args: { + "0": [ + "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a" + ] + })).called(2); + verify(client?.getBatchHistory(args: { + "0": [ + "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc" + ] + })).called(1); + verify(client?.getBatchHistory(args: { + "0": [ + "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" + ] + })).called(2); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .called(1); + + expect(secureStore.writes, 19); + expect(secureStore.reads, 32); + expect(secureStore.deletes, 12); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); // test("prepareSend fails", () async { // when(client?.getServerFeatures()).thenAnswer((_) async => { @@ -1636,150 +1615,149 @@ void main() { // // 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 nmc?.recoverFromMnemonic( - // mnemonic: TEST_MNEMONIC, - // maxUnusedAddressGap: 2, - // maxNumberOfIndexesToCheck: 1000, - // height: 4000); - // - // nmc?.refreshMutex = true; - // - // await nmc?.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.particl: 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 nmc?.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 nmc?.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(); - // }); + 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 part?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + part?.refreshMutex = true; + + await part?.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.particl: 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 part?.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 part?.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(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); + + tearDown(() async { + await tearDownTestHive(); + }); }); } From 3be4be11dda5482449bbdfd6a54b185c73523491 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 30 Nov 2022 08:24:21 -0800 Subject: [PATCH 042/103] Use flutter version 3.3.4 --- README.md | 2 +- scripts/setup.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 37321c848..9d0f6ff6d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Highlights include: The following prerequisities can be installed with the setup script `scripts/setup.sh` or manually as described below: -- Flutter 3.0.5 [(install manually or with git, do not install with snap)](https://docs.flutter.dev/get-started/install) +- Flutter 3.3.4 [(install manually or with git, do not install with snap)](https://docs.flutter.dev/get-started/install) - Dart SDK Requirement (>=2.17.0, up until <3.0.0) - Android setup ([Android Studio](https://developer.android.com/studio) and subsequent dependencies) diff --git a/scripts/setup.sh b/scripts/setup.sh index 5525d7814..d9c716546 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -12,7 +12,7 @@ sudo apt install -y unzip pkg-config clang cmake ninja-build libgtk-3-dev cd $DEVELOPMENT git clone https://github.com/flutter/flutter.git cd flutter -git checkout 3.0.3 +git checkout 3.3.4 export FLUTTER_DIR=$(pwd)/bin echo 'export PATH="$PATH:'${FLUTTER_DIR}'"' >> ~/.bashrc source ~/.bashrc From b575418e8fd9748baa62f060a47040c1cd26520c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 30 Nov 2022 10:43:39 -0600 Subject: [PATCH 043/103] add litecoin back to coingecko call --- lib/services/price.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/price.dart b/lib/services/price.dart index da0c9e68d..43751c141 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -87,7 +87,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,bitcoin-cash,namecoin,wownero,particl&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,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&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"); From 6d9978c6e1d4f15d12fec43deb37ad333f1f830d Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 14 Nov 2022 11:04:58 -0800 Subject: [PATCH 044/103] Initial Dockerfile for linux desktop build. Update dockerfile Run prebuild script. Will document adding env varibales for API keys. Update linux Dockerfile to fix cmake-data package version and install latest tomli Specify version numbers of packages in Linux Dockerfile. Update flutter version to 3.3.4 in Dockerfile. --- dockerfile.linux | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 dockerfile.linux diff --git a/dockerfile.linux b/dockerfile.linux new file mode 100644 index 000000000..0853741d9 --- /dev/null +++ b/dockerfile.linux @@ -0,0 +1,17 @@ +FROM ubuntu:20.04 as base +COPY . /stack_wallet +WORKDIR /stack_wallet/scripts/linux +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y git=1:2.25.1-1ubuntu3.6 make=4.2.1-1.2 curl=7.68.0-1ubuntu2.14 cargo=0.62.0ubuntu0libgit2-0ubuntu0.20.04.1 \ + file=1:5.38-4 ca-certificates=20211016~20.04.1 cmake=3.16.3-1ubuntu1.20.04.1 cmake-data=3.16.3-1ubuntu1.20.04.1 g++=4:9.3.0-1ubuntu2 libgmp-dev=2:6.2.0+dfsg-4ubuntu0.1 libssl-dev=1.1.1f-1ubuntu2.16 libclang-dev=1:10.0-50~exp1 \ + unzip=6.0-25ubuntu1.1 python3=3.8.2-0ubuntu2 pkg-config=0.29.1-0ubuntu4 libglib2.0-dev=2.64.6-1~ubuntu20.04.4 libgcrypt20-dev=1.8.5-5ubuntu1.1 gettext-base=0.19.8.1-10build1 libgirepository1.0-dev=1.64.1-1~ubuntu20.04.1 \ + valac=0.48.6-0ubuntu1 xsltproc=1.1.34-4ubuntu0.20.04.1 docbook-xsl=1.79.1+dfsg-2 python3-pip=20.0.2-5ubuntu1.6 ninja-build=1.10.0-1build1 clang=1:10.0-50~exp1 libgtk-3-dev=3.24.20-0ubuntu1.1 \ + libunbound-dev=1.9.4-2ubuntu1.4 libzmq3-dev=4.3.2-2ubuntu1 libtool=2.4.6-14 autoconf=2.69-11.1 automake=1:1.16.1-4ubuntu6 bison=2:3.5.1+dfsg-1 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && pip3 install --upgrade meson==0.64.1 markdown==3.4.1 markupsafe==2.1.1 jinja2==3.1.2 pygments==2.13.0 toml==0.10.2 typogrify==2.0.7 tomli==2.0.1 && cd .. && ./prebuild.sh && cd linux && ./build_all.sh +WORKDIR / +RUN git clone https://github.com/flutter/flutter.git -b 3.3.4 +ENV PATH "$PATH:/flutter/bin" +WORKDIR /stack_wallet +RUN flutter pub get Linux && flutter build linux +ENTRYPOINT ["/bin/bash"] From f0f008471cfb487ca51bf89c2a00788329672ddc Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 30 Nov 2022 13:28:12 -0600 Subject: [PATCH 045/103] add isParticl param to relevant bitcoindart calls --- lib/services/coins/particl/particl_wallet.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index d15053415..73e134861 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -3308,7 +3308,7 @@ class ParticlWallet extends CoinServiceAPI { 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); + utxoSigningData[txid]["output"] as Uint8List, '', true); } // Add transaction output @@ -3321,11 +3321,11 @@ class ParticlWallet extends CoinServiceAPI { 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?, - ); + vin: i, + keyPair: utxoSigningData[txid]["keyPair"] as ECPair, + witnessValue: utxosToUse[i].value, + redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + isParticl: true); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", From 3d0c5092cbd30fca0be227ce64ecb935cca37c14 Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 1 Dec 2022 17:07:43 +0200 Subject: [PATCH 046/103] Update, uncomment signing tx --- lib/services/coins/particl/particl_wallet.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 73e134861..27c129284 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -3302,17 +3302,19 @@ class ParticlWallet extends CoinServiceAPI { .log("Starting buildTransaction ----------", level: LogLevel.Info); final txb = TransactionBuilder(network: _network); - txb.setVersion(1); + txb.setVersion(160); // 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, '', true); + utxoSigningData[txid]["output"] as Uint8List, ''); } // Add transaction output for (var i = 0; i < recipients.length; i++) { + print("RECIPIENT IS ${recipients[i]}"); + print("AMOINT IS ${satoshiAmounts[i]}"); txb.addOutput(recipients[i], satoshiAmounts[i], particl.bech32!); } @@ -3324,8 +3326,7 @@ class ParticlWallet extends CoinServiceAPI { vin: i, keyPair: utxoSigningData[txid]["keyPair"] as ECPair, witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, - isParticl: true); + redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", @@ -3333,9 +3334,11 @@ class ParticlWallet extends CoinServiceAPI { rethrow; } - final builtTx = txb.build(); + final builtTx = txb.buildIncomplete(); final vSize = builtTx.virtualSize(); + print("BUILT TX IS ${builtTx.toHex()}"); + return {"hex": builtTx.toHex(), "vSize": vSize}; } From 3d5e8812e1f2f2ce717b9aa223014dd2496e963a Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 1 Dec 2022 22:19:32 +0200 Subject: [PATCH 047/103] WIP: DEbugging txinwitness data --- .../coins/particl/particl_wallet.dart | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 27c129284..d89d78bdb 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -2725,7 +2725,7 @@ class ParticlWallet extends CoinServiceAPI { final List availableOutputs = utxos ?? outputsList; final List spendableOutputs = []; int spendableSatoshiValue = 0; - + print("AVAILABLE UTXOS IS ::::: ${availableOutputs}"); // Build list of spendable outputs and totaling their satoshi amount for (var i = 0; i < availableOutputs.length; i++) { if (availableOutputs[i].blocked == false && @@ -3075,6 +3075,7 @@ class ParticlWallet extends CoinServiceAPI { for (final output in tx["vout"] as List) { final n = output["n"]; if (n != null && n == utxosToUse[i].vout) { + print("SCRIPT PUB KEY IS ${output["scriptPubKey"]}"); final address = output["scriptPubKey"]["address"] as String; if (!addressTxid.containsKey(address)) { addressTxid[address] = []; @@ -3156,6 +3157,7 @@ class ParticlWallet extends CoinServiceAPI { // p2sh / bip49 final p2shLength = addressesP2SH.length; if (p2shLength > 0) { + print("THIS P2SH IS NOT NULL"); final receiveDerivations = await _fetchDerivations( chain: 0, derivePathType: DerivePathType.bip49, @@ -3192,6 +3194,7 @@ class ParticlWallet extends CoinServiceAPI { }; } } else { + print("THIS IS WHERE ITS AT - CHANGE p2wpkh"); // if its not a receive, check change final changeDerivation = changeDerivations[addressesP2SH[i]]; // if a match exists it will not be null @@ -3282,7 +3285,8 @@ class ParticlWallet extends CoinServiceAPI { } } } - + Logging.instance.log("FETCHED TX BUILD DATA IS -----$results", + level: LogLevel.Info, printFullLength: true); return results; } catch (e, s) { Logging.instance @@ -3301,11 +3305,17 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance .log("Starting buildTransaction ----------", level: LogLevel.Info); + Logging.instance.log("UTXOs SIGNING DATA IS -----$utxoSigningData", + level: LogLevel.Info, printFullLength: true); + final txb = TransactionBuilder(network: _network); txb.setVersion(160); // Add transaction inputs for (var i = 0; i < utxosToUse.length; i++) { + Logging.instance.log("UTXOs TO USE IS -----${utxosToUse[i].vout}", + level: LogLevel.Info, printFullLength: true); + final txid = utxosToUse[i].txid; txb.addInput(txid, utxosToUse[i].vout, null, utxoSigningData[txid]["output"] as Uint8List, ''); @@ -3313,8 +3323,6 @@ class ParticlWallet extends CoinServiceAPI { // Add transaction output for (var i = 0; i < recipients.length; i++) { - print("RECIPIENT IS ${recipients[i]}"); - print("AMOINT IS ${satoshiAmounts[i]}"); txb.addOutput(recipients[i], satoshiAmounts[i], particl.bech32!); } @@ -3322,6 +3330,16 @@ class ParticlWallet extends CoinServiceAPI { // Sign the transaction accordingly for (var i = 0; i < utxosToUse.length; i++) { final txid = utxosToUse[i].txid; + Logging.instance.log("WITNESS VALUE IS -----${utxosToUse[i].value}", + level: LogLevel.Info, printFullLength: true); + + Logging.instance.log( + "REDEEM SCRIPT IS -----${utxoSigningData[txid]["redeemScript"]}", + level: LogLevel.Info, + printFullLength: true); + + Logging.instance.log("AND THIS DATA IS -----${utxoSigningData[txid]}", + level: LogLevel.Info, printFullLength: true); txb.sign( vin: i, keyPair: utxoSigningData[txid]["keyPair"] as ECPair, @@ -3334,7 +3352,7 @@ class ParticlWallet extends CoinServiceAPI { rethrow; } - final builtTx = txb.buildIncomplete(); + final builtTx = txb.build(); final vSize = builtTx.virtualSize(); print("BUILT TX IS ${builtTx.toHex()}"); From 8e2c8c8b53bfcd66ec7d7b5f47759c9664c85d8c Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 2 Dec 2022 20:52:38 +0200 Subject: [PATCH 048/103] WIP: trim hex at beginning --- lib/services/coins/particl/particl_wallet.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index d89d78bdb..1af063e8f 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -3355,9 +3355,22 @@ class ParticlWallet extends CoinServiceAPI { final builtTx = txb.build(); final vSize = builtTx.virtualSize(); - print("BUILT TX IS ${builtTx.toHex()}"); + print("BUILT TX IS ${builtTx.toHex().toString()}"); + String hexBefore = builtTx.toHex(); + if (builtTx.toHex().toString().endsWith('0000')) { + // print("END WITH ZERO"); + String stripped = hexBefore.substring(0, hexBefore.length - 4); + return {"hex": stripped, "vSize": vSize}; + // } else { + // print("DOES NOT END WITH ZERO"); + // return {"hex": builtTx.toHex(), "vSize": vSize}; + } return {"hex": builtTx.toHex(), "vSize": vSize}; + + // print("AND NOW IT IS $stripped"); + // + // return {"hex": stripped, "vSize": vSize}; } @override From b1d2d1ce26b6dc1bd5f466b40f7a0ab94391111d Mon Sep 17 00:00:00 2001 From: likho Date: Sun, 4 Dec 2022 16:17:41 +0200 Subject: [PATCH 049/103] Remove default bip84 for address to get bubKey type addresses and WIP: Fix TX signing --- .../coins/particl/particl_wallet.dart | 1286 ++++++----------- 1 file changed, 466 insertions(+), 820 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 1af063e8f..c141b72f1 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -50,15 +50,10 @@ const String GENESIS_HASH_MAINNET = const String GENESIS_HASH_TESTNET = "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; -enum DerivePathType { bip44, bip49, bip84 } +enum DerivePathType { bip44, bip49 } -bip32.BIP32 getBip32Node( - int chain, - int index, - String mnemonic, - NetworkType network, - DerivePathType derivePathType, -) { +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); @@ -79,11 +74,7 @@ bip32.BIP32 getBip32NodeWrapper( } bip32.BIP32 getBip32NodeFromRoot( - int chain, - int index, - bip32.BIP32 root, - DerivePathType derivePathType, -) { + int chain, int index, bip32.BIP32 root, DerivePathType derivePathType) { String coinType; switch (root.network.wif) { case 0x6c: // PART mainnet wif @@ -97,8 +88,6 @@ bip32.BIP32 getBip32NodeFromRoot( 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."); } @@ -138,7 +127,6 @@ bip32.BIP32 getBip32RootWrapper(Tuple2 args) { class ParticlWallet extends CoinServiceAPI { static const integrationTestFlag = bool.fromEnvironment("IS_INTEGRATION_TEST"); - final _prefs = Prefs.instance; Timer? timer; @@ -151,30 +139,12 @@ class ParticlWallet extends CoinServiceAPI { case Coin.particl: return particl; default: - throw Exception("Invalid network type!"); + throw Exception("Particl network type not set!"); } } 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; @@ -228,18 +198,14 @@ class ParticlWallet extends CoinServiceAPI { } @override - Future get currentReceivingAddress => _currentReceivingAddress ??= - _getCurrentAddressForChain(0, DerivePathType.bip84); - Future? _currentReceivingAddress; - - Future get currentLegacyReceivingAddress => + Future get currentReceivingAddress => _currentReceivingAddressP2PKH ??= _getCurrentAddressForChain(0, DerivePathType.bip44); Future? _currentReceivingAddressP2PKH; - Future get currentReceivingAddressP2SH => - _currentReceivingAddressP2SH ??= - _getCurrentAddressForChain(0, DerivePathType.bip49); + // Future get currentReceivingAddressP2SH => + // _currentReceivingAddressP2SH ??= + // _getCurrentAddressForChain(0, DerivePathType.bip49); Future? _currentReceivingAddressP2SH; @override @@ -261,9 +227,9 @@ class ParticlWallet extends CoinServiceAPI { @override Future get maxFee async { - final fee = (await fees).fast as String; - final satsFee = - Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin(coin)); + final fee = (await fees).fast; + final satsFee = Format.satoshisToAmount(fee, coin: coin) * + Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -281,7 +247,7 @@ class ParticlWallet extends CoinServiceAPI { } } - int get storedChainHeight { + Future get storedChainHeight async { final storedHeight = DB.instance .get(boxName: walletId, key: "storedChainHeight") as int?; return storedHeight ?? 0; @@ -323,7 +289,7 @@ class ParticlWallet extends CoinServiceAPI { throw ArgumentError('Invalid address version'); } // P2WPKH - return DerivePathType.bip84; + throw ArgumentError('$address has no matching Script'); } } @@ -350,20 +316,10 @@ class ParticlWallet extends CoinServiceAPI { throw Exception("genesis hash does not match main net!"); } break; - break; default: throw Exception( - "Attempted to generate a ParticlWallet using a non particl coin type: ${coin.name}"); + "Attempted to generate a ParticlWallet using a non bch 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 @@ -393,6 +349,183 @@ class ParticlWallet extends CoinServiceAPI { level: LogLevel.Info); } + Future _recoverWalletFromBIP32SeedPhrase({ + required String mnemonic, + int maxUnusedAddressGap = 20, + int maxNumberOfIndexesToCheck = 1000, + }) async { + longMutex = true; + + Map> p2pkhReceiveDerivations = {}; + Map> p2shReceiveDerivations = {}; + Map> p2pkhChangeDerivations = {}; + Map> p2shChangeDerivations = {}; + + final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); + + List p2pkhReceiveAddressArray = []; + List p2shReceiveAddressArray = []; + int p2pkhReceiveIndex = -1; + int p2shReceiveIndex = -1; + + List p2pkhChangeAddressArray = []; + List p2shChangeAddressArray = []; + int p2pkhChangeIndex = -1; + int p2shChangeIndex = -1; + + // The gap limit will be capped at [maxUnusedAddressGap] + // int receivingGapCounter = 0; + // int changeGapCounter = 0; + + // actual size is 24 due to p2pkh and p2sh so 12x2 + 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); + + 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); + + await Future.wait( + [resultReceive44, resultReceive49, resultChange44, resultChange49]); + + 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>; + + 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>; + + // 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 (p2pkhChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhChangeDerivations); + } + if (p2shChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip49, + derivationsToAdd: p2shChangeDerivations); + } + + // 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 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; + } + + 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: '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.Info); + + longMutex = false; + rethrow; + } + } + Future> _checkGaps( int maxNumberOfIndexesToCheck, int maxUnusedAddressGap, @@ -446,13 +579,6 @@ class ParticlWallet extends CoinServiceAPI { .data .address!; break; - case DerivePathType.bip84: - address = P2WPKH( - network: _network, - data: PaymentData(pubkey: node.publicKey)) - .data - .address!; - break; default: throw Exception("No Path type $type exists"); } @@ -469,7 +595,9 @@ class ParticlWallet extends CoinServiceAPI { // get address tx counts final counts = await _getBatchTxCount(addresses: txCountCallArgs); - + if (kDebugMode) { + print("Counts $counts"); + } // check and add appropriate addresses for (int k = 0; k < txCountBatchSize; k++) { int count = counts["${_id}_$k"]!; @@ -525,249 +653,6 @@ class ParticlWallet extends CoinServiceAPI { } } - 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; @@ -775,6 +660,9 @@ class ParticlWallet extends CoinServiceAPI { try { bool needsRefresh = false; + Logging.instance.log( + "notified unconfirmed transactions: ${txTracker.pendings}", + level: LogLevel.Info); Set txnsToCheck = {}; for (final String txid in txTracker.pendings) { @@ -785,7 +673,8 @@ class ParticlWallet extends CoinServiceAPI { for (String txid in txnsToCheck) { final txn = await electrumXClient.getTransaction(txHash: txid); - int confirmations = txn["confirmations"] as int? ?? 0; + var confirmations = txn["confirmations"]; + if (confirmations is! int) continue; bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; if (!isUnconfirmed) { // unconfirmedTxs = {}; @@ -813,7 +702,7 @@ class ParticlWallet extends CoinServiceAPI { } catch (e, s) { Logging.instance.log( "Exception caught in refreshIfThereIsNewData: $e\n$s", - level: LogLevel.Error); + level: LogLevel.Info); rethrow; } } @@ -825,16 +714,15 @@ class ParticlWallet extends CoinServiceAPI { List unconfirmedTxnsToNotifyPending = []; List unconfirmedTxnsToNotifyConfirmed = []; + // Get all unconfirmed incoming transactions 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); } @@ -842,35 +730,39 @@ class ParticlWallet extends CoinServiceAPI { } } - // notify on unconfirmed transactions + // notify on new incoming transaction 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, - )); + unawaited( + 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") { - 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, - )); + 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); } } @@ -878,31 +770,38 @@ class ParticlWallet extends CoinServiceAPI { // 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, - )); + unawaited( + 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") { - 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, - )); + unawaited( + 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 @@ -923,11 +822,6 @@ class ParticlWallet extends CoinServiceAPI { } } - @override - bool get isRefreshing => refreshMutex; - - bool refreshMutex = false; - //TODO Show percentages properly/more consistently /// Refreshes display data for the wallet @override @@ -964,16 +858,14 @@ class ParticlWallet extends CoinServiceAPI { if (currentHeight != storedHeight) { if (currentHeight != -1) { // -1 failed to fetch current height - unawaited(updateStoredChainHeight(newHeight: currentHeight)); + await updateStoredChainHeight(newHeight: currentHeight); } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); - final changeAddressForTransactions = - _checkChangeAddressForTransactions(DerivePathType.bip84); + await _checkChangeAddressForTransactions(DerivePathType.bip44); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); - final currentReceivingAddressesForTransactions = - _checkCurrentReceivingAddressesForTransactions(); + await _checkCurrentReceivingAddressesForTransactions(); final newTxData = _fetchTransactionData(); GlobalEventBus.instance @@ -993,20 +885,11 @@ class ParticlWallet extends CoinServiceAPI { GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.80, walletId)); - final allTxsToWatch = getAllTxsToWatch(await newTxData); - await Future.wait([ - newTxData, - changeAddressForTransactions, - currentReceivingAddressesForTransactions, - newUtxoData, - feeObj, - allTxsToWatch, - ]); + await getAllTxsToWatch(await newTxData); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.90, walletId)); } - refreshMutex = false; GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( @@ -1015,12 +898,10 @@ class ParticlWallet extends CoinServiceAPI { coin, ), ); + refreshMutex = false; 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); + timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async { // chain height check currently broken // if ((await chainHeight) != (await storedChainHeight)) { if (await refreshIfThereIsNewData()) { @@ -1083,7 +964,6 @@ class ParticlWallet extends CoinServiceAPI { } else { rate = feeRateAmount as int; } - // check for send all bool isSendAll = false; final balance = @@ -1092,49 +972,36 @@ class ParticlWallet extends CoinServiceAPI { isSendAll = true; } - final txData = + final result = 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!!!"); - } + 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!!!"); } - } 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!"); @@ -1147,15 +1014,12 @@ class ParticlWallet extends CoinServiceAPI { } @override - Future confirmSend({required Map txData}) async { + Future confirmSend({dynamic 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); + 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", @@ -1238,7 +1102,6 @@ class ParticlWallet extends CoinServiceAPI { throw Exception( "Attempted to initialize a new wallet using an existing wallet ID!"); } - await _prefs.init(); try { await _generateNewWallet(); @@ -1248,7 +1111,7 @@ class ParticlWallet extends CoinServiceAPI { rethrow; } await Future.wait([ - DB.instance.put(boxName: walletId, key: "id", value: walletId), + DB.instance.put(boxName: walletId, key: "id", value: _walletId), DB.instance .put(boxName: walletId, key: "isFavorite", value: false), ]); @@ -1279,7 +1142,6 @@ class ParticlWallet extends CoinServiceAPI { TransactionData? cachedTxData; - // TODO make sure this copied implementation from bitcoin_wallet.dart applies for particl just as well--or import it // hack to add tx to txData before refresh completes // required based on current app architecture where we don't properly store // transactions locally in a good way @@ -1326,6 +1188,15 @@ class ParticlWallet extends CoinServiceAPI { _transactionData = Future(() => cachedTxData!); } + bool validateCashAddr(String cashAddr) { + String addr = cashAddr; + if (cashAddr.contains(":")) { + addr = cashAddr.split(":").last; + } + + return addr.startsWith("q"); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network, particl.bech32!); @@ -1431,31 +1302,23 @@ class ParticlWallet extends CoinServiceAPI { 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 < 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); @@ -1466,16 +1329,6 @@ class ParticlWallet extends CoinServiceAPI { 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; } @@ -1520,7 +1373,7 @@ class ParticlWallet extends CoinServiceAPI { break; default: throw Exception( - "Attempted to generate a ParticlWallet using a non particl coin type: ${coin.name}"); + "Attempted to generate a Particl using a non bitcoin coin type: ${coin.name}"); } } @@ -1534,10 +1387,6 @@ class ParticlWallet extends CoinServiceAPI { 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 @@ -1558,95 +1407,36 @@ class ParticlWallet extends CoinServiceAPI { 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, - ), - ), + final initialReceivingAddressP2PKH = + await _generateAddressForChain(0, 0, DerivePathType.bip44); + final initialChangeAddressP2PKH = + await _generateAddressForChain(1, 0, DerivePathType.bip44); - // 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, - ), - ), + final initialReceivingAddressP2SH = + await _generateAddressForChain(0, 0, DerivePathType.bip49); + final initialChangeAddressP2SH = + await _generateAddressForChain(1, 0, DerivePathType.bip49); - // 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, - ), - ), - ]); + await _addToAddressesArrayForChain( + initialReceivingAddressP2PKH, 0, DerivePathType.bip44); + await _addToAddressesArrayForChain( + initialChangeAddressP2PKH, 1, DerivePathType.bip44); - // // 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, - // )); + await _addToAddressesArrayForChain( + initialReceivingAddressP2SH, 0, DerivePathType.bip49); + await _addToAddressesArrayForChain( + initialChangeAddressP2SH, 1, DerivePathType.bip49); + + // this._currentReceivingAddress = Future(() => initialReceivingAddress); + + var newaddr = await _getCurrentAddressForChain(0, DerivePathType.bip44); + _currentReceivingAddressP2PKH = Future(() => newaddr); + _currentReceivingAddressP2SH = Future(() => initialReceivingAddressP2SH); 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. + /// Generates a new internal or external chain address for the wallet using a 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( @@ -1666,6 +1456,10 @@ class ParticlWallet extends CoinServiceAPI { ), ); final data = PaymentData(pubkey: node.publicKey); + final p2shData = PaymentData( + redeem: + P2WPKH(data: PaymentData(pubkey: node.publicKey), network: _network) + .data); String address; switch (derivePathType) { @@ -1673,16 +1467,11 @@ class ParticlWallet extends CoinServiceAPI { address = P2PKH(data: data, network: _network).data.address!; break; case DerivePathType.bip49: - address = P2SH( - data: PaymentData( - redeem: P2WPKH(data: data, network: _network).data), - network: _network) - .data - .address!; - break; - case DerivePathType.bip84: - address = P2WPKH(network: _network, data: data).data.address!; + address = P2SH(data: p2shData, network: _network).data.address!; break; + // default: + // // should never hit this due to all enum cases handled + // return null; } // add generated address & info to derivations @@ -1710,9 +1499,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: indexKey += "P2SH"; break; - case DerivePathType.bip84: - indexKey += "P2WPKH"; - break; } final newIndex = @@ -1739,9 +1525,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: chainArray += "P2SH"; break; - case DerivePathType.bip84: - chainArray += "P2WPKH"; - break; } final addressArray = @@ -1779,19 +1562,18 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: arrayKey += "P2SH"; break; - case DerivePathType.bip84: - arrayKey += "P2WPKH"; - break; + } + + if (kDebugMode) { + print("Array key is ${jsonEncode(arrayKey)}"); } final internalChainArray = DB.instance.get(boxName: walletId, key: arrayKey); return internalChainArray.last as String; } - String _buildDerivationStorageKey({ - required int chain, - required DerivePathType derivePathType, - }) { + String _buildDerivationStorageKey( + {required int chain, required DerivePathType derivePathType}) { String key; String chainId = chain == 0 ? "receive" : "change"; switch (derivePathType) { @@ -1801,17 +1583,12 @@ class ParticlWallet extends CoinServiceAPI { 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 { + Future> _fetchDerivations( + {required int chain, required DerivePathType derivePathType}) async { // build lookup key final key = _buildDerivationStorageKey( chain: chain, derivePathType: derivePathType); @@ -1897,13 +1674,16 @@ class ParticlWallet extends CoinServiceAPI { final fetchedUtxoList = >>[]; final Map>> batches = {}; - const batchSizeMax = 100; + 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); + if (kDebugMode) { + print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); + } batches[batchNumber]!.addAll({ scripthash: [scripthash] }); @@ -1921,6 +1701,7 @@ class ParticlWallet extends CoinServiceAPI { } } } + final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; @@ -1941,7 +1722,9 @@ class ParticlWallet extends CoinServiceAPI { final Map utxo = {}; final int confirmations = txn["confirmations"] as int? ?? 0; - final bool confirmed = confirmations >= MINIMUM_CONFIRMATIONS; + final bool confirmed = txn["confirmations"] == null + ? false + : txn["confirmations"] as int >= MINIMUM_CONFIRMATIONS; if (!confirmed) { satoshiBalancePending += value; } @@ -2000,8 +1783,7 @@ class ParticlWallet extends CoinServiceAPI { 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?; + DB.instance.get(boxName: walletId, key: 'latest_utxo_model'); if (latestTxModel == null) { final emptyModel = { @@ -2014,7 +1796,7 @@ class ParticlWallet extends CoinServiceAPI { } else { Logging.instance .log("Old output model located", level: LogLevel.Warning); - return latestTxModel; + return latestTxModel as models.UtxoData; } } } @@ -2078,15 +1860,28 @@ class ParticlWallet extends CoinServiceAPI { }) async { try { final Map> args = {}; + if (kDebugMode) { + print("Address $addresses"); + } for (final entry in addresses.entries) { args[entry.key] = [_convertToScriptHash(entry.value, _network)]; } - final response = await electrumXClient.getBatchHistory(args: args); + if (kDebugMode) { + print("Args ${jsonEncode(args)}"); + } + + final response = await electrumXClient.getBatchHistory(args: args); + if (kDebugMode) { + print("Response ${jsonEncode(response)}"); + } final Map result = {}; for (final entry in response.entries) { result[entry.key] = entry.value.length; } + if (kDebugMode) { + print("result ${jsonEncode(result)}"); + } return result; } catch (e, s) { Logging.instance.log( @@ -2119,9 +1914,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: indexKey += "P2SH"; break; - case DerivePathType.bip84: - indexKey += "P2WPKH"; - break; } final newReceivingIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; @@ -2143,11 +1935,13 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: _currentReceivingAddressP2SH = Future(() => newReceivingAddress); break; - case DerivePathType.bip84: - _currentReceivingAddress = 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", @@ -2179,9 +1973,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: indexKey += "P2SH"; break; - case DerivePathType.bip84: - indexKey += "P2WPKH"; - break; } final newChangeIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; @@ -2193,14 +1984,9 @@ class ParticlWallet extends CoinServiceAPI { // 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", + "Exception rethrown from _checkChangeAddressForTransactions($derivePathType): $e\n$s", level: LogLevel.Error); rethrow; } @@ -2214,7 +2000,7 @@ class ParticlWallet extends CoinServiceAPI { } catch (e, s) { Logging.instance.log( "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", - level: LogLevel.Error); + level: LogLevel.Info); rethrow; } } @@ -2256,7 +2042,7 @@ class ParticlWallet extends CoinServiceAPI { /// attempts to convert a string to a valid scripthash /// - /// Returns the scripthash or throws an exception on invalid particl address + /// Returns the scripthash or throws an exception on invalid bch address String _convertToScriptHash(String particlAddress, NetworkType network) { try { final output = Address.addressToOutputScript( @@ -2284,7 +2070,7 @@ class ParticlWallet extends CoinServiceAPI { final Map>> batches = {}; final Map requestIdToAddressMap = {}; - const batchSizeMax = 100; + const batchSizeMax = 10; int batchNumber = 0; for (int i = 0; i < allAddresses.length; i++) { if (batches[batchNumber] == null) { @@ -2371,18 +2157,13 @@ class ParticlWallet extends CoinServiceAPI { Future _fetchTransactionData() async { final List allAddresses = await _fetchAllOwnAddresses(); - final changeAddresses = DB.instance.get( - boxName: walletId, key: 'changeAddressesP2WPKH') as List; - final changeAddressesP2PKH = + final changeAddresses = 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); } @@ -2712,20 +2493,15 @@ class ParticlWallet extends CoinServiceAPI { /// 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 { + 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; - print("AVAILABLE UTXOS IS ::::: ${availableOutputs}"); + // Build list of spendable outputs and totaling their satoshi amount for (var i = 0; i < availableOutputs.length; i++) { if (availableOutputs[i].blocked == false && @@ -2787,6 +2563,8 @@ class ParticlWallet extends CoinServiceAPI { .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]; @@ -2809,11 +2587,8 @@ class ParticlWallet extends CoinServiceAPI { vSize: vSizeForOneOutput, feeRatePerKB: selectedTxFeeRate, ); - - final int roughEstimate = - roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); - if (feeForOneOutput < roughEstimate) { - feeForOneOutput = roughEstimate; + if (feeForOneOutput < (vSizeForOneOutput + 1)) { + feeForOneOutput = (vSizeForOneOutput + 1); } final int amount = satoshiAmountToSend - feeForOneOutput; @@ -2844,25 +2619,38 @@ class ParticlWallet extends CoinServiceAPI { utxoSigningData: utxoSigningData, recipients: [ _recipientAddress, - await _getCurrentAddressForChain(1, DerivePathType.bip84), + await _getCurrentAddressForChain(1, DerivePathType.bip44), ], satoshiAmounts: [ satoshiAmountToSend, - satoshisBeingUsed - satoshiAmountToSend - 1 + 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 - final feeForOneOutput = estimateTxFee( + var feeForOneOutput = estimateTxFee( vSize: vSizeForOneOutput, feeRatePerKB: selectedTxFeeRate, ); // Assume 2 outputs, one for recipient and one for change - final feeForTwoOutputs = estimateTxFee( + 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 @@ -2876,15 +2664,15 @@ class ParticlWallet extends CoinServiceAPI { 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 + // 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.bip84); + await _checkChangeAddressForTransactions(DerivePathType.bip44); final String newChangeAddress = - await _getCurrentAddressForChain(1, DerivePathType.bip84); + await _getCurrentAddressForChain(1, DerivePathType.bip44); int feeBeingPaid = satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; @@ -2951,7 +2739,7 @@ class ParticlWallet extends CoinServiceAPI { 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. + // 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); @@ -2978,7 +2766,7 @@ class ParticlWallet extends CoinServiceAPI { return transactionObject; } } else { - // No additional outputs needed since adding one would mean that it'd be smaller than DUST_LIMIT sats + // 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); @@ -3054,6 +2842,8 @@ class ParticlWallet extends CoinServiceAPI { Future> fetchBuildTxData( List utxosToUse, ) async { + Logging.instance.log("UTXO TO USE FOR SIGNING IS -----$utxosToUse", + level: LogLevel.Info, printFullLength: true); // return data Map results = {}; Map> addressTxid = {}; @@ -3061,7 +2851,6 @@ class ParticlWallet extends CoinServiceAPI { // addresses to check List addressesP2PKH = []; List addressesP2SH = []; - List addressesP2WPKH = []; try { // Populating the addresses to check @@ -3088,9 +2877,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip49: addressesP2SH.add(address); break; - case DerivePathType.bip84: - addressesP2WPKH.add(address); - break; } } } @@ -3194,7 +2980,6 @@ class ParticlWallet extends CoinServiceAPI { }; } } else { - print("THIS IS WHERE ITS AT - CHANGE p2wpkh"); // if its not a receive, check change final changeDerivation = changeDerivations[addressesP2SH[i]]; // if a match exists it will not be null @@ -3227,64 +3012,6 @@ class ParticlWallet extends CoinServiceAPI { } } - // 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, - ).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, - ).data; - - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } - } - } - } Logging.instance.log("FETCHED TX BUILD DATA IS -----$results", level: LogLevel.Info, printFullLength: true); return results; @@ -3340,11 +3067,11 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance.log("AND THIS DATA IS -----${utxoSigningData[txid]}", level: LogLevel.Info, printFullLength: true); - txb.sign( - vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?); + // txb.sign( + // vin: i, + // keyPair: utxoSigningData[txid]["keyPair"] as ECPair, + // witnessValue: utxosToUse[i].value, + // redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", @@ -3352,20 +3079,20 @@ class ParticlWallet extends CoinServiceAPI { rethrow; } - final builtTx = txb.build(); + final builtTx = txb.buildIncomplete(); final vSize = builtTx.virtualSize(); print("BUILT TX IS ${builtTx.toHex().toString()}"); - String hexBefore = builtTx.toHex(); - if (builtTx.toHex().toString().endsWith('0000')) { - // print("END WITH ZERO"); - String stripped = hexBefore.substring(0, hexBefore.length - 4); - return {"hex": stripped, "vSize": vSize}; - // } else { - // print("DOES NOT END WITH ZERO"); - // return {"hex": builtTx.toHex(), "vSize": vSize}; - } + // String hexBefore = builtTx.toHex(); + // if (builtTx.toHex().toString().endsWith('0000')) { + // // print("END WITH ZERO"); + // String stripped = hexBefore.substring(0, hexBefore.length - 4); + // return {"hex": stripped, "vSize": vSize}; + // // } else { + // // print("DOES NOT END WITH ZERO"); + // // return {"hex": builtTx.toHex(), "vSize": vSize}; + // } return {"hex": builtTx.toHex(), "vSize": vSize}; // print("AND NOW IT IS $stripped"); @@ -3500,40 +3227,6 @@ class ParticlWallet extends CoinServiceAPI { 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"); @@ -3567,24 +3260,6 @@ class ParticlWallet extends CoinServiceAPI { 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'); @@ -3674,43 +3349,6 @@ class ParticlWallet extends CoinServiceAPI { 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"); @@ -3743,22 +3381,6 @@ class ParticlWallet extends CoinServiceAPI { 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'); @@ -3770,6 +3392,28 @@ class ParticlWallet extends CoinServiceAPI { 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 (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; + } + } + + @override + bool get isRefreshing => refreshMutex; + bool isActive = false; @override @@ -3819,8 +3463,9 @@ class ParticlWallet extends CoinServiceAPI { } } + // TODO: correct formula for bch? int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + return ((181 * inputCount) + (34 * outputCount) + 10) * (feeRatePerKB / 1000).ceil(); } @@ -3844,22 +3489,23 @@ class ParticlWallet extends CoinServiceAPI { Future generateNewAddress() async { try { await _incrementAddressIndexForChain( - 0, DerivePathType.bip84); // First increment the receiving index + 0, DerivePathType.bip44); // First increment the receiving index final newReceivingIndex = DB.instance.get( boxName: walletId, - key: 'receivingIndexP2WPKH') as int; // Check the new receiving index + key: 'receivingIndexP2PKH') as int; // Check the new receiving index final newReceivingAddress = await _generateAddressForChain( 0, newReceivingIndex, DerivePathType - .bip84); // Use new index to derive a new receiving address + .bip44); // 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 + .bip44); // Add that new receiving address to the array of receiving addresses + var newaddr = await _getCurrentAddressForChain(0, DerivePathType.bip44); + _currentReceivingAddressP2PKH = Future( + () => newaddr); // Set the new receiving address that the service return true; } catch (e, s) { From bc5e7fcaac8f2609222173d1da85df7fd16c2deb Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 5 Dec 2022 12:52:59 +0200 Subject: [PATCH 050/103] Remove P2SH --- .../coins/particl/particl_wallet.dart | 378 +----------------- 1 file changed, 18 insertions(+), 360 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index c141b72f1..0c9840ea6 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -50,7 +50,7 @@ const String GENESIS_HASH_MAINNET = const String GENESIS_HASH_TESTNET = "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; -enum DerivePathType { bip44, bip49 } +enum DerivePathType { bip44, bip84 } bip32.BIP32 getBip32Node(int chain, int index, String mnemonic, NetworkType network, DerivePathType derivePathType) { @@ -86,8 +86,8 @@ bip32.BIP32 getBip32NodeFromRoot( 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."); } @@ -203,11 +203,6 @@ class ParticlWallet extends CoinServiceAPI { _getCurrentAddressForChain(0, DerivePathType.bip44); Future? _currentReceivingAddressP2PKH; - // Future get currentReceivingAddressP2SH => - // _currentReceivingAddressP2SH ??= - // _getCurrentAddressForChain(0, DerivePathType.bip49); - Future? _currentReceivingAddressP2SH; - @override Future exit() async { _hasCalledExit = true; @@ -271,10 +266,6 @@ class ParticlWallet extends CoinServiceAPI { // P2PKH return DerivePathType.bip44; } - if (decodeBase58[0] == _network.scriptHash) { - // P2SH - return DerivePathType.bip49; - } throw ArgumentError('Invalid version or Network mismatch'); } else { try { @@ -357,27 +348,20 @@ class ParticlWallet extends CoinServiceAPI { longMutex = true; Map> p2pkhReceiveDerivations = {}; - Map> p2shReceiveDerivations = {}; Map> p2pkhChangeDerivations = {}; - Map> p2shChangeDerivations = {}; final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); List p2pkhReceiveAddressArray = []; - List p2shReceiveAddressArray = []; int p2pkhReceiveIndex = -1; - int p2shReceiveIndex = -1; List p2pkhChangeAddressArray = []; List p2shChangeAddressArray = []; int p2pkhChangeIndex = -1; - int p2shChangeIndex = -1; // The gap limit will be capped at [maxUnusedAddressGap] - // int receivingGapCounter = 0; - // int changeGapCounter = 0; - // actual size is 24 due to p2pkh and p2sh so 12x2 + // actual size is 12 due to p2pkh so 12x1 const txCountBatchSize = 12; try { @@ -387,20 +371,13 @@ class ParticlWallet extends CoinServiceAPI { final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); - final resultReceive49 = _checkGaps(maxNumberOfIndexesToCheck, - maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 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); - - await Future.wait( - [resultReceive44, resultReceive49, resultChange44, resultChange49]); + await Future.wait([resultReceive44, resultChange44]); p2pkhReceiveAddressArray = (await resultReceive44)['addressArray'] as List; @@ -408,24 +385,12 @@ class ParticlWallet extends CoinServiceAPI { p2pkhReceiveDerivations = (await resultReceive44)['derivations'] as Map>; - p2shReceiveAddressArray = - (await resultReceive49)['addressArray'] as List; - p2shReceiveIndex = (await resultReceive49)['index'] as int; - p2shReceiveDerivations = (await resultReceive49)['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>; - // save the derivations (if any) if (p2pkhReceiveDerivations.isNotEmpty) { await addDerivations( @@ -433,24 +398,13 @@ class ParticlWallet extends CoinServiceAPI { derivePathType: DerivePathType.bip44, derivationsToAdd: p2pkhReceiveDerivations); } - if (p2shReceiveDerivations.isNotEmpty) { - await addDerivations( - chain: 0, - derivePathType: DerivePathType.bip49, - derivationsToAdd: p2shReceiveDerivations); - } + 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 restoring a wallet that never received any funds, then set receivingArray manually // If we didn't do this, it'd store an empty array @@ -460,12 +414,6 @@ class ParticlWallet extends CoinServiceAPI { p2pkhReceiveAddressArray.add(address); p2pkhReceiveIndex = 0; } - if (p2shReceiveIndex == -1) { - final address = - await _generateAddressForChain(0, 0, DerivePathType.bip49); - p2shReceiveAddressArray.add(address); - p2shReceiveIndex = 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. @@ -475,12 +423,6 @@ class ParticlWallet extends CoinServiceAPI { p2pkhChangeAddressArray.add(address); p2pkhChangeIndex = 0; } - if (p2shChangeIndex == -1) { - final address = - await _generateAddressForChain(1, 0, DerivePathType.bip49); - p2shChangeAddressArray.add(address); - p2shChangeIndex = 0; - } await DB.instance.put( boxName: walletId, @@ -490,10 +432,7 @@ class ParticlWallet extends CoinServiceAPI { boxName: walletId, key: 'changeAddressesP2PKH', value: p2pkhChangeAddressArray); - await DB.instance.put( - boxName: walletId, - key: 'receivingAddressesP2SH', - value: p2shReceiveAddressArray); + await DB.instance.put( boxName: walletId, key: 'changeAddressesP2SH', @@ -504,12 +443,7 @@ class ParticlWallet extends CoinServiceAPI { 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 @@ -568,17 +502,6 @@ class ParticlWallet extends CoinServiceAPI { .data .address!; break; - case DerivePathType.bip49: - address = P2SH( - data: PaymentData( - redeem: P2WPKH( - data: PaymentData(pubkey: node.publicKey), - network: _network) - .data), - network: _network) - .data - .address!; - break; default: throw Exception("No Path type $type exists"); } @@ -903,14 +826,12 @@ class ParticlWallet extends CoinServiceAPI { 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) { @@ -1309,16 +1230,6 @@ class ParticlWallet extends CoinServiceAPI { 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); @@ -1391,10 +1302,7 @@ class ParticlWallet extends CoinServiceAPI { .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', @@ -1412,26 +1320,13 @@ class ParticlWallet extends CoinServiceAPI { final initialChangeAddressP2PKH = await _generateAddressForChain(1, 0, DerivePathType.bip44); - final initialReceivingAddressP2SH = - await _generateAddressForChain(0, 0, DerivePathType.bip49); - final initialChangeAddressP2SH = - await _generateAddressForChain(1, 0, DerivePathType.bip49); - await _addToAddressesArrayForChain( initialReceivingAddressP2PKH, 0, DerivePathType.bip44); await _addToAddressesArrayForChain( initialChangeAddressP2PKH, 1, DerivePathType.bip44); - await _addToAddressesArrayForChain( - initialReceivingAddressP2SH, 0, DerivePathType.bip49); - await _addToAddressesArrayForChain( - initialChangeAddressP2SH, 1, DerivePathType.bip49); - - // this._currentReceivingAddress = Future(() => initialReceivingAddress); - var newaddr = await _getCurrentAddressForChain(0, DerivePathType.bip44); _currentReceivingAddressP2PKH = Future(() => newaddr); - _currentReceivingAddressP2SH = Future(() => initialReceivingAddressP2SH); Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); } @@ -1456,23 +1351,7 @@ class ParticlWallet extends CoinServiceAPI { ), ); final data = PaymentData(pubkey: node.publicKey); - final p2shData = PaymentData( - redeem: - P2WPKH(data: PaymentData(pubkey: node.publicKey), network: _network) - .data); - String address; - - switch (derivePathType) { - case DerivePathType.bip44: - address = P2PKH(data: data, network: _network).data.address!; - break; - case DerivePathType.bip49: - address = P2SH(data: p2shData, network: _network).data.address!; - break; - // default: - // // should never hit this due to all enum cases handled - // return null; - } + String address = P2PKH(data: data, network: _network).data.address!; // add generated address & info to derivations await addDerivation( @@ -1496,9 +1375,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: indexKey += "P2PKH"; break; - case DerivePathType.bip49: - indexKey += "P2SH"; - break; } final newIndex = @@ -1522,9 +1398,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: chainArray += "P2PKH"; break; - case DerivePathType.bip49: - chainArray += "P2SH"; - break; } final addressArray = @@ -1559,9 +1432,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: arrayKey += "P2PKH"; break; - case DerivePathType.bip49: - arrayKey += "P2SH"; - break; } if (kDebugMode) { @@ -1576,14 +1446,8 @@ class ParticlWallet extends CoinServiceAPI { {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; - } + + key = "${walletId}_${chainId}DerivationsP2PKH"; return key; } @@ -1911,9 +1775,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: indexKey += "P2PKH"; break; - case DerivePathType.bip49: - indexKey += "P2SH"; - break; } final newReceivingIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; @@ -1932,9 +1793,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); break; - case DerivePathType.bip49: - _currentReceivingAddressP2SH = Future(() => newReceivingAddress); - break; } } } on SocketException catch (se, s) { @@ -1970,9 +1828,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: indexKey += "P2PKH"; break; - case DerivePathType.bip49: - indexKey += "P2SH"; - break; } final newChangeIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; @@ -2160,13 +2015,6 @@ class ParticlWallet extends CoinServiceAPI { final changeAddresses = 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 < changeAddressesP2SH.length; i++) { - changeAddresses.add(changeAddressesP2SH[i] as String); - } final List> allTxHashes = await _fetchHistory(allAddresses); @@ -2874,9 +2722,6 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: addressesP2PKH.add(address); break; - case DerivePathType.bip49: - addressesP2SH.add(address); - break; } } } @@ -2940,78 +2785,6 @@ class ParticlWallet extends CoinServiceAPI { } } - // p2sh / bip49 - final p2shLength = addressesP2SH.length; - if (p2shLength > 0) { - print("THIS P2SH IS NOT NULL"); - 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) - .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) - .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, - }; - } - } - } - } - } - Logging.instance.log("FETCHED TX BUILD DATA IS -----$results", level: LogLevel.Info, printFullLength: true); return results; @@ -3067,11 +2840,12 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance.log("AND THIS DATA IS -----${utxoSigningData[txid]}", level: LogLevel.Info, printFullLength: true); - // txb.sign( - // vin: i, - // keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - // witnessValue: utxosToUse[i].value, - // redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?); + txb.sign( + vin: i, + keyPair: utxoSigningData[txid]["keyPair"] as ECPair, + witnessValue: utxosToUse[i].value, + hashType: 1, + redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", @@ -3082,22 +2856,7 @@ class ParticlWallet extends CoinServiceAPI { final builtTx = txb.buildIncomplete(); final vSize = builtTx.virtualSize(); - print("BUILT TX IS ${builtTx.toHex().toString()}"); - - // String hexBefore = builtTx.toHex(); - // if (builtTx.toHex().toString().endsWith('0000')) { - // // print("END WITH ZERO"); - // String stripped = hexBefore.substring(0, hexBefore.length - 4); - // return {"hex": stripped, "vSize": vSize}; - // // } else { - // // print("DOES NOT END WITH ZERO"); - // // return {"hex": builtTx.toHex(), "vSize": vSize}; - // } return {"hex": builtTx.toHex(), "vSize": vSize}; - - // print("AND NOW IT IS $stripped"); - // - // return {"hex": stripped, "vSize": vSize}; } @override @@ -3195,38 +2954,6 @@ class ParticlWallet extends CoinServiceAPI { 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); - // P2PKH derivations final p2pkhReceiveDerivationsString = await _secureStore.read( key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); @@ -3244,22 +2971,6 @@ class ParticlWallet extends CoinServiceAPI { 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"); - // UTXOs final utxoData = DB.instance .get(boxName: walletId, key: 'latest_utxo_model_BACKUP'); @@ -3312,43 +3023,6 @@ class ParticlWallet extends CoinServiceAPI { 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); - // P2PKH derivations final p2pkhReceiveDerivationsString = await _secureStore.read(key: "${walletId}_receiveDerivationsP2PKH"); @@ -3365,22 +3039,6 @@ class ParticlWallet extends CoinServiceAPI { 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"); - // UTXOs final utxoData = DB.instance.get(boxName: walletId, key: 'latest_utxo_model'); From e18aa8bd3a1b39e0c3475bf3e91bf13d57b138b9 Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 5 Dec 2022 16:53:21 +0200 Subject: [PATCH 051/103] Default to P2WPKH to get witnessScript --- .../coins/particl/particl_wallet.dart | 1090 +++++++++++------ 1 file changed, 726 insertions(+), 364 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 0c9840ea6..7e46fc928 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -52,8 +52,13 @@ const String GENESIS_HASH_TESTNET = enum DerivePathType { bip44, bip84 } -bip32.BIP32 getBip32Node(int chain, int index, String mnemonic, - NetworkType network, DerivePathType derivePathType) { +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); @@ -74,7 +79,11 @@ bip32.BIP32 getBip32NodeWrapper( } bip32.BIP32 getBip32NodeFromRoot( - int chain, int index, bip32.BIP32 root, DerivePathType derivePathType) { + int chain, + int index, + bip32.BIP32 root, + DerivePathType derivePathType, +) { String coinType; switch (root.network.wif) { case 0x6c: // PART mainnet wif @@ -127,6 +136,7 @@ bip32.BIP32 getBip32RootWrapper(Tuple2 args) { class ParticlWallet extends CoinServiceAPI { static const integrationTestFlag = bool.fromEnvironment("IS_INTEGRATION_TEST"); + final _prefs = Prefs.instance; Timer? timer; @@ -139,12 +149,30 @@ class ParticlWallet extends CoinServiceAPI { case Coin.particl: return particl; default: - throw Exception("Particl network type not set!"); + 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; @@ -198,7 +226,11 @@ class ParticlWallet extends CoinServiceAPI { } @override - Future get currentReceivingAddress => + Future get currentReceivingAddress => _currentReceivingAddress ??= + _getCurrentAddressForChain(0, DerivePathType.bip84); + Future? _currentReceivingAddress; + + Future get currentLegacyReceivingAddress => _currentReceivingAddressP2PKH ??= _getCurrentAddressForChain(0, DerivePathType.bip44); Future? _currentReceivingAddressP2PKH; @@ -222,9 +254,9 @@ class ParticlWallet extends CoinServiceAPI { @override Future get maxFee async { - final fee = (await fees).fast; - final satsFee = Format.satoshisToAmount(fee, coin: coin) * - Decimal.fromInt(Constants.satsPerCoin(coin)); + final fee = (await fees).fast as String; + final satsFee = + Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -242,7 +274,7 @@ class ParticlWallet extends CoinServiceAPI { } } - Future get storedChainHeight async { + int get storedChainHeight { final storedHeight = DB.instance .get(boxName: walletId, key: "storedChainHeight") as int?; return storedHeight ?? 0; @@ -280,7 +312,7 @@ class ParticlWallet extends CoinServiceAPI { throw ArgumentError('Invalid address version'); } // P2WPKH - throw ArgumentError('$address has no matching Script'); + return DerivePathType.bip84; } } @@ -307,10 +339,20 @@ class ParticlWallet extends CoinServiceAPI { throw Exception("genesis hash does not match main net!"); } break; + break; default: throw Exception( - "Attempted to generate a ParticlWallet using a non bch coin type: ${coin.name}"); + "Attempted to generate a ParticlWallet using a non particl 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 @@ -340,126 +382,6 @@ class ParticlWallet extends CoinServiceAPI { 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 = []; - List p2shChangeAddressArray = []; - int p2pkhChangeIndex = -1; - - // The gap limit will be capped at [maxUnusedAddressGap] - - // actual size is 12 due to p2pkh so 12x1 - 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); - - Logging.instance - .log("checking change addresses...", level: LogLevel.Info); - // change addresses - final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, - maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); - - await Future.wait([resultReceive44, resultChange44]); - - p2pkhReceiveAddressArray = - (await resultReceive44)['addressArray'] as List; - p2pkhReceiveIndex = (await resultReceive44)['index'] as int; - p2pkhReceiveDerivations = (await resultReceive44)['derivations'] - as Map>; - - p2pkhChangeAddressArray = - (await resultChange44)['addressArray'] as List; - p2pkhChangeIndex = (await resultChange44)['index'] as int; - p2pkhChangeDerivations = (await resultChange44)['derivations'] - as Map>; - - // 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: 'changeAddressesP2SH', - value: p2shChangeAddressArray); - 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: "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> _checkGaps( int maxNumberOfIndexesToCheck, int maxUnusedAddressGap, @@ -502,6 +424,13 @@ class ParticlWallet extends CoinServiceAPI { .data .address!; break; + case DerivePathType.bip84: + address = P2WPKH( + network: _network, + data: PaymentData(pubkey: node.publicKey)) + .data + .address!; + break; default: throw Exception("No Path type $type exists"); } @@ -518,9 +447,7 @@ class ParticlWallet extends CoinServiceAPI { // get address tx counts final counts = await _getBatchTxCount(addresses: txCountCallArgs); - if (kDebugMode) { - print("Counts $counts"); - } + // check and add appropriate addresses for (int k = 0; k < txCountBatchSize; k++) { int count = counts["${_id}_$k"]!; @@ -576,6 +503,185 @@ class ParticlWallet extends CoinServiceAPI { } } + Future _recoverWalletFromBIP32SeedPhrase({ + required String mnemonic, + int maxUnusedAddressGap = 20, + int maxNumberOfIndexesToCheck = 1000, + }) async { + longMutex = true; + + Map> p2pkhReceiveDerivations = {}; + Map> p2wpkhReceiveDerivations = {}; + Map> p2pkhChangeDerivations = {}; + Map> p2wpkhChangeDerivations = {}; + + final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); + + List p2pkhReceiveAddressArray = []; + List p2wpkhReceiveAddressArray = []; + int p2pkhReceiveIndex = -1; + int p2wpkhReceiveIndex = -1; + + List p2pkhChangeAddressArray = []; + List p2wpkhChangeAddressArray = []; + int p2pkhChangeIndex = -1; + int p2wpkhChangeIndex = -1; + + // actual size is 24 due to p2pkh, and p2wpkh so 12x2 + 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 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 resultChange84 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 1); + + await Future.wait( + [resultReceive44, resultReceive84, resultChange44, resultChange84]); + + p2pkhReceiveAddressArray = + (await resultReceive44)['addressArray'] as List; + p2pkhReceiveIndex = (await resultReceive44)['index'] as int; + p2pkhReceiveDerivations = (await resultReceive44)['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>; + + 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 (p2wpkhReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip84, + derivationsToAdd: p2wpkhReceiveDerivations); + } + if (p2pkhChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhChangeDerivations); + } + + 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 (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 (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: '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: "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; @@ -583,9 +689,6 @@ class ParticlWallet extends CoinServiceAPI { try { bool needsRefresh = false; - Logging.instance.log( - "notified unconfirmed transactions: ${txTracker.pendings}", - level: LogLevel.Info); Set txnsToCheck = {}; for (final String txid in txTracker.pendings) { @@ -596,8 +699,7 @@ class ParticlWallet extends CoinServiceAPI { for (String txid in txnsToCheck) { final txn = await electrumXClient.getTransaction(txHash: txid); - var confirmations = txn["confirmations"]; - if (confirmations is! int) continue; + int confirmations = txn["confirmations"] as int? ?? 0; bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; if (!isUnconfirmed) { // unconfirmedTxs = {}; @@ -625,7 +727,7 @@ class ParticlWallet extends CoinServiceAPI { } catch (e, s) { Logging.instance.log( "Exception caught in refreshIfThereIsNewData: $e\n$s", - level: LogLevel.Info); + level: LogLevel.Error); rethrow; } } @@ -637,15 +739,16 @@ class ParticlWallet extends CoinServiceAPI { List unconfirmedTxnsToNotifyPending = []; List unconfirmedTxnsToNotifyConfirmed = []; - // Get all unconfirmed incoming transactions 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); } @@ -653,39 +756,35 @@ class ParticlWallet extends CoinServiceAPI { } } - // notify on new incoming transaction + // 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.now(), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, - ), - ); + 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, - ), - ); + 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); } } @@ -693,38 +792,31 @@ class ParticlWallet extends CoinServiceAPI { // 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.now(), - shouldWatchForUpdates: false, - coinName: coin.name, - ), - ); - + 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.now(), - shouldWatchForUpdates: false, - coinName: coin.name, - ), - ); + 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 refreshMutex = false; - bool _shouldAutoSync = false; @override @@ -745,6 +837,11 @@ class ParticlWallet extends CoinServiceAPI { } } + @override + bool get isRefreshing => refreshMutex; + + bool refreshMutex = false; + //TODO Show percentages properly/more consistently /// Refreshes display data for the wallet @override @@ -781,14 +878,16 @@ class ParticlWallet extends CoinServiceAPI { if (currentHeight != storedHeight) { if (currentHeight != -1) { // -1 failed to fetch current height - await updateStoredChainHeight(newHeight: currentHeight); + unawaited(updateStoredChainHeight(newHeight: currentHeight)); } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); - await _checkChangeAddressForTransactions(DerivePathType.bip44); + final changeAddressForTransactions = + _checkChangeAddressForTransactions(DerivePathType.bip84); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); - await _checkCurrentReceivingAddressesForTransactions(); + final currentReceivingAddressesForTransactions = + _checkCurrentReceivingAddressesForTransactions(); final newTxData = _fetchTransactionData(); GlobalEventBus.instance @@ -808,11 +907,20 @@ class ParticlWallet extends CoinServiceAPI { GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.80, walletId)); - await getAllTxsToWatch(await newTxData); + 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( @@ -821,17 +929,21 @@ class ParticlWallet extends CoinServiceAPI { coin, ), ); - refreshMutex = false; if (shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async { + 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) { @@ -885,6 +997,7 @@ class ParticlWallet extends CoinServiceAPI { } else { rate = feeRateAmount as int; } + // check for send all bool isSendAll = false; final balance = @@ -893,36 +1006,49 @@ class ParticlWallet extends CoinServiceAPI { isSendAll = true; } - final result = + final txData = 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"); + 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"); } - return result as Map; } else { - throw Exception("sent hex is not a String!!!"); + 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!"); @@ -935,12 +1061,15 @@ class ParticlWallet extends CoinServiceAPI { } @override - Future confirmSend({dynamic txData}) async { + Future confirmSend({required Map txData}) async { try { Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); - final txHash = await _electrumXClient.broadcastTransaction( - rawTx: txData["hex"] as String); + + 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", @@ -1023,6 +1152,7 @@ class ParticlWallet extends CoinServiceAPI { throw Exception( "Attempted to initialize a new wallet using an existing wallet ID!"); } + await _prefs.init(); try { await _generateNewWallet(); @@ -1032,7 +1162,7 @@ class ParticlWallet extends CoinServiceAPI { rethrow; } await Future.wait([ - DB.instance.put(boxName: walletId, key: "id", value: _walletId), + DB.instance.put(boxName: walletId, key: "id", value: walletId), DB.instance .put(boxName: walletId, key: "isFavorite", value: false), ]); @@ -1063,6 +1193,7 @@ class ParticlWallet extends CoinServiceAPI { TransactionData? cachedTxData; + // TODO make sure this copied implementation from bitcoin_wallet.dart applies for particl just as well--or import it // hack to add tx to txData before refresh completes // required based on current app architecture where we don't properly store // transactions locally in a good way @@ -1109,15 +1240,6 @@ class ParticlWallet extends CoinServiceAPI { _transactionData = Future(() => cachedTxData!); } - bool validateCashAddr(String cashAddr) { - String addr = cashAddr; - if (cashAddr.contains(":")) { - addr = cashAddr.split(":").last; - } - - return addr.startsWith("q"); - } - @override bool validateAddress(String address) { return Address.validateAddress(address, _network, particl.bech32!); @@ -1223,13 +1345,26 @@ class ParticlWallet extends CoinServiceAPI { 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; + 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); @@ -1240,6 +1375,7 @@ class ParticlWallet extends CoinServiceAPI { allAddresses.add(changeAddressesP2PKH[i] as String); } } + return allAddresses; } @@ -1284,7 +1420,7 @@ class ParticlWallet extends CoinServiceAPI { break; default: throw Exception( - "Attempted to generate a Particl using a non bitcoin coin type: ${coin.name}"); + "Attempted to generate a ParticlWallet using a non particl coin type: ${coin.name}"); } } @@ -1298,11 +1434,14 @@ class ParticlWallet extends CoinServiceAPI { 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: 'blocked_tx_hashes', @@ -1315,23 +1454,78 @@ class ParticlWallet extends CoinServiceAPI { value: {}); // Generate and add addresses to relevant arrays - final initialReceivingAddressP2PKH = - await _generateAddressForChain(0, 0, DerivePathType.bip44); - final initialChangeAddressP2PKH = - await _generateAddressForChain(1, 0, DerivePathType.bip44); + 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, + ), + ), - await _addToAddressesArrayForChain( - initialReceivingAddressP2PKH, 0, DerivePathType.bip44); - await _addToAddressesArrayForChain( - initialChangeAddressP2PKH, 1, DerivePathType.bip44); + // 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, + ), + ), + ]); - var newaddr = await _getCurrentAddressForChain(0, DerivePathType.bip44); - _currentReceivingAddressP2PKH = Future(() => newaddr); + // // 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 BIP44 or BIP49 derivation path. + /// 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( @@ -1351,7 +1545,16 @@ class ParticlWallet extends CoinServiceAPI { ), ); final data = PaymentData(pubkey: node.publicKey); - String address = P2PKH(data: data, network: _network).data.address!; + String address; + + switch (derivePathType) { + case DerivePathType.bip44: + address = P2PKH(data: data, network: _network).data.address!; + break; + case DerivePathType.bip84: + address = P2WPKH(network: _network, data: data).data.address!; + break; + } // add generated address & info to derivations await addDerivation( @@ -1375,6 +1578,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: indexKey += "P2PKH"; break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; } final newIndex = @@ -1398,6 +1604,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: chainArray += "P2PKH"; break; + case DerivePathType.bip84: + chainArray += "P2WPKH"; + break; } final addressArray = @@ -1432,27 +1641,36 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: arrayKey += "P2PKH"; break; - } - - if (kDebugMode) { - print("Array key is ${jsonEncode(arrayKey)}"); + 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 _buildDerivationStorageKey({ + required int chain, + required DerivePathType derivePathType, + }) { String key; String chainId = chain == 0 ? "receive" : "change"; - - key = "${walletId}_${chainId}DerivationsP2PKH"; + switch (derivePathType) { + case DerivePathType.bip44: + key = "${walletId}_${chainId}DerivationsP2PKH"; + break; + case DerivePathType.bip84: + key = "${walletId}_${chainId}DerivationsP2WPKH"; + break; + } return key; } - Future> _fetchDerivations( - {required int chain, required DerivePathType derivePathType}) async { + Future> _fetchDerivations({ + required int chain, + required DerivePathType derivePathType, + }) async { // build lookup key final key = _buildDerivationStorageKey( chain: chain, derivePathType: derivePathType); @@ -1538,16 +1756,13 @@ class ParticlWallet extends CoinServiceAPI { final fetchedUtxoList = >>[]; final Map>> batches = {}; - const batchSizeMax = 10; + 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); - if (kDebugMode) { - print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); - } batches[batchNumber]!.addAll({ scripthash: [scripthash] }); @@ -1565,7 +1780,6 @@ class ParticlWallet extends CoinServiceAPI { } } } - final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; @@ -1586,9 +1800,7 @@ class ParticlWallet extends CoinServiceAPI { final Map utxo = {}; final int confirmations = txn["confirmations"] as int? ?? 0; - final bool confirmed = txn["confirmations"] == null - ? false - : txn["confirmations"] as int >= MINIMUM_CONFIRMATIONS; + final bool confirmed = confirmations >= MINIMUM_CONFIRMATIONS; if (!confirmed) { satoshiBalancePending += value; } @@ -1647,7 +1859,8 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); final latestTxModel = - DB.instance.get(boxName: walletId, key: 'latest_utxo_model'); + DB.instance.get(boxName: walletId, key: 'latest_utxo_model') + as models.UtxoData?; if (latestTxModel == null) { final emptyModel = { @@ -1660,7 +1873,7 @@ class ParticlWallet extends CoinServiceAPI { } else { Logging.instance .log("Old output model located", level: LogLevel.Warning); - return latestTxModel as models.UtxoData; + return latestTxModel; } } } @@ -1724,28 +1937,15 @@ class ParticlWallet extends CoinServiceAPI { }) async { try { final Map> args = {}; - if (kDebugMode) { - print("Address $addresses"); - } for (final entry in addresses.entries) { args[entry.key] = [_convertToScriptHash(entry.value, _network)]; } - - if (kDebugMode) { - print("Args ${jsonEncode(args)}"); - } - final response = await electrumXClient.getBatchHistory(args: args); - if (kDebugMode) { - print("Response ${jsonEncode(response)}"); - } + final Map result = {}; for (final entry in response.entries) { result[entry.key] = entry.value.length; } - if (kDebugMode) { - print("result ${jsonEncode(result)}"); - } return result; } catch (e, s) { Logging.instance.log( @@ -1775,6 +1975,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: indexKey += "P2PKH"; break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; } final newReceivingIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; @@ -1793,13 +1996,11 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); break; + case DerivePathType.bip84: + _currentReceivingAddress = 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", @@ -1828,6 +2029,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: indexKey += "P2PKH"; break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; } final newChangeIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; @@ -1839,9 +2043,14 @@ class ParticlWallet extends CoinServiceAPI { // 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 _checkChangeAddressForTransactions($derivePathType): $e\n$s", + "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", level: LogLevel.Error); rethrow; } @@ -1855,7 +2064,7 @@ class ParticlWallet extends CoinServiceAPI { } catch (e, s) { Logging.instance.log( "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", - level: LogLevel.Info); + level: LogLevel.Error); rethrow; } } @@ -1897,7 +2106,7 @@ class ParticlWallet extends CoinServiceAPI { /// attempts to convert a string to a valid scripthash /// - /// Returns the scripthash or throws an exception on invalid bch address + /// Returns the scripthash or throws an exception on invalid particl address String _convertToScriptHash(String particlAddress, NetworkType network) { try { final output = Address.addressToOutputScript( @@ -1925,7 +2134,7 @@ class ParticlWallet extends CoinServiceAPI { final Map>> batches = {}; final Map requestIdToAddressMap = {}; - const batchSizeMax = 10; + const batchSizeMax = 100; int batchNumber = 0; for (int i = 0; i < allAddresses.length; i++) { if (batches[batchNumber] == null) { @@ -2012,10 +2221,16 @@ class ParticlWallet extends CoinServiceAPI { Future _fetchTransactionData() async { final List allAddresses = await _fetchAllOwnAddresses(); - final changeAddresses = + final changeAddresses = DB.instance.get( + boxName: walletId, key: 'changeAddressesP2WPKH') as List; + final changeAddressesP2PKH = DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') as List; + for (var i = 0; i < changeAddressesP2PKH.length; i++) { + changeAddresses.add(changeAddressesP2PKH[i] as String); + } + final List> allTxHashes = await _fetchHistory(allAddresses); @@ -2341,15 +2556,20 @@ class ParticlWallet extends CoinServiceAPI { /// 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 { + 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; - + print("AVAILABLE UTXOS IS ::::: ${availableOutputs}"); // Build list of spendable outputs and totaling their satoshi amount for (var i = 0; i < availableOutputs.length; i++) { if (availableOutputs[i].blocked == false && @@ -2411,8 +2631,6 @@ class ParticlWallet extends CoinServiceAPI { .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]; @@ -2435,8 +2653,11 @@ class ParticlWallet extends CoinServiceAPI { vSize: vSizeForOneOutput, feeRatePerKB: selectedTxFeeRate, ); - if (feeForOneOutput < (vSizeForOneOutput + 1)) { - feeForOneOutput = (vSizeForOneOutput + 1); + + final int roughEstimate = + roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + if (feeForOneOutput < roughEstimate) { + feeForOneOutput = roughEstimate; } final int amount = satoshiAmountToSend - feeForOneOutput; @@ -2467,38 +2688,25 @@ class ParticlWallet extends CoinServiceAPI { utxoSigningData: utxoSigningData, recipients: [ _recipientAddress, - await _getCurrentAddressForChain(1, DerivePathType.bip44), + await _getCurrentAddressForChain(1, DerivePathType.bip84), ], satoshiAmounts: [ satoshiAmountToSend, - satoshisBeingUsed - satoshiAmountToSend - 1, + 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( + final feeForOneOutput = estimateTxFee( vSize: vSizeForOneOutput, feeRatePerKB: selectedTxFeeRate, ); // Assume 2 outputs, one for recipient and one for change - var feeForTwoOutputs = estimateTxFee( + final 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 @@ -2512,15 +2720,15 @@ class ParticlWallet extends CoinServiceAPI { 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 + // 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.bip44); + await _checkChangeAddressForTransactions(DerivePathType.bip84); final String newChangeAddress = - await _getCurrentAddressForChain(1, DerivePathType.bip44); + await _getCurrentAddressForChain(1, DerivePathType.bip84); int feeBeingPaid = satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; @@ -2587,7 +2795,7 @@ class ParticlWallet extends CoinServiceAPI { 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. + // 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); @@ -2614,7 +2822,7 @@ class ParticlWallet extends CoinServiceAPI { return transactionObject; } } else { - // No additional outputs needed since adding one would mean that it'd be smaller than 546 sats + // 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); @@ -2690,15 +2898,13 @@ class ParticlWallet extends CoinServiceAPI { Future> fetchBuildTxData( List utxosToUse, ) async { - Logging.instance.log("UTXO TO USE FOR SIGNING IS -----$utxosToUse", - level: LogLevel.Info, printFullLength: true); // return data Map results = {}; Map> addressTxid = {}; // addresses to check List addressesP2PKH = []; - List addressesP2SH = []; + List addressesP2WPKH = []; try { // Populating the addresses to check @@ -2722,6 +2928,9 @@ class ParticlWallet extends CoinServiceAPI { case DerivePathType.bip44: addressesP2PKH.add(address); break; + case DerivePathType.bip84: + addressesP2WPKH.add(address); + break; } } } @@ -2785,6 +2994,64 @@ class ParticlWallet extends CoinServiceAPI { } } + // 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, + ).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, + ).data; + + for (String tx in addressTxid[addressesP2WPKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + changeDerivation["wif"] as String, + network: _network, + ), + }; + } + } + } + } + } Logging.instance.log("FETCHED TX BUILD DATA IS -----$results", level: LogLevel.Info, printFullLength: true); return results; @@ -2844,7 +3111,6 @@ class ParticlWallet extends CoinServiceAPI { vin: i, keyPair: utxoSigningData[txid]["keyPair"] as ECPair, witnessValue: utxosToUse[i].value, - hashType: 1, redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?); } } catch (e, s) { @@ -2853,10 +3119,25 @@ class ParticlWallet extends CoinServiceAPI { rethrow; } - final builtTx = txb.buildIncomplete(); + final builtTx = txb.build(); final vSize = builtTx.virtualSize(); + print("BUILT TX IS ${builtTx.toHex().toString()}"); + + String hexBefore = builtTx.toHex(); + if (builtTx.toHex().toString().endsWith('0000')) { + // print("END WITH ZERO"); + String stripped = hexBefore.substring(0, hexBefore.length - 4); + return {"hex": stripped, "vSize": vSize}; + // } else { + // print("DOES NOT END WITH ZERO"); + // return {"hex": builtTx.toHex(), "vSize": vSize}; + } return {"hex": builtTx.toHex(), "vSize": vSize}; + + // print("AND NOW IT IS $stripped"); + // + // return {"hex": stripped, "vSize": vSize}; } @override @@ -2954,6 +3235,40 @@ class ParticlWallet extends CoinServiceAPI { await DB.instance .delete(key: 'changeIndexP2PKH_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"); @@ -2971,6 +3286,24 @@ class ParticlWallet extends CoinServiceAPI { key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH_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'); @@ -3023,6 +3356,43 @@ class ParticlWallet extends CoinServiceAPI { await DB.instance .delete(key: 'changeIndexP2PKH', 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"); @@ -3039,6 +3409,22 @@ class ParticlWallet extends CoinServiceAPI { await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH"); await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH"); + // 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'); @@ -3050,28 +3436,6 @@ class ParticlWallet extends CoinServiceAPI { 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 (returning false by default): $e\n$s", - level: LogLevel.Error); - return false; - } - } - - @override - bool get isRefreshing => refreshMutex; - bool isActive = false; @override @@ -3121,9 +3485,8 @@ class ParticlWallet extends CoinServiceAPI { } } - // TODO: correct formula for bch? int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((181 * inputCount) + (34 * outputCount) + 10) * + return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * (feeRatePerKB / 1000).ceil(); } @@ -3147,23 +3510,22 @@ class ParticlWallet extends CoinServiceAPI { Future generateNewAddress() async { try { await _incrementAddressIndexForChain( - 0, DerivePathType.bip44); // First increment the receiving index + 0, DerivePathType.bip84); // First increment the receiving index final newReceivingIndex = DB.instance.get( boxName: walletId, - key: 'receivingIndexP2PKH') as int; // Check the new receiving index + key: 'receivingIndexP2WPKH') as int; // Check the new receiving index final newReceivingAddress = await _generateAddressForChain( 0, newReceivingIndex, DerivePathType - .bip44); // Use new index to derive a new receiving address + .bip84); // 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 - var newaddr = await _getCurrentAddressForChain(0, DerivePathType.bip44); - _currentReceivingAddressP2PKH = Future( - () => newaddr); // Set the new receiving address that the service + .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) { From 332be96e895c8e6657a7055a01f4b9a61a373ab8 Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Mon, 5 Dec 2022 20:31:29 -0700 Subject: [PATCH 052/103] Bump version (1.5.23, build 95) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 02c4bc433..9d0551e7a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.22+95 +version: 1.5.23+96 environment: sdk: ">=2.17.0 <3.0.0" From 03ac0f277880fe95907cdd5ec609b8de185368f9 Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 6 Dec 2022 14:46:08 +0200 Subject: [PATCH 053/103] Commit before change address to bip84 --- lib/services/coins/particl/particl_wallet.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 7e46fc928..a61899bc2 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -2902,6 +2902,8 @@ class ParticlWallet extends CoinServiceAPI { Map results = {}; Map> addressTxid = {}; + print("CALLING FETCH BUILD TX DATA"); + // addresses to check List addressesP2PKH = []; List addressesP2WPKH = []; @@ -3119,7 +3121,7 @@ class ParticlWallet extends CoinServiceAPI { rethrow; } - final builtTx = txb.build(); + final builtTx = txb.buildIncomplete(); final vSize = builtTx.virtualSize(); print("BUILT TX IS ${builtTx.toHex().toString()}"); From d4f494bbaaaaf3695e4c3fc40c69f0afb1d181c7 Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 6 Dec 2022 18:02:46 +0200 Subject: [PATCH 054/103] Use bip44 change addresses and remove trailing zeros from tx --- .../coins/particl/particl_wallet.dart | 691 +++++++++--------- 1 file changed, 344 insertions(+), 347 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index a61899bc2..9a0f5da36 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -50,7 +50,7 @@ const String GENESIS_HASH_MAINNET = const String GENESIS_HASH_TESTNET = "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; -enum DerivePathType { bip44, bip84 } +enum DerivePathType { bip84 } bip32.BIP32 getBip32Node( int chain, @@ -93,8 +93,6 @@ bip32.BIP32 getBip32NodeFromRoot( throw Exception("Invalid Particl network type used!"); } switch (derivePathType) { - case DerivePathType.bip44: - return root.derivePath("m/44'/$coinType'/0'/$chain/$index"); case DerivePathType.bip84: return root.derivePath("m/84'/$coinType'/0'/$chain/$index"); default: @@ -230,10 +228,10 @@ class ParticlWallet extends CoinServiceAPI { _getCurrentAddressForChain(0, DerivePathType.bip84); Future? _currentReceivingAddress; - Future get currentLegacyReceivingAddress => - _currentReceivingAddressP2PKH ??= - _getCurrentAddressForChain(0, DerivePathType.bip44); - Future? _currentReceivingAddressP2PKH; + // Future get currentLegacyReceivingAddress => + // _currentReceivingAddressP2PKH ??= + // _getCurrentAddressForChain(0, DerivePathType.bip44); + // Future? _currentReceivingAddressP2PKH; @override Future exit() async { @@ -293,27 +291,29 @@ class ParticlWallet extends CoinServiceAPI { } 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, particl.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; - } + + return DerivePathType.bip84; + // 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, particl.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; @@ -417,13 +417,13 @@ class ParticlWallet extends CoinServiceAPI { ); String? address; switch (type) { - case DerivePathType.bip44: - address = P2PKH( - data: PaymentData(pubkey: node.publicKey), - network: _network) - .data - .address!; - break; + // case DerivePathType.bip44: + // address = P2PKH( + // data: PaymentData(pubkey: node.publicKey), + // network: _network) + // .data + // .address!; + // break; case DerivePathType.bip84: address = P2WPKH( network: _network, @@ -510,21 +510,21 @@ class ParticlWallet extends CoinServiceAPI { }) async { longMutex = true; - Map> p2pkhReceiveDerivations = {}; + // Map> p2pkhReceiveDerivations = {}; Map> p2wpkhReceiveDerivations = {}; - Map> p2pkhChangeDerivations = {}; + // Map> p2pkhChangeDerivations = {}; Map> p2wpkhChangeDerivations = {}; final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); - List p2pkhReceiveAddressArray = []; + // List p2pkhReceiveAddressArray = []; List p2wpkhReceiveAddressArray = []; - int p2pkhReceiveIndex = -1; + // int p2pkhReceiveIndex = -1; int p2wpkhReceiveIndex = -1; - List p2pkhChangeAddressArray = []; + // List p2pkhChangeAddressArray = []; List p2wpkhChangeAddressArray = []; - int p2pkhChangeIndex = -1; + // int p2pkhChangeIndex = -1; int p2wpkhChangeIndex = -1; // actual size is 24 due to p2pkh, and p2wpkh so 12x2 @@ -534,8 +534,8 @@ class ParticlWallet extends CoinServiceAPI { // receiving addresses Logging.instance .log("checking receiving addresses...", level: LogLevel.Info); - final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, - maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); + // final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, + // maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); final resultReceive84 = _checkGaps(maxNumberOfIndexesToCheck, maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 0); @@ -543,20 +543,19 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance .log("checking change addresses...", level: LogLevel.Info); // change addresses - final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, - maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); + // final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, + // maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); final resultChange84 = _checkGaps(maxNumberOfIndexesToCheck, maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 1); - await Future.wait( - [resultReceive44, resultReceive84, resultChange44, resultChange84]); + await Future.wait([resultReceive84, resultChange84]); - p2pkhReceiveAddressArray = - (await resultReceive44)['addressArray'] as List; - p2pkhReceiveIndex = (await resultReceive44)['index'] as int; - p2pkhReceiveDerivations = (await resultReceive44)['derivations'] - as Map>; + // p2pkhReceiveAddressArray = + // (await resultReceive44)['addressArray'] as List; + // p2pkhReceiveIndex = (await resultReceive44)['index'] as int; + // p2pkhReceiveDerivations = (await resultReceive44)['derivations'] + // as Map>; p2wpkhReceiveAddressArray = (await resultReceive84)['addressArray'] as List; @@ -564,11 +563,11 @@ class ParticlWallet extends CoinServiceAPI { p2wpkhReceiveDerivations = (await resultReceive84)['derivations'] as Map>; - p2pkhChangeAddressArray = - (await resultChange44)['addressArray'] as List; - p2pkhChangeIndex = (await resultChange44)['index'] as int; - p2pkhChangeDerivations = (await resultChange44)['derivations'] - as Map>; + // p2pkhChangeAddressArray = + // (await resultChange44)['addressArray'] as List; + // p2pkhChangeIndex = (await resultChange44)['index'] as int; + // p2pkhChangeDerivations = (await resultChange44)['derivations'] + // as Map>; p2wpkhChangeAddressArray = (await resultChange84)['addressArray'] as List; @@ -577,12 +576,12 @@ class ParticlWallet extends CoinServiceAPI { as Map>; // save the derivations (if any) - if (p2pkhReceiveDerivations.isNotEmpty) { - await addDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - derivationsToAdd: p2pkhReceiveDerivations); - } + // if (p2pkhReceiveDerivations.isNotEmpty) { + // await addDerivations( + // chain: 0, + // derivePathType: DerivePathType.bip44, + // derivationsToAdd: p2pkhReceiveDerivations); + // } if (p2wpkhReceiveDerivations.isNotEmpty) { await addDerivations( @@ -590,12 +589,12 @@ class ParticlWallet extends CoinServiceAPI { derivePathType: DerivePathType.bip84, derivationsToAdd: p2wpkhReceiveDerivations); } - if (p2pkhChangeDerivations.isNotEmpty) { - await addDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - derivationsToAdd: p2pkhChangeDerivations); - } + // if (p2pkhChangeDerivations.isNotEmpty) { + // await addDerivations( + // chain: 1, + // derivePathType: DerivePathType.bip44, + // derivationsToAdd: p2pkhChangeDerivations); + // } if (p2wpkhChangeDerivations.isNotEmpty) { await addDerivations( @@ -606,12 +605,12 @@ class ParticlWallet extends CoinServiceAPI { // 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 (p2pkhReceiveIndex == -1) { + // final address = + // await _generateAddressForChain(0, 0, DerivePathType.bip44); + // p2pkhReceiveAddressArray.add(address); + // p2pkhReceiveIndex = 0; + // } if (p2wpkhReceiveIndex == -1) { final address = @@ -622,12 +621,12 @@ class ParticlWallet extends CoinServiceAPI { // 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 (p2pkhChangeIndex == -1) { + // final address = + // await _generateAddressForChain(1, 0, DerivePathType.bip44); + // p2pkhChangeAddressArray.add(address); + // p2pkhChangeIndex = 0; + // } if (p2wpkhChangeIndex == -1) { final address = @@ -644,14 +643,14 @@ class ParticlWallet extends CoinServiceAPI { 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: 'receivingAddressesP2PKH', + // value: p2pkhReceiveAddressArray); + // await DB.instance.put( + // boxName: walletId, + // key: 'changeAddressesP2PKH', + // value: p2pkhChangeAddressArray); await DB.instance.put( boxName: walletId, key: 'receivingIndexP2WPKH', @@ -660,12 +659,12 @@ class ParticlWallet extends CoinServiceAPI { 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: 'changeIndexP2PKH', value: p2pkhChangeIndex); + // await DB.instance.put( + // boxName: walletId, + // key: 'receivingIndexP2PKH', + // value: p2pkhReceiveIndex); await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance @@ -1229,7 +1228,12 @@ class ParticlWallet extends CoinServiceAPI { confirmations: 0, ); + Logging.instance.log("CACHED TX DATA IS: $cachedTxData", + level: LogLevel.Info, printFullLength: true); + if (cachedTxData == null) { + Logging.instance.log("CACHED TX DATA IS NULL : $cachedTxData", + level: LogLevel.Info, printFullLength: true); final data = await _fetchTransactionData(); _transactionData = Future(() => data); } @@ -1349,11 +1353,11 @@ class ParticlWallet extends CoinServiceAPI { 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 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])) { @@ -1365,16 +1369,16 @@ class ParticlWallet extends CoinServiceAPI { 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 < 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; } @@ -1438,10 +1442,10 @@ class ParticlWallet extends CoinServiceAPI { .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: "receivingIndexP2PKH", value: 0); + // await DB.instance + // .put(boxName: walletId, key: "changeIndexP2PKH", value: 0); await DB.instance.put( boxName: walletId, key: 'blocked_tx_hashes', @@ -1473,21 +1477,21 @@ class ParticlWallet extends CoinServiceAPI { ), // 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, - ), - ), + // _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, + // ), + // ), ]); // // P2PKH @@ -1545,16 +1549,17 @@ class ParticlWallet extends CoinServiceAPI { ), ); final data = PaymentData(pubkey: node.publicKey); - String address; + // String address; - switch (derivePathType) { - case DerivePathType.bip44: - address = P2PKH(data: data, network: _network).data.address!; - break; - case DerivePathType.bip84: - address = P2WPKH(network: _network, data: data).data.address!; - break; - } + // switch (derivePathType) { + // // case DerivePathType.bip44: + // // address = P2PKH(data: data, network: _network).data.address!; + // // break; + // case DerivePathType.bip84: + // address = P2WPKH(network: _network, data: data).data.address!; + // break; + // } + String address = P2WPKH(network: _network, data: data).data.address!; // add generated address & info to derivations await addDerivation( @@ -1574,15 +1579,15 @@ class ParticlWallet extends CoinServiceAPI { 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.bip84: - indexKey += "P2WPKH"; - break; - } - + // switch (derivePathType) { + // case DerivePathType.bip44: + // indexKey += "P2PKH"; + // break; + // case DerivePathType.bip84: + // indexKey += "P2WPKH"; + // break; + // } + indexKey += "P2WPKH"; final newIndex = (DB.instance.get(boxName: walletId, key: indexKey)) + 1; await DB.instance @@ -1601,9 +1606,9 @@ class ParticlWallet extends CoinServiceAPI { chainArray = 'changeAddresses'; } switch (derivePathType) { - case DerivePathType.bip44: - chainArray += "P2PKH"; - break; + // case DerivePathType.bip44: + // chainArray += "P2PKH"; + // break; case DerivePathType.bip84: chainArray += "P2WPKH"; break; @@ -1638,9 +1643,9 @@ class ParticlWallet extends CoinServiceAPI { // 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.bip44: + // arrayKey += "P2PKH"; + // break; case DerivePathType.bip84: arrayKey += "P2WPKH"; break; @@ -1656,14 +1661,15 @@ class ParticlWallet extends CoinServiceAPI { }) { String key; String chainId = chain == 0 ? "receive" : "change"; - switch (derivePathType) { - case DerivePathType.bip44: - key = "${walletId}_${chainId}DerivationsP2PKH"; - break; - case DerivePathType.bip84: - key = "${walletId}_${chainId}DerivationsP2WPKH"; - break; - } + key = "${walletId}_${chainId}DerivationsP2WPKH"; + // switch (derivePathType) { + // case DerivePathType.bip44: + // key = "${walletId}_${chainId}DerivationsP2PKH"; + // break; + // case DerivePathType.bip84: + // key = "${walletId}_${chainId}DerivationsP2WPKH"; + // break; + // } return key; } @@ -1972,9 +1978,9 @@ class ParticlWallet extends CoinServiceAPI { // Check the new receiving index String indexKey = "receivingIndex"; switch (derivePathType) { - case DerivePathType.bip44: - indexKey += "P2PKH"; - break; + // case DerivePathType.bip44: + // indexKey += "P2PKH"; + // break; case DerivePathType.bip84: indexKey += "P2WPKH"; break; @@ -1993,9 +1999,9 @@ class ParticlWallet extends CoinServiceAPI { // Set the new receiving address that the service switch (derivePathType) { - case DerivePathType.bip44: - _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); - break; + // case DerivePathType.bip44: + // _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); + // break; case DerivePathType.bip84: _currentReceivingAddress = Future(() => newReceivingAddress); break; @@ -2026,9 +2032,9 @@ class ParticlWallet extends CoinServiceAPI { // Check the new change index String indexKey = "changeIndex"; switch (derivePathType) { - case DerivePathType.bip44: - indexKey += "P2PKH"; - break; + // case DerivePathType.bip44: + // indexKey += "P2PKH"; + // break; case DerivePathType.bip84: indexKey += "P2WPKH"; break; @@ -2223,13 +2229,13 @@ class ParticlWallet extends CoinServiceAPI { final changeAddresses = DB.instance.get( boxName: walletId, key: 'changeAddressesP2WPKH') as List; - final changeAddressesP2PKH = - DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') - as List; - - for (var i = 0; i < changeAddressesP2PKH.length; i++) { - changeAddresses.add(changeAddressesP2PKH[i] as String); - } + // final changeAddressesP2PKH = + // DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') + // as List; + // + // for (var i = 0; i < changeAddressesP2PKH.length; i++) { + // changeAddresses.add(changeAddressesP2PKH[i] as String); + // } final List> allTxHashes = await _fetchHistory(allAddresses); @@ -2905,7 +2911,7 @@ class ParticlWallet extends CoinServiceAPI { print("CALLING FETCH BUILD TX DATA"); // addresses to check - List addressesP2PKH = []; + // List addressesP2PKH = []; List addressesP2WPKH = []; try { @@ -2927,9 +2933,9 @@ class ParticlWallet extends CoinServiceAPI { } (addressTxid[address] as List).add(txid); switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; + // case DerivePathType.bip44: + // addressesP2PKH.add(address); + // break; case DerivePathType.bip84: addressesP2WPKH.add(address); break; @@ -2939,62 +2945,62 @@ class ParticlWallet extends CoinServiceAPI { } // 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, - ), - }; - } - } - } - } - } + // 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, + // ), + // }; + // } + // } + // } + // } + // } // p2wpkh / bip84 final p2wpkhLength = addressesP2WPKH.length; @@ -3121,25 +3127,16 @@ class ParticlWallet extends CoinServiceAPI { rethrow; } - final builtTx = txb.buildIncomplete(); + final builtTx = txb.build(); final vSize = builtTx.virtualSize(); print("BUILT TX IS ${builtTx.toHex().toString()}"); - String hexBefore = builtTx.toHex(); - if (builtTx.toHex().toString().endsWith('0000')) { - // print("END WITH ZERO"); - String stripped = hexBefore.substring(0, hexBefore.length - 4); - return {"hex": stripped, "vSize": vSize}; - // } else { - // print("DOES NOT END WITH ZERO"); - // return {"hex": builtTx.toHex(), "vSize": vSize}; - } - return {"hex": builtTx.toHex(), "vSize": vSize}; + String hexBefore = builtTx.toHex().toString(); - // print("AND NOW IT IS $stripped"); - // - // return {"hex": stripped, "vSize": vSize}; + String strippedTrailingBytes = + hexBefore.replaceAll(RegExp(r"([.]*0+)(?!.*\d)"), ""); + return {"hex": strippedTrailingBytes, "vSize": vSize}; } @override @@ -3204,38 +3201,38 @@ class ParticlWallet extends CoinServiceAPI { // 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); + // 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); // p2wpkh final tempReceivingAddressesP2WPKH = DB.instance.get( @@ -3272,21 +3269,21 @@ class ParticlWallet extends CoinServiceAPI { .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"); + // 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"); // P2WPKH derivations final p2wpkhReceiveDerivationsString = await _secureStore.read( @@ -3322,41 +3319,41 @@ class ParticlWallet extends CoinServiceAPI { // 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); + // 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); // p2wpkh final tempReceivingAddressesP2WPKH = DB.instance @@ -3396,20 +3393,20 @@ class ParticlWallet extends CoinServiceAPI { .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"); + // 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"); // P2WPKH derivations final p2wpkhReceiveDerivationsString = From b890fe61dbb6c1a3d1bb860efcea16084c82012a Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 6 Dec 2022 20:22:14 +0200 Subject: [PATCH 055/103] Fix trailing bytes issue --- .../coins/particl/particl_wallet.dart | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 9a0f5da36..f837b3839 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -3134,9 +3134,22 @@ class ParticlWallet extends CoinServiceAPI { String hexBefore = builtTx.toHex().toString(); - String strippedTrailingBytes = - hexBefore.replaceAll(RegExp(r"([.]*0+)(?!.*\d)"), ""); - return {"hex": strippedTrailingBytes, "vSize": vSize}; + // String strippedTrailingBytes = + // hexBefore.replaceAll(RegExp(r"([.]*0+)(?!.*\d)"), ""); + // return {"hex": strippedTrailingBytes, "vSize": vSize}; + + if (hexBefore.endsWith('000000')) { + String stripped = hexBefore.substring(0, hexBefore.length - 6); + return {"hex": stripped, "vSize": vSize}; + } else if (hexBefore.endsWith('0000')) { + String stripped = hexBefore.substring(0, hexBefore.length - 4); + return {"hex": stripped, "vSize": vSize}; + } else if (hexBefore.endsWith('00')) { + String stripped = hexBefore.substring(0, hexBefore.length - 2); + return {"hex": stripped, "vSize": vSize}; + } else { + return {"hex": hexBefore, "vSize": vSize}; + } } @override From 9dc9682686d9618b1c3b43df4444b34e9c39ee8d Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Tue, 6 Dec 2022 15:31:47 -0700 Subject: [PATCH 056/103] Bump version (1.5.24, build 97) iOS only) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9d0551e7a..3d129cec3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.23+96 +version: 1.5.24+97 environment: sdk: ">=2.17.0 <3.0.0" From 4e3a5d23db4fd94fdec641c00219906cdcf92d9b Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 7 Dec 2022 11:20:35 +0200 Subject: [PATCH 057/103] Update node to stack node --- lib/utilities/default_nodes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index bab113cde..1757b956e 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -146,7 +146,7 @@ abstract class DefaultNodes { ); static NodeModel get particl => NodeModel( - host: "164.92.93.20", + host: "particl.stackwallet.com", port: 50002, name: defaultName, id: _nodeId(Coin.particl), From 76c57eef64a8c4ae35b6b4a5b0ca34867c4417bd Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 7 Dec 2022 14:46:53 +0200 Subject: [PATCH 058/103] Fix fetch tx error after broadcast --- .../coins/particl/particl_wallet.dart | 638 +++++++++--------- 1 file changed, 308 insertions(+), 330 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index f837b3839..01c2e8773 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -50,7 +50,7 @@ const String GENESIS_HASH_MAINNET = const String GENESIS_HASH_TESTNET = "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"; -enum DerivePathType { bip84 } +enum DerivePathType { bip44, bip84 } bip32.BIP32 getBip32Node( int chain, @@ -93,6 +93,8 @@ bip32.BIP32 getBip32NodeFromRoot( throw Exception("Invalid Particl network type used!"); } switch (derivePathType) { + case DerivePathType.bip44: + return root.derivePath("m/44'/$coinType'/0'/$chain/$index"); case DerivePathType.bip84: return root.derivePath("m/84'/$coinType'/0'/$chain/$index"); default: @@ -228,10 +230,10 @@ class ParticlWallet extends CoinServiceAPI { _getCurrentAddressForChain(0, DerivePathType.bip84); Future? _currentReceivingAddress; - // Future get currentLegacyReceivingAddress => - // _currentReceivingAddressP2PKH ??= - // _getCurrentAddressForChain(0, DerivePathType.bip44); - // Future? _currentReceivingAddressP2PKH; + Future get currentLegacyReceivingAddress => + _currentReceivingAddressP2PKH ??= + _getCurrentAddressForChain(0, DerivePathType.bip44); + Future? _currentReceivingAddressP2PKH; @override Future exit() async { @@ -292,28 +294,28 @@ class ParticlWallet extends CoinServiceAPI { // Base58check decode fail } - return DerivePathType.bip84; - // 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, particl.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; - // } + // return DerivePathType.bip84; + 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, particl.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; @@ -339,7 +341,6 @@ class ParticlWallet extends CoinServiceAPI { throw Exception("genesis hash does not match main net!"); } break; - break; default: throw Exception( "Attempted to generate a ParticlWallet using a non particl coin type: ${coin.name}"); @@ -417,13 +418,13 @@ class ParticlWallet extends CoinServiceAPI { ); String? address; switch (type) { - // case DerivePathType.bip44: - // address = P2PKH( - // data: PaymentData(pubkey: node.publicKey), - // network: _network) - // .data - // .address!; - // break; + case DerivePathType.bip44: + address = P2PKH( + data: PaymentData(pubkey: node.publicKey), + network: _network) + .data + .address!; + break; case DerivePathType.bip84: address = P2WPKH( network: _network, @@ -510,21 +511,21 @@ class ParticlWallet extends CoinServiceAPI { }) async { longMutex = true; - // Map> p2pkhReceiveDerivations = {}; + Map> p2pkhReceiveDerivations = {}; Map> p2wpkhReceiveDerivations = {}; - // Map> p2pkhChangeDerivations = {}; + Map> p2pkhChangeDerivations = {}; Map> p2wpkhChangeDerivations = {}; final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); - // List p2pkhReceiveAddressArray = []; + List p2pkhReceiveAddressArray = []; List p2wpkhReceiveAddressArray = []; - // int p2pkhReceiveIndex = -1; + int p2pkhReceiveIndex = -1; int p2wpkhReceiveIndex = -1; - // List p2pkhChangeAddressArray = []; + List p2pkhChangeAddressArray = []; List p2wpkhChangeAddressArray = []; - // int p2pkhChangeIndex = -1; + int p2pkhChangeIndex = -1; int p2wpkhChangeIndex = -1; // actual size is 24 due to p2pkh, and p2wpkh so 12x2 @@ -534,8 +535,8 @@ class ParticlWallet extends CoinServiceAPI { // receiving addresses Logging.instance .log("checking receiving addresses...", level: LogLevel.Info); - // final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, - // maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); + final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); final resultReceive84 = _checkGaps(maxNumberOfIndexesToCheck, maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 0); @@ -543,19 +544,20 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance .log("checking change addresses...", level: LogLevel.Info); // change addresses - // final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, - // maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); + final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); final resultChange84 = _checkGaps(maxNumberOfIndexesToCheck, maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 1); - await Future.wait([resultReceive84, resultChange84]); + await Future.wait( + [resultReceive44, resultReceive84, resultChange44, resultChange84]); - // p2pkhReceiveAddressArray = - // (await resultReceive44)['addressArray'] as List; - // p2pkhReceiveIndex = (await resultReceive44)['index'] as int; - // p2pkhReceiveDerivations = (await resultReceive44)['derivations'] - // as Map>; + p2pkhReceiveAddressArray = + (await resultReceive44)['addressArray'] as List; + p2pkhReceiveIndex = (await resultReceive44)['index'] as int; + p2pkhReceiveDerivations = (await resultReceive44)['derivations'] + as Map>; p2wpkhReceiveAddressArray = (await resultReceive84)['addressArray'] as List; @@ -563,11 +565,11 @@ class ParticlWallet extends CoinServiceAPI { p2wpkhReceiveDerivations = (await resultReceive84)['derivations'] as Map>; - // p2pkhChangeAddressArray = - // (await resultChange44)['addressArray'] as List; - // p2pkhChangeIndex = (await resultChange44)['index'] as int; - // p2pkhChangeDerivations = (await resultChange44)['derivations'] - // as Map>; + p2pkhChangeAddressArray = + (await resultChange44)['addressArray'] as List; + p2pkhChangeIndex = (await resultChange44)['index'] as int; + p2pkhChangeDerivations = (await resultChange44)['derivations'] + as Map>; p2wpkhChangeAddressArray = (await resultChange84)['addressArray'] as List; @@ -576,12 +578,12 @@ class ParticlWallet extends CoinServiceAPI { as Map>; // save the derivations (if any) - // if (p2pkhReceiveDerivations.isNotEmpty) { - // await addDerivations( - // chain: 0, - // derivePathType: DerivePathType.bip44, - // derivationsToAdd: p2pkhReceiveDerivations); - // } + if (p2pkhReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhReceiveDerivations); + } if (p2wpkhReceiveDerivations.isNotEmpty) { await addDerivations( @@ -589,12 +591,12 @@ class ParticlWallet extends CoinServiceAPI { derivePathType: DerivePathType.bip84, derivationsToAdd: p2wpkhReceiveDerivations); } - // if (p2pkhChangeDerivations.isNotEmpty) { - // await addDerivations( - // chain: 1, - // derivePathType: DerivePathType.bip44, - // derivationsToAdd: p2pkhChangeDerivations); - // } + if (p2pkhChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhChangeDerivations); + } if (p2wpkhChangeDerivations.isNotEmpty) { await addDerivations( @@ -605,12 +607,12 @@ class ParticlWallet extends CoinServiceAPI { // 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 (p2pkhReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip44); + p2pkhReceiveAddressArray.add(address); + p2pkhReceiveIndex = 0; + } if (p2wpkhReceiveIndex == -1) { final address = @@ -621,12 +623,12 @@ class ParticlWallet extends CoinServiceAPI { // 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 (p2pkhChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip44); + p2pkhChangeAddressArray.add(address); + p2pkhChangeIndex = 0; + } if (p2wpkhChangeIndex == -1) { final address = @@ -643,14 +645,14 @@ class ParticlWallet extends CoinServiceAPI { 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: 'receivingAddressesP2PKH', + value: p2pkhReceiveAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: p2pkhChangeAddressArray); await DB.instance.put( boxName: walletId, key: 'receivingIndexP2WPKH', @@ -659,12 +661,12 @@ class ParticlWallet extends CoinServiceAPI { 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: 'changeIndexP2PKH', value: p2pkhChangeIndex); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: p2pkhReceiveIndex); await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance @@ -934,15 +936,12 @@ class ParticlWallet extends CoinServiceAPI { 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) { @@ -1228,20 +1227,15 @@ class ParticlWallet extends CoinServiceAPI { confirmations: 0, ); - Logging.instance.log("CACHED TX DATA IS: $cachedTxData", - level: LogLevel.Info, printFullLength: true); - if (cachedTxData == null) { - Logging.instance.log("CACHED TX DATA IS NULL : $cachedTxData", - level: LogLevel.Info, printFullLength: true); final data = await _fetchTransactionData(); _transactionData = Future(() => data); + } else { + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); } - - final transactions = cachedTxData!.getAllTransactions(); - transactions[tx.txid] = tx; - cachedTxData = models.TransactionData.fromMap(transactions); - _transactionData = Future(() => cachedTxData!); } @override @@ -1353,11 +1347,11 @@ class ParticlWallet extends CoinServiceAPI { 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 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])) { @@ -1442,10 +1436,10 @@ class ParticlWallet extends CoinServiceAPI { .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: "receivingIndexP2PKH", value: 0); + await DB.instance + .put(boxName: walletId, key: "changeIndexP2PKH", value: 0); await DB.instance.put( boxName: walletId, key: 'blocked_tx_hashes', @@ -1510,21 +1504,6 @@ class ParticlWallet extends CoinServiceAPI { // 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); } @@ -1549,17 +1528,17 @@ class ParticlWallet extends CoinServiceAPI { ), ); final data = PaymentData(pubkey: node.publicKey); - // String address; + String address; - // switch (derivePathType) { - // // case DerivePathType.bip44: - // // address = P2PKH(data: data, network: _network).data.address!; - // // break; - // case DerivePathType.bip84: - // address = P2WPKH(network: _network, data: data).data.address!; - // break; - // } - String address = P2WPKH(network: _network, data: data).data.address!; + switch (derivePathType) { + case DerivePathType.bip44: + address = P2PKH(data: data, network: _network).data.address!; + break; + case DerivePathType.bip84: + address = P2WPKH(network: _network, data: data).data.address!; + break; + } + // String address = P2WPKH(network: _network, data: data).data.address!; // add generated address & info to derivations await addDerivation( @@ -1579,15 +1558,14 @@ class ParticlWallet extends CoinServiceAPI { 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.bip84: - // indexKey += "P2WPKH"; - // break; - // } - indexKey += "P2WPKH"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } final newIndex = (DB.instance.get(boxName: walletId, key: indexKey)) + 1; await DB.instance @@ -1606,9 +1584,9 @@ class ParticlWallet extends CoinServiceAPI { chainArray = 'changeAddresses'; } switch (derivePathType) { - // case DerivePathType.bip44: - // chainArray += "P2PKH"; - // break; + case DerivePathType.bip44: + chainArray += "P2PKH"; + break; case DerivePathType.bip84: chainArray += "P2WPKH"; break; @@ -1643,9 +1621,9 @@ class ParticlWallet extends CoinServiceAPI { // 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.bip44: + arrayKey += "P2PKH"; + break; case DerivePathType.bip84: arrayKey += "P2WPKH"; break; @@ -1661,15 +1639,15 @@ class ParticlWallet extends CoinServiceAPI { }) { String key; String chainId = chain == 0 ? "receive" : "change"; - key = "${walletId}_${chainId}DerivationsP2WPKH"; - // switch (derivePathType) { - // case DerivePathType.bip44: - // key = "${walletId}_${chainId}DerivationsP2PKH"; - // break; - // case DerivePathType.bip84: - // key = "${walletId}_${chainId}DerivationsP2WPKH"; - // break; - // } + + switch (derivePathType) { + case DerivePathType.bip44: + key = "${walletId}_${chainId}DerivationsP2PKH"; + break; + case DerivePathType.bip84: + key = "${walletId}_${chainId}DerivationsP2WPKH"; + break; + } return key; } @@ -1978,9 +1956,9 @@ class ParticlWallet extends CoinServiceAPI { // Check the new receiving index String indexKey = "receivingIndex"; switch (derivePathType) { - // case DerivePathType.bip44: - // indexKey += "P2PKH"; - // break; + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; case DerivePathType.bip84: indexKey += "P2WPKH"; break; @@ -1999,9 +1977,9 @@ class ParticlWallet extends CoinServiceAPI { // Set the new receiving address that the service switch (derivePathType) { - // case DerivePathType.bip44: - // _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); - // break; + case DerivePathType.bip44: + _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); + break; case DerivePathType.bip84: _currentReceivingAddress = Future(() => newReceivingAddress); break; @@ -2032,9 +2010,9 @@ class ParticlWallet extends CoinServiceAPI { // Check the new change index String indexKey = "changeIndex"; switch (derivePathType) { - // case DerivePathType.bip44: - // indexKey += "P2PKH"; - // break; + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; case DerivePathType.bip84: indexKey += "P2WPKH"; break; @@ -2911,7 +2889,7 @@ class ParticlWallet extends CoinServiceAPI { print("CALLING FETCH BUILD TX DATA"); // addresses to check - // List addressesP2PKH = []; + List addressesP2PKH = []; List addressesP2WPKH = []; try { @@ -2933,9 +2911,9 @@ class ParticlWallet extends CoinServiceAPI { } (addressTxid[address] as List).add(txid); switch (addressType(address: address)) { - // case DerivePathType.bip44: - // addressesP2PKH.add(address); - // break; + case DerivePathType.bip44: + addressesP2PKH.add(address); + break; case DerivePathType.bip84: addressesP2WPKH.add(address); break; @@ -2945,62 +2923,62 @@ class ParticlWallet extends CoinServiceAPI { } // 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, - // ), - // }; - // } - // } - // } - // } - // } + 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, + ), + }; + } + } + } + } + } // p2wpkh / bip84 final p2wpkhLength = addressesP2WPKH.length; @@ -3214,38 +3192,38 @@ class ParticlWallet extends CoinServiceAPI { // 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); + 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); // p2wpkh final tempReceivingAddressesP2WPKH = DB.instance.get( @@ -3282,21 +3260,21 @@ class ParticlWallet extends CoinServiceAPI { .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"); + 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"); // P2WPKH derivations final p2wpkhReceiveDerivationsString = await _secureStore.read( @@ -3332,41 +3310,41 @@ class ParticlWallet extends CoinServiceAPI { // 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); + 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); // p2wpkh final tempReceivingAddressesP2WPKH = DB.instance @@ -3406,20 +3384,20 @@ class ParticlWallet extends CoinServiceAPI { .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"); + 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"); // P2WPKH derivations final p2wpkhReceiveDerivationsString = From 9309a86cfda5a991893b713ba87c5a342f15af9a Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 7 Dec 2022 15:25:48 +0200 Subject: [PATCH 059/103] Add back bip44, ensure we're using default bip84 addresses --- .../coins/particl/particl_wallet.dart | 68 +++++++------------ 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 01c2e8773..1fc6a64aa 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -1363,16 +1363,16 @@ class ParticlWallet extends CoinServiceAPI { 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 < 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; } @@ -1471,40 +1471,23 @@ class ParticlWallet extends CoinServiceAPI { ), // 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, - // ), - // ), + _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, + ), + ), ]); - // // 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, - // )); - // - Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); } @@ -1538,7 +1521,6 @@ class ParticlWallet extends CoinServiceAPI { address = P2WPKH(network: _network, data: data).data.address!; break; } - // String address = P2WPKH(network: _network, data: data).data.address!; // add generated address & info to derivations await addDerivation( From 20dbb86742471860f2a447923516cb4a2e4670cb Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 7 Dec 2022 17:47:55 +0200 Subject: [PATCH 060/103] Update pubspec.yaml to use commit for Particl flag in bitcoindart --- .../coins/particl/particl_wallet.dart | 22 +------ lib/utilities/default_nodes.dart | 2 +- pubspec.lock | 66 +++++++++---------- pubspec.yaml | 2 +- 4 files changed, 36 insertions(+), 56 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 1fc6a64aa..3a1f58651 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -3048,9 +3048,6 @@ class ParticlWallet extends CoinServiceAPI { // Add transaction inputs for (var i = 0; i < utxosToUse.length; i++) { - Logging.instance.log("UTXOs TO USE IS -----${utxosToUse[i].vout}", - level: LogLevel.Info, printFullLength: true); - final txid = utxosToUse[i].txid; txb.addInput(txid, utxosToUse[i].vout, null, utxoSigningData[txid]["output"] as Uint8List, ''); @@ -3065,16 +3062,6 @@ class ParticlWallet extends CoinServiceAPI { // Sign the transaction accordingly for (var i = 0; i < utxosToUse.length; i++) { final txid = utxosToUse[i].txid; - Logging.instance.log("WITNESS VALUE IS -----${utxosToUse[i].value}", - level: LogLevel.Info, printFullLength: true); - - Logging.instance.log( - "REDEEM SCRIPT IS -----${utxoSigningData[txid]["redeemScript"]}", - level: LogLevel.Info, - printFullLength: true); - - Logging.instance.log("AND THIS DATA IS -----${utxoSigningData[txid]}", - level: LogLevel.Info, printFullLength: true); txb.sign( vin: i, keyPair: utxoSigningData[txid]["keyPair"] as ECPair, @@ -3090,14 +3077,7 @@ class ParticlWallet extends CoinServiceAPI { final builtTx = txb.build(); final vSize = builtTx.virtualSize(); - print("BUILT TX IS ${builtTx.toHex().toString()}"); - - String hexBefore = builtTx.toHex().toString(); - - // String strippedTrailingBytes = - // hexBefore.replaceAll(RegExp(r"([.]*0+)(?!.*\d)"), ""); - // return {"hex": strippedTrailingBytes, "vSize": vSize}; - + String hexBefore = builtTx.toHex(isParticl: true).toString(); if (hexBefore.endsWith('000000')) { String stripped = hexBefore.substring(0, hexBefore.length - 6); return {"hex": stripped, "vSize": vSize}; diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 1757b956e..bab113cde 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -146,7 +146,7 @@ abstract class DefaultNodes { ); static NodeModel get particl => NodeModel( - host: "particl.stackwallet.com", + host: "164.92.93.20", port: 50002, name: defaultName, id: _nodeId(Coin.particl), diff --git a/pubspec.lock b/pubspec.lock index 9cfa3ea8d..e8f875d35 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -56,7 +56,7 @@ packages: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.4.0" async: dependency: transitive description: @@ -109,8 +109,8 @@ packages: dependency: "direct main" description: path: "." - ref: particl - resolved-ref: "65eb920719c8f7895c5402a07497647e7fc4b346" + ref: "004d6f82dff7389b561e5078b4649adcd2d9c10f" + resolved-ref: "004d6f82dff7389b561e5078b4649adcd2d9c10f" url: "https://github.com/cypherstack/bitcoindart.git" source: git version: "3.0.1" @@ -169,7 +169,7 @@ packages: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.2.6" + version: "7.2.7" built_collection: dependency: transitive description: @@ -183,7 +183,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.4.1" + version: "8.4.2" characters: dependency: transitive description: @@ -260,7 +260,7 @@ packages: name: connectivity_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.2.3" connectivity_plus_web: dependency: transitive description: @@ -379,7 +379,7 @@ packages: name: decimal url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.2" dependency_validator: dependency: "direct dev" description: @@ -463,7 +463,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "5.2.1" + version: "5.2.3" fixnum: dependency: transitive description: @@ -550,7 +550,7 @@ packages: name: flutter_mobx url: "https://pub.dartlang.org" source: hosted - version: "2.0.6+4" + version: "2.0.6+5" flutter_native_splash: dependency: "direct main" description: @@ -592,35 +592,35 @@ packages: name: flutter_secure_storage_linux url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.1.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.3" flutter_spinkit: dependency: "direct main" description: @@ -634,7 +634,7 @@ packages: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "1.1.5" + version: "1.1.6" flutter_test: dependency: "direct dev" description: flutter @@ -677,7 +677,7 @@ packages: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" hex: dependency: transitive description: @@ -719,7 +719,7 @@ packages: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: "direct main" description: @@ -843,7 +843,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" local_auth: dependency: "direct main" description: @@ -892,14 +892,14 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" mobx: dependency: transitive description: name: mobx url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.3" mockingjay: dependency: "direct dev" description: @@ -927,7 +927,7 @@ packages: name: mutex url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" nm: dependency: transitive description: @@ -1025,7 +1025,7 @@ packages: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.20" + version: "2.0.22" path_provider_ios: dependency: transitive description: @@ -1067,7 +1067,7 @@ packages: name: permission_handler url: "https://pub.dartlang.org" source: hosted - version: "10.1.0" + version: "10.2.0" permission_handler_android: dependency: transitive description: @@ -1151,7 +1151,7 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" pubspec_parse: dependency: transitive description: @@ -1179,7 +1179,7 @@ packages: name: rational url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.2" riverpod: dependency: transitive description: @@ -1207,7 +1207,7 @@ packages: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.5" + version: "0.27.7" share_plus: dependency: "direct main" description: @@ -1235,7 +1235,7 @@ packages: name: share_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "3.2.0" share_plus_web: dependency: transitive description: @@ -1333,7 +1333,7 @@ packages: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" sky_engine: dependency: transitive description: flutter @@ -1410,7 +1410,7 @@ packages: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -1522,14 +1522,14 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.1.6" + version: "6.1.7" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.19" + version: "6.0.22" url_launcher_ios: dependency: transitive description: @@ -1578,7 +1578,7 @@ packages: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.6" + version: "3.0.7" vector_math: dependency: transitive description: @@ -1662,7 +1662,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.1.2" window_size: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d1f834241..8e0b7a6a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: bitcoindart: git: url: https://github.com/cypherstack/bitcoindart.git - ref: particl # TODO change to hash ref when merging in particl support + ref: 004d6f82dff7389b561e5078b4649adcd2d9c10f # TODO change to hash ref when merging in particl support stack_wallet_backup: git: From 11d5ad44279becc82c3bf86ec2fb7b277c0aa0c8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 7 Dec 2022 11:32:20 -0600 Subject: [PATCH 061/103] add particl logo --- assets/svg/coin_icons/Particl.svg | 1 + lib/utilities/assets.dart | 3 +-- lib/utilities/theme/color_theme.dart | 2 +- pubspec.yaml | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 assets/svg/coin_icons/Particl.svg diff --git a/assets/svg/coin_icons/Particl.svg b/assets/svg/coin_icons/Particl.svg new file mode 100644 index 000000000..3f8a920ab --- /dev/null +++ b/assets/svg/coin_icons/Particl.svg @@ -0,0 +1 @@ +particl-part-logo \ No newline at end of file diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 5e7e15f0b..1d37113fb 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -180,8 +180,7 @@ class _SVG { String get monero => "assets/svg/coin_icons/Monero.svg"; String get wownero => "assets/svg/coin_icons/Wownero.svg"; String get namecoin => "assets/svg/coin_icons/Namecoin.svg"; - String get particl => - "assets/svg/coin_icons/Namecoin.svg"; //TODO - Update icon to particl + String get particl => "assets/svg/coin_icons/Particl.svg"; String get chevronRight => "assets/svg/chevron-right.svg"; String get minimize => "assets/svg/minimize.svg"; diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 8de0954e8..34cdc3187 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -193,7 +193,7 @@ class CoinThemeColor { Color get monero => const Color(0xFFFF9E6B); Color get namecoin => const Color(0xFF91B1E1); Color get wownero => const Color(0xFFED80C1); - Color get particl => const Color(0xFFED80C1); //TODO - Use part colors + Color get particl => const Color(0xFF8175BD); Color forCoin(Coin coin) { switch (coin) { diff --git a/pubspec.yaml b/pubspec.yaml index 8e0b7a6a0..f290a7dfc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -310,6 +310,7 @@ flutter: - assets/svg/coin_icons/Monero.svg - assets/svg/coin_icons/Wownero.svg - assets/svg/coin_icons/Namecoin.svg + - assets/svg/coin_icons/Particl.svg # lottie animations - assets/lottie/test.json - assets/lottie/test2.json From ffeaeacc29dc85dda2f93eded5acc5dcf31bf460 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 7 Dec 2022 11:40:19 -0600 Subject: [PATCH 062/103] add particl bgimg --- assets/images/particl.png | Bin 0 -> 320172 bytes lib/utilities/assets.dart | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 assets/images/particl.png diff --git a/assets/images/particl.png b/assets/images/particl.png new file mode 100644 index 0000000000000000000000000000000000000000..bfcdfbf370d8df7ea156db9f17c419dca8365da1 GIT binary patch literal 320172 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
  • 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

    )@zUJ~}Oc3F@@Uq>O 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≫rW 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 literal 0 HcmV?d00001 diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 1d37113fb..b02c6584e 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -246,7 +246,7 @@ class _PNG { String get epicCash => "assets/images/epic-cash.png"; String get bitcoincash => "assets/images/bitcoincash.png"; String get namecoin => "assets/images/namecoin.png"; - String get particl => "assets/images/namecoin.png"; //TODO - use particl png + String get particl => "assets/images/particl.png"; String get glasses => "assets/images/glasses.png"; String get glassesHidden => "assets/images/glasses-hidden.png"; From cd419ee02c581b2d205f6f87793d4d9a4c2748e9 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 7 Dec 2022 12:09:10 -0600 Subject: [PATCH 063/103] update particl bgimg --- assets/images/particl.png | Bin 320172 -> 342818 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/images/particl.png b/assets/images/particl.png index bfcdfbf370d8df7ea156db9f17c419dca8365da1..ef5939f474a6079f4a7f30fac841a469f0cc7905 100644 GIT binary patch literal 342818 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�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

    DIC_1@kY6X1--0O~Px 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

  • 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

    )@zUJ~}Oc3F@@Uq>O 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≫rW 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 From a760e4989aa3cdcb5fef241a58ba518209c4ea7f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 7 Dec 2022 12:12:33 -0600 Subject: [PATCH 064/103] add bgimg to pubspec --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index f290a7dfc..e7a9113fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -209,6 +209,7 @@ flutter: - assets/images/epic-cash.png - assets/images/bitcoincash.png - assets/images/namecoin.png + - assets/images/particl.png - assets/images/glasses.png - assets/images/glasses-hidden.png - assets/svg/plus.svg From a8c1787dbccd474215a50172d449dba4e9c8c21b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 7 Dec 2022 14:47:22 -0600 Subject: [PATCH 065/103] add particl to list of default nodes --- lib/utilities/default_nodes.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index bab113cde..21da92a8e 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -18,6 +18,7 @@ abstract class DefaultNodes { bitcoincash, namecoin, wownero, + particl, bitcoinTestnet, litecoinTestNet, bitcoincashTestnet, From 0f7a5cb5e63a258e929939ed2a57728e4b544890 Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 8 Dec 2022 13:27:17 +0200 Subject: [PATCH 066/103] Clean up --- lib/services/coins/particl/particl_wallet.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 3a1f58651..5379041e1 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -2189,13 +2189,6 @@ class ParticlWallet extends CoinServiceAPI { final changeAddresses = DB.instance.get( boxName: walletId, key: 'changeAddressesP2WPKH') as List; - // final changeAddressesP2PKH = - // DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') - // as List; - // - // for (var i = 0; i < changeAddressesP2PKH.length; i++) { - // changeAddresses.add(changeAddressesP2PKH[i] as String); - // } final List> allTxHashes = await _fetchHistory(allAddresses); From 028e77baf4f21023a1100a35f23ec6b10014ccb8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 8 Dec 2022 13:51:49 -0600 Subject: [PATCH 067/103] detect and handle ct and ringct transactions accordingly and hopefully catch staking outputs as well --- lib/models/paymint/transactions_model.dart | 54 +++++++++++---- .../coins/particl/particl_wallet.dart | 65 +++++++++++++++---- 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/lib/models/paymint/transactions_model.dart b/lib/models/paymint/transactions_model.dart index 382459922..9090115cd 100644 --- a/lib/models/paymint/transactions_model.dart +++ b/lib/models/paymint/transactions_model.dart @@ -380,19 +380,45 @@ class Output { factory Output.fromJson(Map json) { // TODO determine if any of this code is needed. - final address = json["scriptPubKey"]["addresses"] == null - ? json['scriptPubKey']['type'] as String - : json["scriptPubKey"]["addresses"][0] as String; - return Output( - scriptpubkey: json['scriptPubKey']['hex'] as String?, - scriptpubkeyAsm: json['scriptPubKey']['asm'] as String?, - scriptpubkeyType: json['scriptPubKey']['type'] as String?, - scriptpubkeyAddress: address, - value: (Decimal.parse(json["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin(Coin - .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure - .toBigInt() - .toInt(), - ); + // Particl has different tx types that need to be detected and handled here + if (json.containsKey('scriptPubKey') as bool) { + // output is transparent + final address = json["scriptPubKey"]["addresses"] == null + ? json['scriptPubKey']['type'] as String + : json["scriptPubKey"]["addresses"][0] as String; + return Output( + scriptpubkey: json['scriptPubKey']['hex'] as String?, + scriptpubkeyAsm: json['scriptPubKey']['asm'] as String?, + scriptpubkeyType: json['scriptPubKey']['type'] as String?, + scriptpubkeyAddress: address, + value: (Decimal.parse(json["value"].toString()) * + Decimal.fromInt(Constants.satsPerCoin(Coin + .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure + .toBigInt() + .toInt(), + ); + } /* else if (json.containsKey('ct_fee') as bool) { + // or type: data + // output is blinded (CT) + } else if (json.containsKey('rangeproof') as bool) { + // or valueCommitment or type: anon + // output is private (RingCT) + } */ + else { + // TODO detect staking + // TODO handle CT, RingCT, and staking accordingly + // print("transaction not supported: ${json}"); + return Output( + // Return output object with null values; allows wallet history to be built + scriptpubkey: null, + scriptpubkeyAsm: null, + scriptpubkeyType: null, + scriptpubkeyAddress: "", + value: (Decimal.parse(0.toString()) * + Decimal.fromInt(Constants.satsPerCoin(Coin + .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure + .toBigInt() + .toInt()); + } } } diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 5379041e1..07e255bab 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -2297,9 +2297,24 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance.log("sendersArray: $sendersArray", level: LogLevel.Info); for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["address"] as String?; - if (address != null) { - recipientsArray.add(address); + // Particl has different tx types that need to be detected and handled here + if (output.containsKey('scriptPubKey') as bool) { + // Logging.instance.log("output is transparent", level: LogLevel.Info); + final address = output["scriptPubKey"]["address"] as String?; + if (address != null) { + recipientsArray.add(address); + } + } else if (output.containsKey('ct_fee') as bool) { + // or type: data + Logging.instance.log("output is blinded (CT)", level: LogLevel.Info); + } else if (output.containsKey('rangeproof') as bool) { + // or valueCommitment or type: anon + Logging.instance + .log("output is private (RingCT)", level: LogLevel.Info); + } else { + // TODO detect staking + Logging.instance.log("output type not detected; output: ${output}", + level: LogLevel.Info); } } @@ -2336,19 +2351,41 @@ class ParticlWallet extends CoinServiceAPI { totalInput = inputAmtSentFromWallet; int totalOutput = 0; + Logging.instance.log("txObject: ${txObject}", level: LogLevel.Info); + for (final output in txObject["vout"] as List) { - final String address = output["scriptPubKey"]!["address"] as String; - final value = output["value"]!; - final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toBigInt() - .toInt(); - totalOutput += _value; - if (changeAddresses.contains(address)) { - inputAmtSentFromWallet -= _value; + // Particl has different tx types that need to be detected and handled here + if (output.containsKey('scriptPubKey') as bool) { + // Logging.instance.log("output is transparent", level: LogLevel.Info); + final String address = output["scriptPubKey"]!["address"] as String; + final value = output["value"]!; + final _value = (Decimal.parse(value.toString()) * + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); + totalOutput += _value; + if (changeAddresses.contains(address)) { + inputAmtSentFromWallet -= _value; + } else { + // change address from 'sent from' to the 'sent to' address + txObject["address"] = address; + } + } else if (output.containsKey('ct_fee') as bool) { + // or type: data + // TODO handle CT tx + Logging.instance.log( + "output is blinded (CT); cannot parse output values", + level: LogLevel.Info); + } else if (output.containsKey('rangeproof') as bool) { + // or valueCommitment or type: anon + // TODO handle RingCT tx + Logging.instance.log( + "output is private (RingCT); cannot parse output values", + level: LogLevel.Info); } else { - // change address from 'sent from' to the 'sent to' address - txObject["address"] = address; + // TODO detect staking + Logging.instance.log("output type not detected; output: ${output}", + level: LogLevel.Info); } } // calculate transaction fee From 4aa6fc5d800c5000e12c2dee5ea0bf3e60416b85 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 8 Dec 2022 17:37:51 -0600 Subject: [PATCH 068/103] do not trigger null safety --- lib/models/paymint/transactions_model.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/models/paymint/transactions_model.dart b/lib/models/paymint/transactions_model.dart index 9090115cd..06a21e16a 100644 --- a/lib/models/paymint/transactions_model.dart +++ b/lib/models/paymint/transactions_model.dart @@ -410,9 +410,9 @@ class Output { // print("transaction not supported: ${json}"); return Output( // Return output object with null values; allows wallet history to be built - scriptpubkey: null, - scriptpubkeyAsm: null, - scriptpubkeyType: null, + scriptpubkey: "", + scriptpubkeyAsm: "", + scriptpubkeyType: "", scriptpubkeyAddress: "", value: (Decimal.parse(0.toString()) * Decimal.fromInt(Constants.satsPerCoin(Coin From 7ec0606b08edcaf50edf3b7f4b0342b156a74ba6 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 8 Dec 2022 17:37:57 -0600 Subject: [PATCH 069/103] account for ct_fee --- lib/services/coins/particl/particl_wallet.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 07e255bab..0249fb205 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -2376,6 +2376,15 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance.log( "output is blinded (CT); cannot parse output values", level: LogLevel.Info); + final ct_fee = output["ct_fee"]!; + final fee_value = (Decimal.parse(ct_fee.toString()) * + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); + Logging.instance.log( + "ct_fee ${ct_fee} subtracted from inputAmtSentFromWallet ${inputAmtSentFromWallet}", + level: LogLevel.Info); + inputAmtSentFromWallet += fee_value; } else if (output.containsKey('rangeproof') as bool) { // or valueCommitment or type: anon // TODO handle RingCT tx From a15c975553b909c3bbec49e0e12f86bb3194e3a4 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 09:24:39 -0600 Subject: [PATCH 070/103] fix bip49 derivation path test --- test/services/coins/particl/particl_wallet_test.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 048c8eeb1..134b5c2ee 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:decimal/decimal.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; @@ -42,9 +40,9 @@ void main() { }); test("particl DerivePathType enum", () { - expect(DerivePathType.values.length, 3); + expect(DerivePathType.values.length, 2); expect(DerivePathType.values.toString(), - "[DerivePathType.bip44, DerivePathType.bip49, DerivePathType.bip84]"); // TODO does particl have BIP49, P2WPKH-P2SH? I'd think no + "[DerivePathType.bip44, DerivePathType.bip84]"); }); group("bip32 node/root", () { From 81935aa00cd62a48929a0553092e0c4cd8b823d9 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 09:30:41 -0600 Subject: [PATCH 071/103] comment cleaning --- test/services/coins/particl/particl_wallet_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 134b5c2ee..25a7b06b6 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -228,7 +228,7 @@ void main() { // late FakeSecureStorage secureStore; // MockTransactionNotificationTracker? tracker; - // NamecoinWallet? nmc; + // ParticlWallet? part; // setUp(() { // client = MockElectrumX(); @@ -237,7 +237,7 @@ void main() { // secureStore = FakeSecureStorage(); // tracker = MockTransactionNotificationTracker(); - // nmc = NamecoinWallet( + // nmc = ParticlWallet( // walletId: "testNetworkConnection", // walletName: "testNetworkConnection", // coin: Coin.particl, @@ -1568,7 +1568,7 @@ void main() { // // 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 { - // // nmc = NamecoinWallet( + // // nmc = ParticlWallet( // // walletId: testWalletId, // // walletName: testWalletName, // // networkType: BasicNetworkType.test, From b2b4fb970f575509b1413a044a427bea3d00f9d1 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 09:36:32 -0600 Subject: [PATCH 072/103] recoverFromMnemonic using non empty seed on mainnet succeeds --- test/services/coins/particl/particl_wallet_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 25a7b06b6..14e52e8b0 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -858,9 +858,9 @@ void main() { true); } - expect(secureStore.interactions, 14); - expect(secureStore.writes, 7); - expect(secureStore.reads, 7); + expect(secureStore.interactions, 10); // 14 + expect(secureStore.writes, 5); // 7 + expect(secureStore.reads, 5); // 7 expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); From ac28432009b779ec24d13449f944fa49859a5277 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 09:44:11 -0600 Subject: [PATCH 073/103] disable recoverFromMnemonic test --- .../coins/particl/particl_wallet_test.dart | 148 +++++++++--------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 14e52e8b0..3ef13afac 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -793,80 +793,80 @@ void main() { // 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 part?.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, 10); // 14 - expect(secureStore.writes, 5); // 7 - expect(secureStore.reads, 5); // 7 - expect(secureStore.deletes, 0); - - 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 part?.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, 10); // 14 + // expect(secureStore.writes, 5); // 7 + // expect(secureStore.reads, 5); // 7 + // expect(secureStore.deletes, 0); + // + // verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(cachedClient); + // verifyNoMoreInteractions(priceAPI); + // }); test("fullRescan succeeds", () async { when(client?.getServerFeatures()).thenAnswer((_) async => { From e3111cf9d23043aaf926a7b9ef23d6d78caac44b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 10:22:13 -0600 Subject: [PATCH 074/103] fullRescan succeeds by removing P2SH and P2WKPH derivation paths which we could re-enable when we need completeness --- .../coins/particl/particl_wallet_test.dart | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 3ef13afac..21e7226de 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -1011,64 +1011,64 @@ void main() { // fetch wallet data again final receivingAddressesP2PKH = await wallet.get('receivingAddressesP2PKH'); - final receivingAddressesP2SH = await wallet.get('receivingAddressesP2SH'); - final receivingAddressesP2WPKH = - await wallet.get('receivingAddressesP2WPKH'); + // 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 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 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 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"); + // 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(preReceivingAddressesP2SH, receivingAddressesP2SH); + // expect(preReceivingAddressesP2WPKH, receivingAddressesP2WPKH); expect(preChangeAddressesP2PKH, changeAddressesP2PKH); - expect(preChangeAddressesP2SH, changeAddressesP2SH); - expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); + // expect(preChangeAddressesP2SH, changeAddressesP2SH); + // expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); expect(preReceivingIndexP2PKH, receivingIndexP2PKH); - expect(preReceivingIndexP2SH, receivingIndexP2SH); - expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); + // expect(preReceivingIndexP2SH, receivingIndexP2SH); + // expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); expect(preChangeIndexP2PKH, changeIndexP2PKH); - expect(preChangeIndexP2SH, changeIndexP2SH); - expect(preChangeIndexP2WPKH, changeIndexP2WPKH); + // 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); + // 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(client?.getBatchHistory(args: { - "0": [ - "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" - ] - })).called(2); + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); + verify(client?.getBatchHistory(args: historyBatchArgs2)).called(2); // 1 + verify(client?.getBatchHistory(args: historyBatchArgs3)).called(2); // 1 + verify(client?.getBatchHistory(args: historyBatchArgs4)).called(2); // 1 + verify(client?.getBatchHistory(args: historyBatchArgs5)).called(2); // 1 + // verify(client?.getBatchHistory(args: { + // "0": [ + // "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" + // ] + // })).called(2); verify(client?.getBatchHistory(args: { "0": [ "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339" @@ -1087,11 +1087,11 @@ void main() { ] })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc" - ] - })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc" + // ] + // })).called(2); verify(client?.getBatchHistory(args: { "0": [ @@ -1101,9 +1101,9 @@ void main() { verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) .called(1); - expect(secureStore.writes, 25); - expect(secureStore.reads, 32); - expect(secureStore.deletes, 6); + expect(secureStore.writes, 19); // 25 + expect(secureStore.reads, 22); // 32 + expect(secureStore.deletes, 4); // 6 verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); From 74a25d5001fecb671504b3a774a8a2c1d884d90a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 10:41:54 -0600 Subject: [PATCH 075/103] recoverFromMnemonic using non empty seed on mainnet succeeds --- .../coins/particl/particl_wallet_test.dart | 148 +++++++++--------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 21e7226de..ccf6ae559 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -793,80 +793,80 @@ void main() { // 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 part?.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, 10); // 14 - // expect(secureStore.writes, 5); // 7 - // expect(secureStore.reads, 5); // 7 - // expect(secureStore.deletes, 0); - // - // 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 part?.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); // TODO remove this def above + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); // TODO remove this def above + 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, 10); // 14 + expect(secureStore.writes, 5); // 7 + expect(secureStore.reads, 5); // 7 + expect(secureStore.deletes, 0); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); test("fullRescan succeeds", () async { when(client?.getServerFeatures()).thenAnswer((_) async => { From 8640353be844b6a46482d1c981e1b2be12d7a725 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 10:53:35 -0600 Subject: [PATCH 076/103] fullRescan fails --- .../coins/particl/particl_wallet_test.dart | 128 ++++++++++-------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index ccf6ae559..c9d64cb96 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -858,9 +858,9 @@ void main() { true); } - expect(secureStore.interactions, 10); // 14 - expect(secureStore.writes, 5); // 7 - expect(secureStore.reads, 5); // 7 + expect(secureStore.interactions, 10); + expect(secureStore.writes, 5); + expect(secureStore.reads, 5); expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); @@ -1058,12 +1058,12 @@ void main() { // expect(preChangeDerivationsStringP2WPKH, changeDerivationsStringP2WPKH); 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(2); // 1 - verify(client?.getBatchHistory(args: historyBatchArgs3)).called(2); // 1 - verify(client?.getBatchHistory(args: historyBatchArgs4)).called(2); // 1 - verify(client?.getBatchHistory(args: historyBatchArgs5)).called(2); // 1 + // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); // TODO remove this def above + // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); // TODO remove this def above + 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(client?.getBatchHistory(args: { // "0": [ // "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" @@ -1101,9 +1101,9 @@ void main() { verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) .called(1); - expect(secureStore.writes, 19); // 25 - expect(secureStore.reads, 22); // 32 - expect(secureStore.deletes, 4); // 6 + expect(secureStore.writes, 19); + expect(secureStore.reads, 22); + expect(secureStore.deletes, 4); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -1216,6 +1216,16 @@ void main() { when(client?.getBatchHistory(args: historyBatchArgs0)) .thenThrow(Exception("fake exception")); + when(client?.getBatchHistory(args: historyBatchArgs1)) + .thenThrow(Exception("fake exception")); + when(client?.getBatchHistory(args: historyBatchArgs2)) + .thenThrow(Exception("fake exception")); + when(client?.getBatchHistory(args: historyBatchArgs3)) + .thenThrow(Exception("fake exception")); + when(client?.getBatchHistory(args: historyBatchArgs4)) + .thenThrow(Exception("fake exception")); + when(client?.getBatchHistory(args: historyBatchArgs5)) + .thenThrow(Exception("fake exception")); bool hasThrown = false; try { @@ -1229,97 +1239,97 @@ void main() { final receivingAddressesP2PKH = await wallet.get('receivingAddressesP2PKH'); final receivingAddressesP2SH = await wallet.get('receivingAddressesP2SH'); - final receivingAddressesP2WPKH = - await wallet.get('receivingAddressesP2WPKH'); + // 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 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 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 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"); + // 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(preReceivingAddressesP2WPKH, receivingAddressesP2WPKH); expect(preChangeAddressesP2PKH, changeAddressesP2PKH); - expect(preChangeAddressesP2SH, changeAddressesP2SH); - expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); + // expect(preChangeAddressesP2SH, changeAddressesP2SH); + // expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); expect(preReceivingIndexP2PKH, receivingIndexP2PKH); - expect(preReceivingIndexP2SH, receivingIndexP2SH); - expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); + // expect(preReceivingIndexP2SH, receivingIndexP2SH); + // expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); expect(preChangeIndexP2PKH, changeIndexP2PKH); - expect(preChangeIndexP2SH, changeIndexP2SH); - expect(preChangeIndexP2WPKH, changeIndexP2WPKH); + // 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); + // 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: 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(client?.getBatchHistory(args: { - "0": [ - "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" - ] - })).called(2); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" + // ] + // })).called(2); verify(client?.getBatchHistory(args: { "0": [ "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339" ] - })).called(2); + })).called(1); verify(client?.getBatchHistory(args: { "0": [ "b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3" ] - })).called(2); + })).called(1); verify(client?.getBatchHistory(args: { "0": [ "0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a" ] - })).called(2); - verify(client?.getBatchHistory(args: { - "0": [ - "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc" - ] })).called(1); + // verify(client?.getBatchHistory(args: { + // "0": [ + // "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc" + // ] + // })).called(1); verify(client?.getBatchHistory(args: { "0": [ "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" ] - })).called(2); + })).called(1); verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) .called(1); - expect(secureStore.writes, 19); - expect(secureStore.reads, 32); - expect(secureStore.deletes, 12); + expect(secureStore.writes, 13); + expect(secureStore.reads, 22); + expect(secureStore.deletes, 8); - verifyNoMoreInteractions(client); + // verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); }); From 363e0da8ffaf1335573e600bad70bc3202b12823 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 10:56:18 -0600 Subject: [PATCH 077/103] refresh wallet mutex locked --- test/services/coins/particl/particl_wallet_test.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index c9d64cb96..2b09537fa 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -1672,8 +1672,8 @@ void main() { await part?.refresh(); verify(client?.getServerFeatures()).called(1); - verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); - verify(client?.getBatchHistory(args: historyBatchArgs1)).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); @@ -1687,9 +1687,9 @@ void main() { true); } - expect(secureStore.interactions, 14); - expect(secureStore.writes, 7); - expect(secureStore.reads, 7); + expect(secureStore.interactions, 10); + expect(secureStore.writes, 5); + expect(secureStore.reads, 5); expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); From 8a5dd7ccc2ca0354a94f544c8d83f7ca5cc7354a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 10:58:25 -0600 Subject: [PATCH 078/103] refresh wallet normally --- test/services/coins/particl/particl_wallet_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 2b09537fa..4f650e3e2 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -1744,7 +1744,7 @@ void main() { await part?.refresh(); verify(client?.getServerFeatures()).called(1); - verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(4); + verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(3); verify(client?.estimateFee(blocks: anyNamed("blocks"))).called(3); verify(client?.getBlockHeadTip()).called(1); verify(priceAPI?.getPricesAnd24hChange(baseCurrency: "USD")).called(2); @@ -1755,9 +1755,9 @@ void main() { verify(client?.getBatchHistory(args: map)).called(1); } - expect(secureStore.interactions, 14); - expect(secureStore.writes, 7); - expect(secureStore.reads, 7); + expect(secureStore.interactions, 10); + expect(secureStore.writes, 5); + expect(secureStore.reads, 5); expect(secureStore.deletes, 0); verifyNoMoreInteractions(cachedClient); From 138796a767c220b1ffa372b3bc69c95b1e45d5b8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 11:40:19 -0600 Subject: [PATCH 079/103] diff minimization --- .gitignore | 1 - pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e4bc4a75a..a36824135 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,6 @@ 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/namecoin/namecoin_wallet_test_parameters.dart.txt # Legacy -test/services/coins/namecoin/namecoin_wallet_test_parameters.txt # Legacy test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart test/services/coins/particl/particl_wallet_test_parameters.dart /integration_test/private.dart diff --git a/pubspec.yaml b/pubspec.yaml index e7a9113fc..1cfb9d90d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: bitcoindart: git: url: https://github.com/cypherstack/bitcoindart.git - ref: 004d6f82dff7389b561e5078b4649adcd2d9c10f # TODO change to hash ref when merging in particl support + ref: 004d6f82dff7389b561e5078b4649adcd2d9c10f stack_wallet_backup: git: From 1c856a9b6608305667bbbc1f1b4799a1831f925f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 12:58:35 -0600 Subject: [PATCH 080/103] update particl node --- lib/utilities/default_nodes.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 21da92a8e..f4eb41b7c 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -147,15 +147,15 @@ abstract class DefaultNodes { ); static NodeModel get particl => NodeModel( - host: "164.92.93.20", - port: 50002, + host: "particl.stackwallet.com", + port: 58002, name: defaultName, id: _nodeId(Coin.particl), useSSL: true, enabled: true, coinName: Coin.particl.name, isFailover: true, - isDown: false); //TODO - UPDATE WITH CORRECT DETAILS + isDown: false); static NodeModel get bitcoinTestnet => NodeModel( host: "electrumx-testnet.cypherstack.com", From be5cfdc099e1976fbcb7e2075161562fcebe6e0e Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 13:10:11 -0600 Subject: [PATCH 081/103] fix "no internet available" price test --- test/price_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/price_test.dart b/test/price_test.dart index 89300122e..b8de5fec2 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -120,7 +120,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); tearDown(() async { From 92602276db9471e125461ec1c3aa410c6f01e3c6 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 14:24:01 -0600 Subject: [PATCH 082/103] fix more price tests fix more price tests fix price tests --- test/price_test.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/price_test.dart b/test/price_test.dart index b8de5fec2..a69d2de0a 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -26,7 +26,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -39,10 +39,10 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -53,7 +53,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -71,12 +71,12 @@ void main() { await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(cachedPrice.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); // verify only called once during filling of cache verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -100,7 +100,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); test("no internet available", () async { From 5f0e03dc1bf95d107f0843e33a5492d23a5fbe4f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 9 Dec 2022 14:55:04 -0600 Subject: [PATCH 083/103] add null check to epic wallet delete --- lib/services/coins/epiccash/epiccash_wallet.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index fcf728fb8..65fcd1643 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -276,7 +276,16 @@ Future deleteEpicWallet({ final wallet = await secureStore.read(key: '${walletId}_wallet'); - return compute(_deleteWalletWrapper, wallet!); + if (wallet == null) { + return "Tried to delete non existent epic wallet file with walletId=$walletId"; + } else { + try { + return compute(_deleteWalletWrapper, wallet); + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Error); + return "deleteEpicWallet($walletId) failed..."; + } + } } Future _initWalletWrapper( From df9e020235a4f203937f6f118792047ee292df29 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 15:22:22 -0600 Subject: [PATCH 084/103] fix values getPricesAnd24hChange fetch --- test/price_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/price_test.dart b/test/price_test.dart index a69d2de0a..55c297b27 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -39,7 +39,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); verify(client.get( Uri.parse( "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), @@ -71,7 +71,7 @@ void main() { await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(cachedPrice.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); // verify only called once during filling of cache verify(client.get( @@ -100,7 +100,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); test("no internet available", () async { From cc9a0efb38c5d765ae3906227b264e12cfe8c63b Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 9 Dec 2022 15:32:27 -0600 Subject: [PATCH 085/103] remove excess/unneeded logging --- .../helpers/restore_create_backup.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 900fdf15d..62a8bf9cb 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -1095,8 +1095,9 @@ abstract class SWB { ExchangeTransaction? exTx; try { exTx = ExchangeTransaction.fromJson(trades[i] as Map); - } catch (e, s) { - Logging.instance.log("$e\n$s", level: LogLevel.Warning); + } catch (e) { + // unneeded log + // Logging.instance.log("$e\n$s", level: LogLevel.Warning); } Trade trade; @@ -1117,8 +1118,9 @@ abstract class SWB { try { exTx = ExchangeTransaction.fromJson(trades.last as Map); - } catch (e, s) { - Logging.instance.log("$e\n$s", level: LogLevel.Warning); + } catch (e) { + // unneeded log + // Logging.instance.log("$e\n$s", level: LogLevel.Warning); } Trade trade; From 5ed5e6e8ef9a673934c5a081c72ffb5a21d0028f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 9 Dec 2022 15:41:56 -0600 Subject: [PATCH 086/103] weird locale error temp fix --- .../backup_and_restore/backup_and_restore_settings.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 69f33d523..de9a68418 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -9,7 +9,6 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/stack_back import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/backup_and_restore/enable_backup_dialog.dart'; import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; -import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; @@ -73,8 +72,10 @@ class _BackupRestoreSettings extends ConsumerState { } else { // if greater than a week return the actual date return DateFormat.yMMMMd( - ref.read(localeServiceChangeNotifierProvider).locale) - .format(time); + // en_CA locale breaks things? + // ref.read(localeServiceChangeNotifierProvider).locale, + "en_US", + ).format(time); } if (value == 1) { From 5fcf642a8bb35d6a6c7b1357a5f5f289e11e73ee Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 9 Dec 2022 15:42:35 -0600 Subject: [PATCH 087/103] desktop routing fix --- .../stack_backup_views/edit_auto_backup_view.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 7187c5311..0cc85d77c 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -18,7 +18,6 @@ import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -218,8 +217,10 @@ class _EditAutoBackupViewState extends ConsumerState { passwordController.text = ""; passwordRepeatController.text = ""; - Navigator.of(context) - .popUntil(ModalRoute.withName(AutoBackupView.routeName)); + if (!Util.isDesktop) { + Navigator.of(context) + .popUntil(ModalRoute.withName(AutoBackupView.routeName)); + } } } else { await showDialog( From 84108a3b2786f4160b21c258e416c12360f94228 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 9 Dec 2022 15:50:17 -0600 Subject: [PATCH 088/103] use system navigator to exit --- lib/pages_desktop_specific/desktop_menu.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_menu.dart b/lib/pages_desktop_specific/desktop_menu.dart index 4ea9dff7c..96b4e6638 100644 --- a/lib/pages_desktop_specific/desktop_menu.dart +++ b/lib/pages_desktop_specific/desktop_menu.dart @@ -1,6 +1,5 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_menu_item.dart'; @@ -219,7 +218,8 @@ class _DesktopMenuState extends ConsumerState { value: 7, onChanged: (_) { // todo: save stuff/ notify before exit? - exit(0); + // exit(0); + SystemNavigator.pop(); }, controller: controllers[7], ), From 419e5a3372b683d1ab8ac73eb6b99e5fa029e1f2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 9 Dec 2022 17:07:44 -0600 Subject: [PATCH 089/103] expect different output in getPricesAnd24hChange --- test/price_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/price_test.dart b/test/price_test.dart index 55c297b27..a52e580af 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -120,7 +120,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); tearDown(() async { From 22c1687fe67e7b1884f389973bc60e1527b045ae Mon Sep 17 00:00:00 2001 From: julian Date: Sat, 10 Dec 2022 10:51:16 -0600 Subject: [PATCH 090/103] fix price test --- test/price_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/price_test.dart b/test/price_test.dart index 55c297b27..6b98b67d1 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -87,7 +87,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -100,7 +100,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); test("no internet available", () async { @@ -108,7 +108,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenThrow(const SocketException( From be64df3315525c566ab7e3f1283f57495655a479 Mon Sep 17 00:00:00 2001 From: likho Date: Sun, 11 Dec 2022 13:48:24 +0200 Subject: [PATCH 091/103] WIP: Update tests for Particl --- .../particl/particl_history_sample_data.dart | 42 +- .../coins/particl/particl_wallet_test.dart | 1129 ++++++++--------- 2 files changed, 514 insertions(+), 657 deletions(-) diff --git a/test/services/coins/particl/particl_history_sample_data.dart b/test/services/coins/particl/particl_history_sample_data.dart index 620453178..048dc0b1f 100644 --- a/test/services/coins/particl/particl_history_sample_data.dart +++ b/test/services/coins/particl/particl_history_sample_data.dart @@ -1,38 +1,4 @@ final Map> historyBatchArgs0 = { - "k_0_0": ["29e9e6410954dea9e527a0d2cac5de4dea5fb600b719badff90d6d43518d3ed8"], - "k_0_1": ["9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc"], - "k_0_2": ["016b150fa7948b0841a2e755b3d40a96a5cbc1d2ac96a105d3ff201137f60d31"], - "k_0_3": ["1b668ee6f82a3f10ea0a515760f1165f53e99e979bd3c557cf6e09d793b9296d"], - "k_0_4": ["351da1f29872e3703dd92625f7face416226f54473bd853c80c1dcc2d8849b63"], - "k_0_5": ["2cd81781064f4679d09bf794d322d44903d8f66c00c8b80753eecaa40a993897"], - "k_0_6": ["ed4d0c5b4c13213e0c91805ef212bf25b81ed0ece46a2fa0d647e66cebebe53d"], - "k_0_7": ["ea558eee7a7b00b4a6207eb5ee783afa422410ababd02da879b687327caf9707"], - "k_0_8": ["90a7df71ab0a57abea44d955f78bf665785a2f104fce6348832fd79dc218d87a"], - "k_0_9": ["4b6d05c5ac9651a1e6452955cde697bbdca941b0c21021e30d0bc772c999a79d"], - "k_0_10": [ - "af8481316f782f8d2696ef6e70a4c23a17658500c2ee1ab5b7ef0eafb9f18112" - ], - "k_0_11": ["608f1621850396138cf0ece48b20c1ecbd138d74d200c81b3640564083d117da"] -}; - -final Map> historyBatchArgs1 = { - "k_0_0": ["a41a236959ee41de770a0c2d360d62d75e4ba010294415cfb9f44eff0f731a70"], - "k_0_1": ["8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247"], - "k_0_2": ["540939221b59a81810398a1af45c22d1e9599e718f24870a44972fbfe55c0a39"], - "k_0_3": ["0323d2dcc60e8d9b6234be43c942f493f1039d45f47b641cd78d0860a5bc61cb"], - "k_0_4": ["c1c61eaa4e0ffa3c2bed0bb32b54281f743113c5d1fe59a4e595559ed1951a8b"], - "k_0_5": ["08dd301d83e42ded2d4ea1990389b281eaec328039b3469d3f2f25b52571be81"], - "k_0_6": ["7de683cf6ac67680166d82505b15974e4ff13caf544001479a2918157b171ccd"], - "k_0_7": ["b15515c8c6dd3eb6835086031f22fc644b490d291cd09772d414c67b5822e95b"], - "k_0_8": ["ff3ff4bc2f223169fa3317c19d399feeb84bae60f32171b1960c94fd61e72041"], - "k_0_9": ["15a21ca5cf24740944b894a9f0482abf1433ab59f156ff52d241ecc234a0dff4"], - "k_0_10": [ - "2037faeb3a55b1bb4545aa220578f9322ab8af8ac7af100d3c1261d25d1b1135" - ], - "k_0_11": ["4e2051a980cb463b523df8a765e45ea18a0d08a670c38dd14f7c4457d7c86bff"] -}; - -final Map> historyBatchArgs2 = { "k_0_0": ["a48f8ee5dc6ff58b29eddeac1afd808b2edff10d736bdede3a2e6a95e588911c"], "k_0_1": ["b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3"], "k_0_2": ["aabdda195909a16141e8a3ab548fc5e8e8ba483762f94e1571cf93572bc6767e"], @@ -49,7 +15,7 @@ final Map> historyBatchArgs2 = { "k_0_11": ["f66b687065339e2d4d567f9ea473947b8aab74c066bf00cdfdb5f918bbd159dc"] }; -final Map> historyBatchArgs3 = { +final Map> historyBatchArgs1 = { "k_0_0": ["0664a4e19dd852c7d6fb53824198070e911dae9049aa9a6a940413cb868bbb27"], "k_0_1": ["c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f"], "k_0_2": ["aa34a5cd34908ed90f41ce31058552bee864b8894eec3b5b3f2583eb56eca759"], @@ -66,7 +32,7 @@ final Map> historyBatchArgs3 = { "k_0_11": ["f8f09b8fe23da8435409c3e688002dcaa87c2b9f3707e17bc668db7392039dab"] }; -final Map> historyBatchArgs4 = { +final Map> historyBatchArgs2 = { "k_0_0": ["4cff1590918be5d24d130f10627aaacc6d1e3f03872643c3afc742e6c77e3e72"], "k_0_1": ["3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339"], "k_0_2": ["68668ef2d53d4cb5bda66ce3adae25dbe7a8466eb3eca64ed816a59cf8362288"], @@ -83,7 +49,7 @@ final Map> historyBatchArgs4 = { "k_0_11": ["f6a7b80c32f2568bebe37d6615ebfa602ec04207cd9edf304ff7f835b03c27d2"] }; -final Map> historyBatchArgs5 = { +final Map> historyBatchArgs3 = { "k_0_0": ["f2547dcbe38adc0fee943dc0b0a543f96b90af587850c9df172c69134a49f4c9"], "k_0_1": ["0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a"], "k_0_2": ["099bdff41fbbfc3d90ea5a8510d5588e71a27509592447025ee6dee4278e13ff"], @@ -179,10 +145,8 @@ final Map>> emptyHistoryBatchResponse = { }; final List activeScriptHashes = [ - "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247", "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339", "b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3", "0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a", - "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc", "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" ]; diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 4f650e3e2..3bb2a1e5f 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -15,6 +15,8 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:tuple/tuple.dart'; import 'particl_history_sample_data.dart'; +import 'particl_transaction_data_samples.dart'; +import 'particl_utxo_sample_data.dart'; import 'particl_wallet_test.mocks.dart'; import 'particl_wallet_test_parameters.dart'; @@ -87,7 +89,7 @@ void main() { // } // expect(didThrow, true); // }); - + //TODO Testnet not setup // test("basic getBip32Node", () { // final node = // getBip32Node(0, 0, TEST_MNEMONIC, testnet, DerivePathType.bip84); @@ -196,17 +198,17 @@ void main() { 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("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( @@ -221,67 +223,67 @@ void main() { }); }); - // group("testNetworkConnection", () { - // MockElectrumX? client; - // MockCachedElectrumX? cachedClient; - // MockPriceAPI? priceAPI; - // late FakeSecureStorage secureStore; - // MockTransactionNotificationTracker? tracker; + group("testNetworkConnection", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; - // ParticlWallet? part; + ParticlWallet? part; - // setUp(() { - // client = MockElectrumX(); - // cachedClient = MockCachedElectrumX(); - // priceAPI = MockPriceAPI(); - // secureStore = FakeSecureStorage(); - // tracker = MockTransactionNotificationTracker(); + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); - // nmc = ParticlWallet( - // walletId: "testNetworkConnection", - // walletName: "testNetworkConnection", - // coin: Coin.particl, - // client: client!, - // cachedClient: cachedClient!, - // tracker: tracker!, - // priceAPI: priceAPI, - // secureStore: secureStore, - // ); - // }); + part = ParticlWallet( + walletId: "testNetworkConnection", + walletName: "testNetworkConnection", + coin: Coin.particl, + 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 nmc?.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 server error", () async { + when(client?.ping()).thenAnswer((_) async => false); + final bool? result = await part?.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 nmc?.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 part?.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 nmc?.testNetworkConnection(); - // expect(result, true); - // 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 part?.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 = "ParticltestWalletID"; @@ -545,7 +547,7 @@ void main() { secureStore: secureStore, ); }); - + //TODO - THis function definition has changed, possibly remove // test("initializeWallet no network", () async { // when(client?.ping()).thenAnswer((_) async => false); // expect(await part?.initializeNew(), false); @@ -567,231 +569,217 @@ void main() { // 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 nmc?.initializeNew(); - // final wallets = await Hive.openBox(testWalletId); - // - // expectLater(() => nmc?.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(() => nmc?.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( - // "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 nmc?.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 nmc?.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("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 Hive.openBox(testWalletId); - // 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 nmc?.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); - // }); + await expectLater( + () => part?.initializeExisting(), throwsA(isA())) + .then((_) { + 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: 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 nmc?.recoverFromMnemonic( - // mnemonic: TEST_MNEMONIC, - // maxUnusedAddressGap: 2, - // maxNumberOfIndexesToCheck: 1000, - // height: 4000); - // - // expect(await nmc?.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("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"); + + await Hive.openBox(testWalletId); + await expectLater( + () => part?.initializeExisting(), throwsA(isA())) + .then((_) { + expect(secureStore.interactions, 1); + 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 part?.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 part?.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); + + // await DB.instance.init(); + final wallet = await Hive.openBox(testWalletId); + bool hasThrown = false; + try { + await part?.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); + + expect(secureStore.interactions, 14); + expect(secureStore.writes, 5); + expect(secureStore.reads, 9); + 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); + + await Hive.openBox(testWalletId); + + await part?.recoverFromMnemonic( + mnemonic: TEST_MNEMONIC, + maxUnusedAddressGap: 2, + maxNumberOfIndexesToCheck: 1000, + height: 4000); + + expect(await part?.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); + + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(priceAPI); + }); test("recoverFromMnemonic using non empty seed on mainnet succeeds", () async { @@ -813,10 +801,6 @@ void main() { .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 = []; @@ -844,12 +828,10 @@ void main() { expect(hasThrown, false); verify(client?.getServerFeatures()).called(1); - // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); // TODO remove this def above - // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); // TODO remove this def above + 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); @@ -887,10 +869,6 @@ void main() { .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.particl)) .thenAnswer((realInvocation) async {}); @@ -940,29 +918,20 @@ void main() { // 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( @@ -971,30 +940,20 @@ void main() { // 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( @@ -1011,64 +970,44 @@ void main() { // fetch wallet data again final receivingAddressesP2PKH = await wallet.get('receivingAddressesP2PKH'); - // final receivingAddressesP2SH = await wallet.get('receivingAddressesP2SH'); - // final receivingAddressesP2WPKH = - // await wallet.get('receivingAddressesP2WPKH'); + 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 changeAddressesP2WPKH = await wallet.get('changeAddressesP2WPKH'); final receivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); - // final receivingIndexP2SH = await wallet.get('receivingIndexP2SH'); - // final receivingIndexP2WPKH = await wallet.get('receivingIndexP2WPKH'); + 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 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"); + 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(preReceivingAddressesP2WPKH, receivingAddressesP2WPKH); expect(preChangeAddressesP2PKH, changeAddressesP2PKH); - // expect(preChangeAddressesP2SH, changeAddressesP2SH); - // expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); + expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); expect(preReceivingIndexP2PKH, receivingIndexP2PKH); - // expect(preReceivingIndexP2SH, receivingIndexP2SH); - // expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); + expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); expect(preChangeIndexP2PKH, changeIndexP2PKH); - // expect(preChangeIndexP2SH, changeIndexP2SH); - // expect(preChangeIndexP2WPKH, changeIndexP2WPKH); + expect(preChangeIndexP2WPKH, changeIndexP2WPKH); expect(preUtxoData, utxoData); expect(preReceiveDerivationsStringP2PKH, receiveDerivationsStringP2PKH); expect(preChangeDerivationsStringP2PKH, changeDerivationsStringP2PKH); - // expect(preReceiveDerivationsStringP2SH, receiveDerivationsStringP2SH); - // expect(preChangeDerivationsStringP2SH, changeDerivationsStringP2SH); - // expect(preReceiveDerivationsStringP2WPKH, receiveDerivationsStringP2WPKH); - // expect(preChangeDerivationsStringP2WPKH, changeDerivationsStringP2WPKH); + expect(preReceiveDerivationsStringP2WPKH, receiveDerivationsStringP2WPKH); + expect(preChangeDerivationsStringP2WPKH, changeDerivationsStringP2WPKH); verify(client?.getServerFeatures()).called(1); - // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); // TODO remove this def above - // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); // TODO remove this def above + 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(client?.getBatchHistory(args: { - // "0": [ - // "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" - // ] - // })).called(2); + verify(client?.getBatchHistory(args: { "0": [ "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339" @@ -1087,12 +1026,6 @@ void main() { ] })).called(2); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc" - // ] - // })).called(2); - verify(client?.getBatchHistory(args: { "0": [ "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" @@ -1101,7 +1034,7 @@ void main() { verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) .called(1); - expect(secureStore.writes, 19); + expect(secureStore.writes, 17); expect(secureStore.reads, 22); expect(secureStore.deletes, 4); @@ -1130,10 +1063,6 @@ void main() { .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(client?.getBatchHistory(args: { "0": [ @@ -1222,10 +1151,6 @@ void main() { .thenThrow(Exception("fake exception")); when(client?.getBatchHistory(args: historyBatchArgs3)) .thenThrow(Exception("fake exception")); - when(client?.getBatchHistory(args: historyBatchArgs4)) - .thenThrow(Exception("fake exception")); - when(client?.getBatchHistory(args: historyBatchArgs5)) - .thenThrow(Exception("fake exception")); bool hasThrown = false; try { @@ -1239,64 +1164,56 @@ void main() { final receivingAddressesP2PKH = await wallet.get('receivingAddressesP2PKH'); final receivingAddressesP2SH = await wallet.get('receivingAddressesP2SH'); - // final receivingAddressesP2WPKH = - // await wallet.get('receivingAddressesP2WPKH'); + 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 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 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 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"); + 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(preReceivingAddressesP2WPKH, receivingAddressesP2WPKH); expect(preChangeAddressesP2PKH, changeAddressesP2PKH); - // expect(preChangeAddressesP2SH, changeAddressesP2SH); - // expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); + expect(preChangeAddressesP2SH, changeAddressesP2SH); + expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); expect(preReceivingIndexP2PKH, receivingIndexP2PKH); - // expect(preReceivingIndexP2SH, receivingIndexP2SH); - // expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); + expect(preReceivingIndexP2SH, receivingIndexP2SH); + expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); expect(preChangeIndexP2PKH, changeIndexP2PKH); - // expect(preChangeIndexP2SH, changeIndexP2SH); - // expect(preChangeIndexP2WPKH, changeIndexP2WPKH); + 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); + 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: 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(client?.getBatchHistory(args: { - // "0": [ - // "8ba03c2c46ed4980fa1e4c84cbceeb2d5e1371a7ccbaf5f3d69c5114161a2247" - // ] - // })).called(2); verify(client?.getBatchHistory(args: { "0": [ "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339" @@ -1312,11 +1229,6 @@ void main() { "0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a" ] })).called(1); - // verify(client?.getBatchHistory(args: { - // "0": [ - // "9b56ab30c7bef0e1eaa10a632c8e2dcdd11b2158d7a917c03d62936afd0015fc" - // ] - // })).called(1); verify(client?.getBatchHistory(args: { "0": [ "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" @@ -1326,206 +1238,194 @@ void main() { .called(1); expect(secureStore.writes, 13); - expect(secureStore.reads, 22); + expect(secureStore.reads, 26); expect(secureStore.deletes, 8); - // verifyNoMoreInteractions(client); + 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: - // "dffa9543852197f9fb90f8adafaab8a0b9b4925e9ada8c6bdcaf00bf2e9f60d7", - // coin: Coin.particl)) - // .thenAnswer((_) async => tx2Raw); - // when(cachedClient?.getTransaction( - // txHash: - // "71b56532e9e7321bd8c30d0f8b14530743049d2f3edd5623065c46eee1dda04d", - // coin: Coin.particl)) - // .thenAnswer((_) async => tx3Raw); - // when(cachedClient?.getTransaction( - // txHash: - // "c7e700f7e23a85bbdd9de86d502322a933607ee7ea7e16adaf02e477cdd849b9", - // coin: Coin.particl, - // )).thenAnswer((_) async => tx4Raw); - // - // // recover to fill data - // await nmc?.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")); - // - // nmc?.outputsList = utxoList; - // - // bool didThrow = false; - // try { - // await nmc?.prepareSend( - // address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v", - // 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.particl, - // // callOutSideMainIsolate: false)) - // // .called(1); - // // verify(cachedClient?.getTransaction( - // // txHash: - // // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", - // // coin: Coin.particl, - // // callOutSideMainIsolate: false)) - // // .called(1); - // // verify(cachedClient?.getTransaction( - // // txHash: - // // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", - // // coin: Coin.particl, - // // 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("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); - // test("confirmSend no hex", () async { - // bool didThrow = false; - // try { - // await nmc?.confirmSend(txData: {"some": "strange map"}); - // } catch (_) { - // didThrow = true; - // } - // - // expect(didThrow, true); - // - // expect(secureStore.interactions, 0); - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + List dynamicArgValues = []; - // test("confirmSend hex is not string", () async { - // bool didThrow = false; - // try { - // await nmc?.confirmSend(txData: {"hex": true}); - // } catch (_) { - // didThrow = true; - // } - // - // expect(didThrow, true); - // - // expect(secureStore.interactions, 0); - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + when(client?.getBatchHistory(args: anyNamed("args"))) + .thenAnswer((realInvocation) async { + if (realInvocation.namedArguments.values.first.length == 1) { + dynamicArgValues.add(realInvocation.namedArguments.values.first); + } - // test("confirmSend hex is string but missing other data", () async { - // bool didThrow = false; - // try { - // await nmc?.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); - // }); + return historyBatchResponse; + }); + + await Hive.openBox(testWalletId); + + when(cachedClient?.getTransaction( + txHash: + "85130125ec9e37a48670fb5eb0a2780b94ea958cd700a1237ff75775d8a0edb0", + coin: Coin.particl)) + .thenAnswer((_) async => tx2Raw); + when(cachedClient?.getTransaction( + txHash: + "bb25567e1ffb2fd6ec9aa3925a7a8dd3055a29521f7811b2b2bc01ce7d8a216e", + coin: Coin.particl)) + .thenAnswer((_) async => tx3Raw); + when(cachedClient?.getTransaction( + txHash: + "bb25567e1ffb2fd6ec9aa3925a7a8dd3055a29521f7811b2b2bc01ce7d8a216e", + coin: Coin.particl, + )).thenAnswer((_) async => tx4Raw); + + // recover to fill data + await part?.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 rcv84 = await secureStore.read( + key: testWalletId + "_receiveDerivationsP2WPKH"); + await secureStore.write( + key: testWalletId + "_receiveDerivationsP2WPKH", + value: rcv84?.replaceFirst( + "pw1qvr6ehcm44vvqe96mxy9zw9aa5sa5yezvr2r94s", + "pw1q66xtkhqzcue808nlg8tp48uq7fshmaddljtkpy")); + + part?.outputsList = utxoList; + + bool didThrow = false; + try { + await part?.prepareSend( + address: "pw1q66xtkhqzcue808nlg8tp48uq7fshmaddljtkpy", + 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.particl, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "ed32c967a0e86d51669ac21c2bb9bc9c50f0f55fbacdd8db21d0a986fba93bd7", + // coin: Coin.particl, + // callOutSideMainIsolate: false)) + // .called(1); + // verify(cachedClient?.getTransaction( + // txHash: + // "3f0032f89ac44b281b50314cff3874c969c922839dddab77ced54e86a21c3fd4", + // coin: Coin.particl, + // 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); + + 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("confirmSend no hex", () async { + bool didThrow = false; + try { + await part?.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 part?.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 part?.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; @@ -1642,11 +1542,6 @@ void main() { .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"))) @@ -1672,12 +1567,10 @@ void main() { await part?.refresh(); verify(client?.getServerFeatures()).called(1); - // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); - // verify(client?.getBatchHistory(args: historyBatchArgs1)).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); From a0e38aab3171edb6e41aa1c82bb2ec0dda59ea43 Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 12 Dec 2022 10:19:36 +0200 Subject: [PATCH 092/103] Fix and test the rest of the tests --- .../coins/particl/particl_wallet_test.dart | 258 +++++++++--------- 1 file changed, 129 insertions(+), 129 deletions(-) diff --git a/test/services/coins/particl/particl_wallet_test.dart b/test/services/coins/particl/particl_wallet_test.dart index 3bb2a1e5f..bbe34a5ec 100644 --- a/test/services/coins/particl/particl_wallet_test.dart +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -376,91 +376,91 @@ void main() { verifyNoMoreInteractions(priceAPI); }); - // test("estimateTxFee", () async { - // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); - // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); - // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); - // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); - // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); - // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); - // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); - // expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 12345), 4628); - // expect(secureStore.interactions, 0); - // verifyNoMoreInteractions(client); - // verifyNoMoreInteractions(cachedClient); - // verifyNoMoreInteractions(priceAPI); - // }); + test("estimateTxFee", () async { + expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1), 356); + expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 900), 356); + expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 999), 356); + expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1000), 356); + expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1001), 712); + expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); + expect(part?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); + expect(part?.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_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)) - // .thenAnswer((realInvocation) async => Decimal.ten); - // - // final fees = await part?.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 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?.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); - // 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 part?.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); - // }); + final fees = await part?.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 part?.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); @@ -481,7 +481,7 @@ void main() { // when(client?.estimateFee(blocks: 1)) // .thenAnswer((realInvocation) async => Decimal.ten); // - // final maxFee = await nmc?.maxFee; + // final maxFee = await part?.maxFee; // expect(maxFee, 1000000000); // // verify(client?.estimateFee(blocks: 1)).called(1); @@ -1427,52 +1427,52 @@ void main() { verifyNoMoreInteractions(priceAPI); }); - // test("confirmSend fails due to vSize being greater than fee", () async { - // bool didThrow = false; - // try { - // await nmc - // ?.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 due to vSize being greater than fee", () async { + bool didThrow = false; + try { + await part + ?.confirmSend(txData: {"hex": "a string", "fee": 1, "vSize": 10}); + } catch (_) { + didThrow = true; + } - // 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 nmc - // ?.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); - // }); + 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 part + ?.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 From ea1c7fd204a1c8068774b058c4dc5c9d8d691fc1 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 12 Dec 2022 15:00:39 -0600 Subject: [PATCH 093/103] fix price test --- test/price_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/price_test.dart b/test/price_test.dart index de21ff8e1..6b98b67d1 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -120,7 +120,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); tearDown(() async { From 236e04f84945db1a0b496024e26afa678af33df0 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 12 Dec 2022 15:59:06 -0600 Subject: [PATCH 094/103] epic node changes and fixes --- .../add_edit_node_view.dart | 75 +++++++++++-------- .../manage_nodes_views/node_details_view.dart | 13 ++-- .../coins/epiccash/epiccash_wallet.dart | 27 +++++-- lib/utilities/test_epic_box_connection.dart | 40 +++++++++- lib/widgets/node_card.dart | 11 ++- lib/widgets/node_options_sheet.dart | 11 ++- 6 files changed, 122 insertions(+), 55 deletions(-) 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 32fa3974a..d34616567 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 @@ -70,20 +70,13 @@ class _AddEditNodeViewState extends ConsumerState { switch (coin) { case Coin.epicCash: try { - final uri = Uri.parse(formData.host!); - if (uri.scheme.startsWith("http")) { - final String path = uri.path.isEmpty ? "/v1/version" : uri.path; + final data = await testEpicNodeConnection(formData); - String uriString = - "${uri.scheme}://${uri.host}:${formData.port ?? 0}$path"; - - if (uri.host == "https") { - ref.read(nodeFormDataProvider).useSSL = true; - } else { - ref.read(nodeFormDataProvider).useSSL = false; - } - - testPassed = await testEpicBoxNodeConnection(Uri.parse(uriString)); + if (data != null) { + testPassed = true; + ref.read(nodeFormDataProvider).host = data.host; + ref.read(nodeFormDataProvider).port = data.port; + ref.read(nodeFormDataProvider).useSSL = data.useSSL; } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); @@ -315,7 +308,7 @@ class _AddEditNodeViewState extends ConsumerState { // strip unused path String address = formData.host!; - if (coin == Coin.monero || coin == Coin.wownero || coin == Coin.epicCash) { + if (coin == Coin.monero || coin == Coin.wownero) { if (address.startsWith("http")) { final uri = Uri.parse(address); address = "${uri.scheme}://${uri.host}"; @@ -673,6 +666,7 @@ class _NodeFormState extends ConsumerState { bool _useSSL = false; bool _isFailover = false; int? port; + late bool enableSSLCheckbox; late final bool enableAuthFields; @@ -692,9 +686,9 @@ class _NodeFormState extends ConsumerState { case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: + case Coin.epicCash: return false; - case Coin.epicCash: case Coin.monero: case Coin.wownero: return true; @@ -768,11 +762,19 @@ class _NodeFormState extends ConsumerState { _usernameController.text = node.loginName ?? ""; _useSSL = node.useSSL; _isFailover = node.isFailover; + if (widget.coin == Coin.epicCash) { + enableSSLCheckbox = !node.host.startsWith("http"); + } + print("enableSSLCheckbox: $enableSSLCheckbox"); WidgetsBinding.instance.addPostFrameCallback((_) { // update provider state object so test connection works without having to modify a field in the ui first _updateState(); }); + } else { + enableSSLCheckbox = true; + // default to port 3413 + // _portController.text = "3413"; } super.initState(); @@ -858,9 +860,7 @@ class _NodeFormState extends ConsumerState { focusNode: _hostFocusNode, style: STextStyles.field(context), decoration: standardInputDecoration( - (widget.coin != Coin.monero && - widget.coin != Coin.wownero && - widget.coin != Coin.epicCash) + (widget.coin != Coin.monero && widget.coin != Coin.wownero) ? "IP address" : "Url", _hostFocusNode, @@ -886,6 +886,17 @@ class _NodeFormState extends ConsumerState { : null, ), onChanged: (newValue) { + if (widget.coin == Coin.epicCash) { + if (newValue.startsWith("https://")) { + _useSSL = true; + enableSSLCheckbox = false; + } else if (newValue.startsWith("http://")) { + _useSSL = false; + enableSSLCheckbox = false; + } else { + enableSSLCheckbox = true; + } + } _updateState(); setState(() {}); }, @@ -1040,20 +1051,18 @@ class _NodeFormState extends ConsumerState { const SizedBox( height: 8, ), - if (widget.coin != Coin.monero && - widget.coin != Coin.wownero && - widget.coin != Coin.epicCash) + if (widget.coin != Coin.monero && widget.coin != Coin.wownero) Row( children: [ GestureDetector( - onTap: widget.readOnly - ? null - : () { + onTap: !widget.readOnly && enableSSLCheckbox + ? () { setState(() { _useSSL = !_useSSL; }); _updateState(); - }, + } + : null, child: Container( color: Colors.transparent, child: Row( @@ -1062,22 +1071,22 @@ class _NodeFormState extends ConsumerState { width: 20, height: 20, child: Checkbox( - fillColor: widget.readOnly - ? MaterialStateProperty.all(Theme.of(context) + fillColor: !widget.readOnly && enableSSLCheckbox + ? null + : MaterialStateProperty.all(Theme.of(context) .extension()! - .checkboxBGDisabled) - : null, + .checkboxBGDisabled), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: _useSSL, - onChanged: widget.readOnly - ? null - : (newValue) { + onChanged: !widget.readOnly && enableSSLCheckbox + ? (newValue) { setState(() { _useSSL = newValue!; }); _updateState(); - }, + } + : null, ), ), const SizedBox( diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 24af0e78e..ad8ad7301 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -70,14 +70,15 @@ class _NodeDetailsViewState extends ConsumerState { switch (coin) { case Coin.epicCash: try { - final uri = Uri.parse(node!.host); - if (uri.scheme.startsWith("http")) { - final String path = uri.path.isEmpty ? "/v1/version" : uri.path; - String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; + testPassed = await testEpicNodeConnection( + NodeFormData() + ..host = node!.host + ..useSSL = node.useSSL + ..port = node.port, + ) != + null; - testPassed = await testEpicBoxNodeConnection(Uri.parse(uriString)); - } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); testPassed = false; diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 65fcd1643..af7541972 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/models/node_model.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/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; @@ -552,6 +553,9 @@ class EpicCashWallet extends CoinServiceAPI { DefaultNodes.getNodeFor(coin); // TODO notify ui/ fire event for node changed? + String stringConfig = await getConfig(); + await _secureStore.write(key: '${_walletId}_config', value: stringConfig); + if (shouldRefresh) { unawaited(refresh()); } @@ -1262,8 +1266,12 @@ class EpicCashWallet extends CoinServiceAPI { } final NodeModel node = _epicNode!; final String nodeAddress = node.host; - int port = node.port; - final String nodeApiAddress = "$nodeAddress:$port"; + final int port = node.port; + + final uri = Uri.parse(nodeAddress).replace(port: port); + + final String nodeApiAddress = uri.toString(); + final walletDir = await currentWalletDirPath(); final Map config = {}; @@ -1272,7 +1280,8 @@ class EpicCashWallet extends CoinServiceAPI { config["chain"] = "mainnet"; config["account"] = "default"; config["api_listen_port"] = port; - config["api_listen_interface"] = nodeAddress; + config["api_listen_interface"] = + nodeApiAddress.replaceFirst(uri.scheme, ""); String stringConfig = json.encode(config); return stringConfig; } @@ -2022,11 +2031,13 @@ class EpicCashWallet extends CoinServiceAPI { try { // force unwrap optional as we want connection test to fail if wallet // wasn't initialized or epicbox node was set to null - final String uriString = - "${_epicNode!.host}:${_epicNode!.port}/v1/version"; - - final Uri uri = Uri.parse(uriString); - return await testEpicBoxNodeConnection(uri); + return await testEpicNodeConnection( + NodeFormData() + ..host = _epicNode!.host + ..useSSL = _epicNode!.useSSL + ..port = _epicNode!.port, + ) != + null; } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); return false; diff --git a/lib/utilities/test_epic_box_connection.dart b/lib/utilities/test_epic_box_connection.dart index f4212b024..4b728fb59 100644 --- a/lib/utilities/test_epic_box_connection.dart +++ b/lib/utilities/test_epic_box_connection.dart @@ -1,15 +1,16 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; import 'package:stackwallet/utilities/logger.dart'; -Future testEpicBoxNodeConnection(Uri uri) async { +Future _testEpicBoxNodeConnection(Uri uri) async { try { final client = http.Client(); final response = await client.get( uri, headers: {'Content-Type': 'application/json'}, - ).timeout(const Duration(milliseconds: 1200), + ).timeout(const Duration(milliseconds: 2000), onTimeout: () async => http.Response('Error', 408)); final json = jsonDecode(response.body); @@ -24,3 +25,38 @@ Future testEpicBoxNodeConnection(Uri uri) async { return false; } } + +// returns node data with properly formatted host/url if successful, otherwise null +Future testEpicNodeConnection(NodeFormData data) async { + if (data.host == null || data.port == null || data.useSSL == null) { + return null; + } + const String path_postfix = "/v1/version"; + + if (data.host!.startsWith("https://")) { + data.useSSL = true; + } else if (data.host!.startsWith("http://")) { + data.useSSL = false; + } else { + if (data.useSSL!) { + data.host = "https://${data.host!}"; + } else { + data.host = "http://${data.host!}"; + } + } + + Uri uri = Uri.parse(data.host! + path_postfix); + + uri = uri.replace(port: data.port); + + try { + if (await _testEpicBoxNodeConnection(uri)) { + return data; + } else { + return null; + } + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + return null; + } +} diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index fb8260b24..e366cd713 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -6,6 +6,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -93,9 +94,13 @@ class _NodeCardState extends ConsumerState { switch (widget.coin) { case Coin.epicCash: try { - final String uriString = "${node.host}:${node.port}/v1/version"; - - testPassed = await testEpicBoxNodeConnection(Uri.parse(uriString)); + testPassed = await testEpicNodeConnection( + NodeFormData() + ..host = node.host + ..useSSL = node.useSSL + ..port = node.port, + ) != + null; } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); } diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 1ef9b07fe..80ce2f5be 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -6,6 +6,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -76,9 +77,13 @@ class NodeOptionsSheet extends ConsumerWidget { switch (coin) { case Coin.epicCash: try { - final String uriString = "${node.host}:${node.port}/v1/version"; - - testPassed = await testEpicBoxNodeConnection(Uri.parse(uriString)); + testPassed = await testEpicNodeConnection( + NodeFormData() + ..host = node.host + ..useSSL = node.useSSL + ..port = node.port, + ) != + null; } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); } From 761a7a30beb52c2c7be2b887a429974b362d076a Mon Sep 17 00:00:00 2001 From: julian-CStack <97684800+julian-CStack@users.noreply.github.com> Date: Mon, 12 Dec 2022 16:10:19 -0600 Subject: [PATCH 095/103] Update test.yaml add particl data to run --- .github/workflows/test.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 83de43779..4638e84dd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -83,6 +83,12 @@ jobs: $secretFileNamecoinHash = Get-FileHash $secretFileNamecoin; Write-Output "Secret file $secretFileNamecoin has hash $($secretFileNamecoinHash.Hash)"; + $secretFileParticl = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/particl/particl_wallet_test_parameters.dart"; + $encodedBytes = [System.Convert]::FromBase64String($env:PARTICL_TEST); + Set-Content $secretFileParticl -Value $encodedBytes -AsByteStream; + $secretFileParticlHash = Get-FileHash $secretFileParticl; + Write-Output "Secret file $secretFileParticl has hash $($secretFileParticlHash.Hash)"; + shell: pwsh env: CHANGE_NOW: ${{ secrets.CHANGE_NOW }} @@ -91,6 +97,7 @@ jobs: FIRO_TEST: ${{ secrets.FIRO_TEST }} BITCOINCASH_TEST: ${{ secrets.BITCOINCASH_TEST }} NAMECOIN_TEST: ${{ secrets.NAMECOIN_TEST }} + PARTICL_TEST: ${{ secrets.PARTICL_TEST }} # - name: Analyze # run: flutter analyze - name: Test @@ -109,6 +116,7 @@ jobs: $secretFileFiro = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/firo/firo_wallet_test_parameters.dart"; $secretFileBitcoinCash = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart"; $secretFileNamecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/namecoin/namecoin_wallet_test_parameters.dart"; + $secretFileParticl = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/particl/particl_wallet_test_parameters.dart"; Remove-Item -Path $secretFileExchange; Remove-Item -Path $secretFileBitcoin; @@ -116,5 +124,6 @@ jobs: Remove-Item -Path $secretFileFiro; Remove-Item -Path $secretFileBitcoinCash; Remove-Item -Path $secretFileNamecoin; + Remove-Item -Path $secretFileParticl; shell: pwsh if: always() From c9269fffef8077acd1793b30372d5dc6abcf9b01 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 13 Dec 2022 07:35:14 -0600 Subject: [PATCH 096/103] increase ping timeout --- lib/electrumx_rpc/electrumx.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/electrumx_rpc/electrumx.dart b/lib/electrumx_rpc/electrumx.dart index 8a816235a..c34b760e4 100644 --- a/lib/electrumx_rpc/electrumx.dart +++ b/lib/electrumx_rpc/electrumx.dart @@ -275,9 +275,9 @@ class ElectrumX { final response = await request( requestID: requestID, command: 'server.ping', - connectionTimeout: const Duration(seconds: 1), + connectionTimeout: const Duration(seconds: 2), retries: retryCount, - ).timeout(const Duration(seconds: 1)) as Map; + ).timeout(const Duration(seconds: 2)) as Map; return response.keys.contains("result") && response["result"] == null; } catch (e) { rethrow; From b83dec53c3f4a783d7a4e049ba2480e63925f259 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 13 Dec 2022 07:58:49 -0600 Subject: [PATCH 097/103] uninitialized var fix --- .../manage_nodes_views/add_edit_node_view.dart | 2 ++ 1 file changed, 2 insertions(+) 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 d34616567..7c18fffcc 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 @@ -764,6 +764,8 @@ class _NodeFormState extends ConsumerState { _isFailover = node.isFailover; if (widget.coin == Coin.epicCash) { enableSSLCheckbox = !node.host.startsWith("http"); + } else { + enableSSLCheckbox = true; } print("enableSSLCheckbox: $enableSSLCheckbox"); From 4f4d6f8e9de1b0ad98d8806b4213f2fe6cbbcd9e Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 13 Dec 2022 11:21:46 -0600 Subject: [PATCH 098/103] remove cached anon set server call --- lib/electrumx_rpc/cached_electrumx.dart | 15 ----------- lib/services/coins/firo/firo_wallet.dart | 34 ------------------------ 2 files changed, 49 deletions(-) diff --git a/lib/electrumx_rpc/cached_electrumx.dart b/lib/electrumx_rpc/cached_electrumx.dart index 2f7916d9e..f62e7e4f5 100644 --- a/lib/electrumx_rpc/cached_electrumx.dart +++ b/lib/electrumx_rpc/cached_electrumx.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/hive/db.dart'; -import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -63,20 +62,6 @@ class CachedElectrumX { "setHash": "", "coins": [], }; - - // try up to 3 times - for (int i = 0; i < 3; i++) { - final result = await getInitialAnonymitySetCache(groupId); - if (result != null) { - set["setHash"] = result["setHash"]; - set["blockHash"] = result["blockHash"]; - set["coins"] = result["coins"]; - Logging.instance.log( - "Populated initial anon set cache for group $groupId", - level: LogLevel.Info); - break; - } - } } else { set = Map.from(cachedSet); } diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 7445d718a..c87427b45 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -733,40 +733,6 @@ Future _setTestnetWrapper(bool isTestnet) async { // setTestnet(isTestnet); } -Future?> getInitialAnonymitySetCache( - String groupID, -) async { - Logging.instance.log("getInitialAnonymitySetCache", level: LogLevel.Info); - final Client client = Client(); - try { - final uri = Uri.parse("$kStackCommunityNodesEndpoint/getAnonymity"); - - final anonSetResult = await client.post( - uri, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - "jsonrpc": "2.0", - "id": "0", - 'aset': groupID, - }), - ); - - final response = jsonDecode(anonSetResult.body.toString()); - Logging.instance.log(response, level: LogLevel.Info); - if (response['status'] == 'success') { - final anonResponse = jsonDecode(response['result'] as String); - - final setData = Map.from(anonResponse as Map); - return setData; - } else { - return null; - } - } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Error); - return null; - } -} - /// Handles a single instance of a firo wallet class FiroWallet extends CoinServiceAPI { static const integrationTestFlag = From a2d6823ee9106ffe2b5ac5c26996d5ab7fceeb6e Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 14 Dec 2022 09:05:47 -0600 Subject: [PATCH 099/103] firo anon set decoding bugfix --- lib/electrumx_rpc/cached_electrumx.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/electrumx_rpc/cached_electrumx.dart b/lib/electrumx_rpc/cached_electrumx.dart index f62e7e4f5..e7d815eab 100644 --- a/lib/electrumx_rpc/cached_electrumx.dart +++ b/lib/electrumx_rpc/cached_electrumx.dart @@ -83,10 +83,10 @@ class CachedElectrumX { // update set with new data if (newSet["setHash"] != "" && set["setHash"] != newSet["setHash"]) { set["setHash"] = !isHexadecimal(newSet["setHash"] as String) - ? base64ToReverseHex(newSet["setHash"] as String) + ? base64ToHex(newSet["setHash"] as String) : newSet["setHash"]; set["blockHash"] = !isHexadecimal(newSet["blockHash"] as String) - ? base64ToHex(newSet["blockHash"] as String) + ? base64ToReverseHex(newSet["blockHash"] as String) : newSet["blockHash"]; for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) { dynamic newCoin = newSet["coins"][i]; From cf82374a0cb510ffc2ad2c834eecdcbc8c8f2f3c Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 14 Dec 2022 09:11:24 -0600 Subject: [PATCH 100/103] selectable balance text --- lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart index 0d6ddd305..d38e17a6a 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -164,7 +164,7 @@ class _WalletSummaryInfoState extends State { const Spacer(), FittedBox( fit: BoxFit.scaleDown, - child: Text( + child: SelectableText( "${Format.localizedStringAsFixed( value: balanceToShow, locale: locale, From c84ae8ff21d5a838753925843c6959c1981a0114 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 14 Dec 2022 10:26:48 -0600 Subject: [PATCH 101/103] db migrate to force firo cache clear --- lib/utilities/constants.dart | 2 +- lib/utilities/db_version_migration.dart | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index c6fe81d74..740e40c24 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -38,7 +38,7 @@ abstract class Constants { // Enable Logger.print statements static const bool disableLogger = false; - static const int currentHiveDbVersion = 3; + static const int currentHiveDbVersion = 4; static int satsPerCoin(Coin coin) { switch (coin) { diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index ae5190fc4..1320fab25 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -141,6 +141,7 @@ class DbVersionMigrator { // try to continue migrating return await migrate(2, secureStore: secureStore); + case 2: await Hive.openBox(DB.boxNamePrefs); final prefs = Prefs.instance; @@ -154,6 +155,20 @@ class DbVersionMigrator { boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 3); return await migrate(3, secureStore: secureStore); + case 3: + // clear possible broken firo cache + await DB.instance.deleteBoxFromDisk( + boxName: DB.instance.boxNameSetCache(coin: Coin.firo)); + await DB.instance.deleteBoxFromDisk( + boxName: DB.instance.boxNameUsedSerialsCache(coin: Coin.firo)); + + // update version + await DB.instance.put( + boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 4); + + // try to continue migrating + return await migrate(4, secureStore: secureStore); + default: // finally return return; From 5d025f90806d8712eb87d432bfc4c29ec381dc54 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 14 Dec 2022 10:51:08 -0600 Subject: [PATCH 102/103] update liblelantus commit --- crypto_plugins/flutter_liblelantus | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index b1e0b2062..265d83e1a 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit b1e0b20621be3ebb280ab3e3de10afe0c11db073 +Subproject commit 265d83e1adb3ca161e700214a9353bc044d16557 From 811bc0e8705e14932c82983786b48d2e7774830c Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 14 Dec 2022 13:57:43 -0600 Subject: [PATCH 103/103] word count fix --- .../new_wallet_recovery_phrase_warning_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 940c9bf08..b159fb9aa 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -184,7 +184,7 @@ class _NewWalletRecoveryPhraseWarningViewState ), ), TextSpan( - text: "12 words", + text: "$_numberOfPhraseWords words", style: STextStyles.desktopH3(context).copyWith( color: Theme.of(context) .extension()!