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() diff --git a/.gitignore b/.gitignore index 323aac218..a36824135 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,9 @@ 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.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/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/assets/images/particl.png b/assets/images/particl.png new file mode 100644 index 000000000..ef5939f47 Binary files /dev/null and b/assets/images/particl.png differ 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 @@ +<svg id="Particl_-_logo_white_text" data-name="Particl - logo white text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 228.62 228.33"><title>particl-part-logo</title><path d="M227.64,47.53a57.36,57.36,0,0,0-20.55-34.84A56.67,56.67,0,0,0,171.31,0Q113.76,0,56.16,0c-20,.23-39,11.82-48.7,29.24C2,38.62,0,49.13,0,59.85q0,52.43,0,104.88c-.07,8.13.29,15.94,2.83,23.74A58,58,0,0,0,42,226.26c-5.51-4.39-7.95-10.5-9.74-17.11-3.71-14.5-4-29.56-3.4-44.42-.06-35,0-69.93,0-104.88.05-6,.5-11.67,3.58-17a27.31,27.31,0,0,1,23.79-14q54.71,0,109.45,0c5.51,0,10.52-.4,15.78,1.59A27.2,27.2,0,0,1,199,50.14c1.08,5.29.79,11.16.81,16.55v102.6c0,5-.56,10-2.73,14.57-4.47,9.35-14.14,15.84-24.61,15.65-28.87,0-57.77,0-86.63,0-.06-9.41,0-18.82,0-28.24q35.36,0,70.69,0c5.07.28,10.91-2.29,13.24-7a15.68,15.68,0,0,0,1.6-7.58q0-42.78,0-85.53a13.21,13.21,0,0,0-7-12.5c-3.9-2.26-8.07-1.68-12.42-1.76-27,.07-54,0-80.94.05A13.87,13.87,0,0,0,57.08,71.24c-.11,41,0,82.15,0,123.14.21,8.14-.05,15.85,4.67,22.9,3.58,5.82,9.63,8.78,16.14,10.07,7.7,1.24,15.06.94,22.79,1q35.36,0,70.68,0A57.17,57.17,0,0,0,204.07,218c10.32-7.61,18.32-18.09,22-30.45,2.54-8.36,2.5-16.48,2.54-25.11q0-49,0-98A114.68,114.68,0,0,0,227.64,47.53Zm-85.28,95H85.83V85.77h56.53Z" style="fill:#45d492"/></svg> \ No newline at end of file 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"] diff --git a/lib/models/paymint/transactions_model.dart b/lib/models/paymint/transactions_model.dart index 382459922..06a21e16a 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<String, dynamic> 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: "", + scriptpubkeyAsm: "", + scriptpubkeyType: "", + 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/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 7c18fffcc..e29b0888f 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 @@ -135,6 +135,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { case Coin.dogecoin: case Coin.firo: case Coin.namecoin: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: @@ -681,6 +682,7 @@ class _NodeFormState extends ConsumerState<NodeForm> { case Coin.firo: case Coin.namecoin: case Coin.bitcoincash: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 16015ea0c..41dbed42b 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,16 @@ abstract class CoinServiceAPI { // tracker: tracker, ); + case Coin.particl: + return ParticlWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + secureStore: secureStorageInterface, + 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..0249fb205 --- /dev/null +++ b/lib/services/coins/particl/particl_wallet.dart @@ -0,0 +1,3538 @@ +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:devicelocale/devicelocale.dart'; +import 'package:flutter/foundation.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, 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<int, int, String, NetworkType, DerivePathType> args, +) { + return getBip32Node( + args.item1, + args.item2, + args.item3, + args.item4, + args.item5, + ); +} + +bip32.BIP32 getBip32NodeFromRoot( + int chain, + int index, + bip32.BIP32 root, + DerivePathType derivePathType, +) { + String coinType; + switch (root.network.wif) { + case 0x6c: // PART mainnet wif + coinType = "44"; // PART mainnet + 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.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<int, int, bip32.BIP32, DerivePathType> args, +) { + return getBip32NodeFromRoot( + args.item1, + args.item2, + args.item3, + args.item4, + ); +} + +bip32.BIP32 getBip32Root(String mnemonic, NetworkType network) { + final seed = bip39.mnemonicToSeed(mnemonic); + final networkType = bip32.NetworkType( + wif: network.wif, + bip32: bip32.Bip32Type( + public: network.bip32.public, + private: network.bip32.private, + ), + ); + + final root = bip32.BIP32.fromSeed(seed, networkType); + return root; +} + +/// wrapper for compute() +bip32.BIP32 getBip32RootWrapper(Tuple2<String, NetworkType> args) { + return getBip32Root(args.item1, args.item2); +} + +class 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; + default: + throw Exception("Invalid network type!"); + } + } + + List<UtxoObject> outputsList = []; + + @override + set isFavorite(bool markFavorite) { + DB.instance.put<dynamic>( + boxName: walletId, key: "isFavorite", value: markFavorite); + } + + @override + bool get isFavorite { + try { + return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") + as bool; + } catch (e, s) { + Logging.instance + .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + @override + Coin get coin => _coin; + + @override + Future<List<String>> get allOwnAddresses => + _allOwnAddresses ??= _fetchAllOwnAddresses(); + Future<List<String>>? _allOwnAddresses; + + Future<UtxoData>? _utxoData; + Future<UtxoData> get utxoData => _utxoData ??= _fetchUtxoData(); + + @override + Future<List<UtxoObject>> get unspentOutputs async => + (await utxoData).unspentOutputArray; + + @override + Future<Decimal> get availableBalance async { + final data = await utxoData; + return Format.satoshisToAmount( + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); + } + + @override + Future<Decimal> get pendingBalance async { + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); + } + + @override + Future<Decimal> get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(); + + @override + Future<Decimal> get totalBalance async { + if (!isActive) { + final totalBalance = DB.instance + .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; + if (totalBalance == null) { + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); + } else { + return Format.satoshisToAmount(totalBalance, coin: coin); + } + } + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); + } + + @override + Future<String> get currentReceivingAddress => _currentReceivingAddress ??= + _getCurrentAddressForChain(0, DerivePathType.bip84); + Future<String>? _currentReceivingAddress; + + Future<String> get currentLegacyReceivingAddress => + _currentReceivingAddressP2PKH ??= + _getCurrentAddressForChain(0, DerivePathType.bip44); + Future<String>? _currentReceivingAddressP2PKH; + + @override + Future<void> exit() async { + _hasCalledExit = true; + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } + + bool _hasCalledExit = false; + + @override + bool get hasCalledExit => _hasCalledExit; + + @override + Future<FeeObject> get fees => _feeObject ??= _getFees(); + Future<FeeObject>? _feeObject; + + @override + Future<int> get maxFee async { + final fee = (await fees).fast as String; + final satsFee = + Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin(coin)); + return satsFee.floor().toBigInt().toInt(); + } + + @override + Future<List<String>> get mnemonic => _getMnemonicList(); + + Future<int> get chainHeight async { + try { + final result = await _electrumXClient.getBlockHeadTip(); + return result["height"] as int; + } catch (e, s) { + Logging.instance.log("Exception caught in chainHeight: $e\n$s", + level: LogLevel.Error); + return -1; + } + } + + int get storedChainHeight { + final storedHeight = DB.instance + .get<dynamic>(boxName: walletId, key: "storedChainHeight") as int?; + return storedHeight ?? 0; + } + + Future<void> updateStoredChainHeight({required int newHeight}) async { + await DB.instance.put<dynamic>( + boxName: walletId, key: "storedChainHeight", value: newHeight); + } + + DerivePathType addressType({required String address}) { + Uint8List? decodeBase58; + Segwit? decodeBech32; + try { + decodeBase58 = bs58check.decode(address); + } catch (err) { + // Base58check decode fail + } + + // 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; + + @override + Future<void> recoverFromMnemonic({ + required String mnemonic, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { + longMutex = true; + final start = DateTime.now(); + try { + Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag", + level: LogLevel.Info); + if (!integrationTestFlag) { + final features = await electrumXClient.getServerFeatures(); + Logging.instance.log("features: $features", level: LogLevel.Info); + switch (coin) { + case Coin.particl: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main 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<Map<String, dynamic>> _checkGaps( + int maxNumberOfIndexesToCheck, + int maxUnusedAddressGap, + int txCountBatchSize, + bip32.BIP32 root, + DerivePathType type, + int account) async { + List<String> addressArray = []; + int returningIndex = -1; + Map<String, Map<String, String>> derivations = {}; + int gapCounter = 0; + for (int index = 0; + index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap; + index += txCountBatchSize) { + List<String> iterationsAddressArray = []; + Logging.instance.log( + "index: $index, \t GapCounter $account ${type.name}: $gapCounter", + level: LogLevel.Info); + + final _id = "k_$index"; + Map<String, String> txCountCallArgs = {}; + final Map<String, dynamic> 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.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<void> getTransactionCacheEarly(List<String> allAddresses) async { + try { + final List<Map<String, dynamic>> allTxHashes = + await _fetchHistory(allAddresses); + for (final txHash in allTxHashes) { + try { + unawaited(cachedElectrumXClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + )); + } catch (e) { + continue; + } + } + } catch (e) { + // + } + } + + Future<void> _recoverWalletFromBIP32SeedPhrase({ + required String mnemonic, + int maxUnusedAddressGap = 20, + int maxNumberOfIndexesToCheck = 1000, + }) async { + longMutex = true; + + Map<String, Map<String, String>> p2pkhReceiveDerivations = {}; + Map<String, Map<String, String>> p2wpkhReceiveDerivations = {}; + Map<String, Map<String, String>> p2pkhChangeDerivations = {}; + Map<String, Map<String, String>> p2wpkhChangeDerivations = {}; + + final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); + + List<String> p2pkhReceiveAddressArray = []; + List<String> p2wpkhReceiveAddressArray = []; + int p2pkhReceiveIndex = -1; + int p2wpkhReceiveIndex = -1; + + List<String> p2pkhChangeAddressArray = []; + List<String> 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<String>; + p2pkhReceiveIndex = (await resultReceive44)['index'] as int; + p2pkhReceiveDerivations = (await resultReceive44)['derivations'] + as Map<String, Map<String, String>>; + + p2wpkhReceiveAddressArray = + (await resultReceive84)['addressArray'] as List<String>; + p2wpkhReceiveIndex = (await resultReceive84)['index'] as int; + p2wpkhReceiveDerivations = (await resultReceive84)['derivations'] + as Map<String, Map<String, String>>; + + p2pkhChangeAddressArray = + (await resultChange44)['addressArray'] as List<String>; + p2pkhChangeIndex = (await resultChange44)['index'] as int; + p2pkhChangeDerivations = (await resultChange44)['derivations'] + as Map<String, Map<String, String>>; + + p2wpkhChangeAddressArray = + (await resultChange84)['addressArray'] as List<String>; + p2wpkhChangeIndex = (await resultChange84)['index'] as int; + p2wpkhChangeDerivations = (await resultChange84)['derivations'] + as Map<String, Map<String, String>>; + + // 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<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2WPKH', + value: p2wpkhReceiveAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2WPKH', + value: p2wpkhChangeAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: p2pkhReceiveAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: p2pkhChangeAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH', + value: p2wpkhReceiveIndex); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2WPKH', + value: p2wpkhChangeIndex); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'changeIndexP2PKH', value: p2pkhChangeIndex); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: p2pkhReceiveIndex); + await DB.instance + .put<dynamic>(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put<dynamic>(boxName: walletId, key: "isFavorite", value: false); + + longMutex = false; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $e\n$s", + level: LogLevel.Error); + + longMutex = false; + rethrow; + } + } + + Future<bool> refreshIfThereIsNewData() async { + if (longMutex) return false; + if (_hasCalledExit) return false; + Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); + + try { + bool needsRefresh = false; + Set<String> txnsToCheck = {}; + + for (final String txid in txTracker.pendings) { + if (!txTracker.wasNotifiedConfirmed(txid)) { + txnsToCheck.add(txid); + } + } + + for (String txid in txnsToCheck) { + final txn = await electrumXClient.getTransaction(txHash: txid); + int confirmations = txn["confirmations"] as int? ?? 0; + bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; + if (!isUnconfirmed) { + // unconfirmedTxs = {}; + needsRefresh = true; + break; + } + } + if (!needsRefresh) { + var allOwnAddresses = await _fetchAllOwnAddresses(); + List<Map<String, dynamic>> allTxs = + await _fetchHistory(allOwnAddresses); + final txData = await transactionData; + for (Map<String, dynamic> transaction in allTxs) { + if (txData.findTransaction(transaction['tx_hash'] as String) == + null) { + Logging.instance.log( + " txid not found in address history already ${transaction['tx_hash']}", + level: LogLevel.Info); + needsRefresh = true; + break; + } + } + } + return needsRefresh; + } catch (e, s) { + Logging.instance.log( + "Exception caught in refreshIfThereIsNewData: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> getAllTxsToWatch( + TransactionData txData, + ) async { + if (_hasCalledExit) return; + List<models.Transaction> unconfirmedTxnsToNotifyPending = []; + List<models.Transaction> 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<void> refresh() async { + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } + + try { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); + + final currentHeight = await chainHeight; + const storedHeight = 1; //await storedChainHeight; + + Logging.instance + .log("chain height: $currentHeight", level: LogLevel.Info); + Logging.instance + .log("cached height: $storedHeight", level: LogLevel.Info); + + if (currentHeight != storedHeight) { + if (currentHeight != -1) { + // -1 failed to fetch current height + 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); + if (await refreshIfThereIsNewData()) { + await refresh(); + GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId)); + } + }); + } + } catch (error, strace) { + refreshMutex = false; + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + coin, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + Logging.instance.log( + "Caught exception in refreshWalletData(): $error\n$strace", + level: LogLevel.Error); + } + } + + @override + Future<Map<String, dynamic>> prepareSend({ + required String address, + required int satoshiAmount, + Map<String, dynamic>? args, + }) async { + try { + final feeRateType = args?["feeRate"]; + final feeRateAmount = args?["feeRateAmount"]; + if (feeRateType is FeeRateType || feeRateAmount is int) { + late final int rate; + if (feeRateType is FeeRateType) { + int fee = 0; + final feeObject = await fees; + switch (feeRateType) { + case FeeRateType.fast: + fee = feeObject.fast; + break; + case FeeRateType.average: + fee = feeObject.medium; + break; + case FeeRateType.slow: + fee = feeObject.slow; + break; + } + rate = fee; + } else { + rate = feeRateAmount as int; + } + + // check for send all + bool isSendAll = false; + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); + 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<String, dynamic>; + } 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<String> confirmSend({required Map<String, 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); + Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + + return txHash; + } catch (e, s) { + Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + Future<String> send({ + required String toAddress, + required int amount, + Map<String, String> args = const {}, + }) async { + try { + final txData = await prepareSend( + address: toAddress, satoshiAmount: amount, args: args); + final txHash = await confirmSend(txData: txData); + return txHash; + } catch (e, s) { + Logging.instance + .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + @override + Future<bool> testNetworkConnection() async { + try { + final result = await _electrumXClient.ping(); + return result; + } catch (_) { + return false; + } + } + + Timer? _networkAliveTimer; + + void startNetworkAlivePinging() { + // call once on start right away + _periodicPingCheck(); + + // then periodically check + _networkAliveTimer = Timer.periodic( + Constants.networkAliveTimerDuration, + (_) async { + _periodicPingCheck(); + }, + ); + } + + void _periodicPingCheck() async { + bool hasNetwork = await testNetworkConnection(); + _isConnected = hasNetwork; + if (_isConnected != hasNetwork) { + NodeConnectionStatus status = hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; + GlobalEventBus.instance + .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); + } + } + + void stopNetworkAlivePinging() { + _networkAliveTimer?.cancel(); + _networkAliveTimer = null; + } + + bool _isConnected = false; + + @override + bool get isConnected => _isConnected; + + @override + Future<void> initializeNew() async { + Logging.instance + .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); + + if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) != null) { + throw Exception( + "Attempted to initialize a new wallet using an existing wallet ID!"); + } + + await _prefs.init(); + try { + await _generateNewWallet(); + } catch (e, s) { + Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", + level: LogLevel.Fatal); + rethrow; + } + await Future.wait([ + DB.instance.put<dynamic>(boxName: walletId, key: "id", value: walletId), + DB.instance + .put<dynamic>(boxName: walletId, key: "isFavorite", value: false), + ]); + } + + @override + Future<void> initializeExisting() async { + Logging.instance.log("Opening existing ${coin.prettyName} wallet.", + level: LogLevel.Info); + + if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) == null) { + throw Exception( + "Attempted to initialize an existing wallet using an unknown wallet ID!"); + } + await _prefs.init(); + final data = + DB.instance.get<dynamic>(boxName: walletId, key: "latest_tx_model") + as TransactionData?; + if (data != null) { + _transactionData = Future(() => data); + } + } + + @override + Future<TransactionData> get transactionData => + _transactionData ??= _fetchTransactionData(); + Future<TransactionData>? _transactionData; + + 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<void> updateSentCachedTxData(Map<String, dynamic> 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); + } else { + 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, particl.bech32!); + } + + @override + String get walletId => _walletId; + late String _walletId; + + @override + String get walletName => _walletName; + late String _walletName; + + // setter for updating on rename + @override + set walletName(String newName) => _walletName = newName; + + late ElectrumX _electrumXClient; + + ElectrumX get electrumXClient => _electrumXClient; + + late CachedElectrumX _cachedElectrumXClient; + + CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; + + late SecureStorageInterface _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, + required SecureStorageInterface secureStore, + }) { + txTracker = tracker; + _walletId = walletId; + _walletName = walletName; + _coin = coin; + _electrumXClient = client; + _cachedElectrumXClient = cachedClient; + + _priceAPI = priceAPI ?? PriceAPI(Client()); + _secureStore = secureStore; + } + + @override + Future<void> updateNode(bool shouldRefresh) async { + final failovers = NodeService(secureStorageInterface: _secureStore) + .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<List<String>> _getMnemonicList() async { + final mnemonicString = + await _secureStore.read(key: '${_walletId}_mnemonic'); + if (mnemonicString == null) { + return []; + } + final List<String> data = mnemonicString.split(' '); + return data; + } + + Future<ElectrumXNode> getCurrentNode() async { + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + + return ElectrumXNode( + address: node.host, + port: node.port, + name: node.name, + useSSL: node.useSSL, + id: node.id, + ); + } + + Future<List<String>> _fetchAllOwnAddresses() async { + final List<String> allAddresses = []; + final receivingAddresses = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2WPKH') as List<dynamic>; + final changeAddresses = DB.instance.get<dynamic>( + boxName: walletId, key: 'changeAddressesP2WPKH') as List<dynamic>; + final receivingAddressesP2PKH = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2PKH') as List<dynamic>; + final changeAddressesP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH') + as List<dynamic>; + + for (var i = 0; i < receivingAddresses.length; i++) { + if (!allAddresses.contains(receivingAddresses[i])) { + allAddresses.add(receivingAddresses[i] 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); + } + } + + return allAddresses; + } + + Future<FeeObject> _getFees() async { + try { + //TODO adjust numbers for different speeds? + const int f = 1, m = 5, s = 20; + + final fast = await electrumXClient.estimateFee(blocks: f); + final medium = await electrumXClient.estimateFee(blocks: m); + final slow = await electrumXClient.estimateFee(blocks: s); + + final feeObject = FeeObject( + numberOfBlocksFast: f, + numberOfBlocksAverage: m, + numberOfBlocksSlow: s, + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), + ); + + Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); + return feeObject; + } catch (e) { + Logging.instance + .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error); + rethrow; + } + } + + Future<void> _generateNewWallet() async { + Logging.instance + .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); + if (!integrationTestFlag) { + final features = await electrumXClient.getServerFeatures(); + Logging.instance.log("features: $features", level: LogLevel.Info); + switch (coin) { + case Coin.particl: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main 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<dynamic>(boxName: walletId, key: "receivingIndexP2WPKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "changeIndexP2WPKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "receivingIndexP2PKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "changeIndexP2PKH", value: 0); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'blocked_tx_hashes', + value: ["0xdefault"], + ); // A list of transaction hashes to represent frozen utxos in wallet + // initialize address book entries + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'addressBookEntries', + value: <String, String>{}); + + // Generate and add addresses to relevant arrays + 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, + ), + ), + ]); + + 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<String> _generateAddressForChain( + int chain, + int index, + DerivePathType derivePathType, + ) async { + final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); + final node = await compute( + getBip32NodeWrapper, + Tuple5( + chain, + index, + mnemonic!, + _network, + derivePathType, + ), + ); + final data = PaymentData(pubkey: node.publicKey); + String address; + + switch (derivePathType) { + case DerivePathType.bip44: + address = P2PKH(data: data, network: _network).data.address!; + break; + 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<void> _incrementAddressIndexForChain( + int chain, DerivePathType derivePathType) async { + // Here we assume chain == 1 if it isn't 0 + String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + final newIndex = + (DB.instance.get<dynamic>(boxName: walletId, key: indexKey)) + 1; + await DB.instance + .put<dynamic>(boxName: walletId, key: indexKey, value: newIndex); + } + + /// Adds [address] to the relevant chain's address array, which is determined by [chain]. + /// [address] - Expects a standard native segwit address + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future<void> _addToAddressesArrayForChain( + String address, int chain, DerivePathType derivePathType) async { + String chainArray = ''; + if (chain == 0) { + chainArray = 'receivingAddresses'; + } else { + chainArray = 'changeAddresses'; + } + switch (derivePathType) { + case DerivePathType.bip44: + chainArray += "P2PKH"; + break; + case DerivePathType.bip84: + chainArray += "P2WPKH"; + break; + } + + final addressArray = + DB.instance.get<dynamic>(boxName: walletId, key: chainArray); + if (addressArray == null) { + Logging.instance.log( + 'Attempting to add the following to $chainArray array for chain $chain:${[ + address + ]}', + level: LogLevel.Info); + await DB.instance + .put<dynamic>(boxName: walletId, key: chainArray, value: [address]); + } else { + // Make a deep copy of the existing list + final List<String> newArray = []; + addressArray + .forEach((dynamic _address) => newArray.add(_address as String)); + newArray.add(address); // Add the address passed into the method + await DB.instance + .put<dynamic>(boxName: walletId, key: chainArray, value: newArray); + } + } + + /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] + /// and + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future<String> _getCurrentAddressForChain( + int chain, DerivePathType derivePathType) async { + // Here, we assume that chain == 1 if it isn't 0 + String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; + switch (derivePathType) { + case DerivePathType.bip44: + arrayKey += "P2PKH"; + break; + case DerivePathType.bip84: + arrayKey += "P2WPKH"; + break; + } + final internalChainArray = + DB.instance.get<dynamic>(boxName: walletId, key: arrayKey); + return internalChainArray.last as String; + } + + String _buildDerivationStorageKey({ + required int chain, + required DerivePathType derivePathType, + }) { + String key; + String chainId = chain == 0 ? "receive" : "change"; + + switch (derivePathType) { + case DerivePathType.bip44: + key = "${walletId}_${chainId}DerivationsP2PKH"; + break; + case DerivePathType.bip84: + key = "${walletId}_${chainId}DerivationsP2WPKH"; + break; + } + return key; + } + + Future<Map<String, dynamic>> _fetchDerivations({ + required int chain, + required DerivePathType derivePathType, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + return Map<String, dynamic>.from( + jsonDecode(derivationsString ?? "{}") as Map); + } + + /// Add a single derivation to the local secure storage for [chain] and + /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. + /// This will overwrite a previous entry where the address of the new derivation + /// matches a derivation currently stored. + Future<void> addDerivation({ + required int chain, + required String address, + required String pubKey, + required String wif, + required DerivePathType derivePathType, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + final derivations = + Map<String, dynamic>.from(jsonDecode(derivationsString ?? "{}") as Map); + + // add derivation + derivations[address] = { + "pubKey": pubKey, + "wif": wif, + }; + + // save derivations + final newReceiveDerivationsString = jsonEncode(derivations); + await _secureStore.write(key: key, value: newReceiveDerivationsString); + } + + /// Add multiple derivations to the local secure storage for [chain] and + /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. + /// This will overwrite any previous entries where the address of the new derivation + /// matches a derivation currently stored. + /// The [derivationsToAdd] must be in the format of: + /// { + /// addressA : { + /// "pubKey": <the pubKey string>, + /// "wif": <the wif string>, + /// }, + /// addressB : { + /// "pubKey": <the pubKey string>, + /// "wif": <the wif string>, + /// }, + /// } + Future<void> addDerivations({ + required int chain, + required DerivePathType derivePathType, + required Map<String, dynamic> derivationsToAdd, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + final derivations = + Map<String, dynamic>.from(jsonDecode(derivationsString ?? "{}") as Map); + + // add derivation + derivations.addAll(derivationsToAdd); + + // save derivations + final newReceiveDerivationsString = jsonEncode(derivations); + await _secureStore.write(key: key, value: newReceiveDerivationsString); + } + + Future<UtxoData> _fetchUtxoData() async { + final List<String> allAddresses = await _fetchAllOwnAddresses(); + + try { + final fetchedUtxoList = <List<Map<String, dynamic>>>[]; + + final Map<int, Map<String, List<dynamic>>> batches = {}; + const batchSizeMax = 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<Map<String, dynamic>> outputArray = []; + int satoshiBalance = 0; + int satoshiBalancePending = 0; + + for (int i = 0; i < fetchedUtxoList.length; i++) { + for (int j = 0; j < fetchedUtxoList[i].length; j++) { + int value = fetchedUtxoList[i][j]["value"] as int; + satoshiBalance += value; + + final txn = await cachedElectrumXClient.getTransaction( + txHash: fetchedUtxoList[i][j]["tx_hash"] as String, + verbose: true, + coin: coin, + ); + + final Map<String, dynamic> utxo = {}; + final int confirmations = txn["confirmations"] as int? ?? 0; + final bool confirmed = confirmations >= MINIMUM_CONFIRMATIONS; + if (!confirmed) { + satoshiBalancePending += value; + } + + utxo["txid"] = txn["txid"]; + utxo["vout"] = fetchedUtxoList[i][j]["tx_pos"]; + utxo["value"] = value; + + utxo["status"] = <String, dynamic>{}; + utxo["status"]["confirmed"] = confirmed; + utxo["status"]["confirmations"] = confirmations; + utxo["status"]["block_height"] = fetchedUtxoList[i][j]["height"]; + utxo["status"]["block_hash"] = txn["blockhash"]; + utxo["status"]["block_time"] = txn["blocktime"]; + + final fiatValue = ((Decimal.fromInt(value) * currentPrice) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2); + utxo["rawWorth"] = fiatValue; + utxo["fiatWorth"] = fiatValue.toString(); + outputArray.add(utxo); + } + } + + Decimal currencyBalanceRaw = + ((Decimal.fromInt(satoshiBalance) * currentPrice) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2); + + final Map<String, dynamic> result = { + "total_user_currency": currencyBalanceRaw.toString(), + "total_sats": satoshiBalance, + "total_btc": (Decimal.fromInt(satoshiBalance) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) + .toString(), + "outputArray": outputArray, + "unconfirmed": satoshiBalancePending, + }; + + final dataModel = UtxoData.fromJson(result); + + final List<UtxoObject> allOutputs = dataModel.unspentOutputArray; + Logging.instance + .log('Outputs fetched: $allOutputs', level: LogLevel.Info); + await _sortOutputs(allOutputs); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_utxo_model', value: dataModel); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'totalBalance', + value: dataModel.satoshiBalance); + return dataModel; + } catch (e, s) { + Logging.instance + .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); + final latestTxModel = + DB.instance.get<dynamic>(boxName: walletId, key: 'latest_utxo_model') + as models.UtxoData?; + + if (latestTxModel == null) { + final emptyModel = { + "total_user_currency": "0.00", + "total_sats": 0, + "total_btc": "0", + "outputArray": <dynamic>[] + }; + return UtxoData.fromJson(emptyModel); + } else { + Logging.instance + .log("Old output model located", level: LogLevel.Warning); + return latestTxModel; + } + } + } + + /// Takes in a list of UtxoObjects and adds a name (dependent on object index within list) + /// and checks for the txid associated with the utxo being blocked and marks it accordingly. + /// Now also checks for output labeling. + Future<void> _sortOutputs(List<UtxoObject> utxos) async { + final blockedHashArray = + DB.instance.get<dynamic>(boxName: walletId, key: 'blocked_tx_hashes') + as List<dynamic>?; + final List<String> lst = []; + if (blockedHashArray != null) { + for (var hash in blockedHashArray) { + lst.add(hash as String); + } + } + final labels = + DB.instance.get<dynamic>(boxName: walletId, key: 'labels') as Map? ?? + {}; + + outputsList = []; + + for (var i = 0; i < utxos.length; i++) { + if (labels[utxos[i].txid] != null) { + utxos[i].txName = labels[utxos[i].txid] as String? ?? ""; + } else { + utxos[i].txName = 'Output #$i'; + } + + if (utxos[i].status.confirmed == false) { + outputsList.add(utxos[i]); + } else { + if (lst.contains(utxos[i].txid)) { + utxos[i].blocked = true; + outputsList.add(utxos[i]); + } else if (!lst.contains(utxos[i].txid)) { + outputsList.add(utxos[i]); + } + } + } + } + + Future<int> getTxCount({required String address}) async { + String? scripthash; + try { + scripthash = _convertToScriptHash(address, _network); + final transactions = + await electrumXClient.getHistory(scripthash: scripthash); + return transactions.length; + } catch (e) { + Logging.instance.log( + "Exception rethrown in _getTxCount(address: $address, scripthash: $scripthash): $e", + level: LogLevel.Error); + rethrow; + } + } + + Future<Map<String, int>> _getBatchTxCount({ + required Map<String, String> addresses, + }) async { + try { + final Map<String, List<dynamic>> args = {}; + for (final entry in addresses.entries) { + args[entry.key] = [_convertToScriptHash(entry.value, _network)]; + } + final response = await electrumXClient.getBatchHistory(args: args); + + final Map<String, int> result = {}; + for (final entry in response.entries) { + result[entry.key] = entry.value.length; + } + return result; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown in _getBatchTxCount(address: $addresses: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _checkReceivingAddressForTransactions( + DerivePathType derivePathType) async { + try { + final String currentExternalAddr = + await _getCurrentAddressForChain(0, derivePathType); + final int txCount = await getTxCount(address: currentExternalAddr); + Logging.instance.log( + 'Number of txs for current receiving address $currentExternalAddr: $txCount', + level: LogLevel.Info); + + if (txCount >= 1) { + // First increment the receiving index + await _incrementAddressIndexForChain(0, derivePathType); + + // Check the new receiving index + String indexKey = "receivingIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + final newReceivingIndex = + DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new receiving address + final newReceivingAddress = await _generateAddressForChain( + 0, newReceivingIndex, derivePathType); + + // Add that new receiving address to the array of receiving addresses + await _addToAddressesArrayForChain( + newReceivingAddress, 0, derivePathType); + + // Set the new receiving address that the service + + switch (derivePathType) { + case DerivePathType.bip44: + _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); + break; + 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<void> _checkChangeAddressForTransactions( + DerivePathType derivePathType) async { + try { + final String currentExternalAddr = + await _getCurrentAddressForChain(1, derivePathType); + final int txCount = await getTxCount(address: currentExternalAddr); + Logging.instance.log( + 'Number of txs for current change address $currentExternalAddr: $txCount', + level: LogLevel.Info); + + if (txCount >= 1) { + // First increment the change index + await _incrementAddressIndexForChain(1, derivePathType); + + // Check the new change index + String indexKey = "changeIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + final newChangeIndex = + DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new change address + final newChangeAddress = + await _generateAddressForChain(1, newChangeIndex, derivePathType); + + // Add that new receiving address to the array of change addresses + await _addToAddressesArrayForChain(newChangeAddress, 1, derivePathType); + } + } on SocketException catch (se, s) { + Logging.instance.log( + "SocketException caught in _checkReceivingAddressForTransactions($derivePathType): $se\n$s", + level: LogLevel.Error); + return; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _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<void> checkCurrentReceivingAddressesForTransactions() async { + if (Platform.environment["FLUTTER_TEST"] == "true") { + try { + return _checkCurrentReceivingAddressesForTransactions(); + } catch (_) { + rethrow; + } + } + } + + Future<void> _checkCurrentChangeAddressesForTransactions() async { + try { + for (final type in DerivePathType.values) { + await _checkChangeAddressForTransactions(type); + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkCurrentChangeAddressesForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + /// public wrapper because dart can't test private... + Future<void> checkCurrentChangeAddressesForTransactions() async { + if (Platform.environment["FLUTTER_TEST"] == "true") { + try { + return _checkCurrentChangeAddressesForTransactions(); + } catch (_) { + rethrow; + } + } + } + + /// attempts to convert a string to a valid scripthash + /// + /// Returns the scripthash or throws an exception on invalid particl address + String _convertToScriptHash(String particlAddress, NetworkType network) { + try { + final output = Address.addressToOutputScript( + particlAddress, network, particl.bech32!); + final hash = sha256.convert(output.toList(growable: false)).toString(); + + final chars = hash.split(""); + final reversedPairs = <String>[]; + var i = chars.length - 1; + while (i > 0) { + reversedPairs.add(chars[i - 1]); + reversedPairs.add(chars[i]); + i -= 2; + } + return reversedPairs.join(""); + } catch (e) { + rethrow; + } + } + + Future<List<Map<String, dynamic>>> _fetchHistory( + List<String> allAddresses) async { + try { + List<Map<String, dynamic>> allTxHashes = []; + + final Map<int, Map<String, List<dynamic>>> batches = {}; + final Map<String, String> requestIdToAddressMap = {}; + const batchSizeMax = 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<Map<String, dynamic>> allTransactions, String txid) { + for (int i = 0; i < allTransactions.length; i++) { + if (allTransactions[i]["txid"] == txid) { + return true; + } + } + return false; + } + + Future<List<Map<String, dynamic>>> fastFetch(List<String> allTxHashes) async { + List<Map<String, dynamic>> allTransactions = []; + + const futureLimit = 30; + List<Future<Map<String, dynamic>>> transactionFutures = []; + int currentFutureCount = 0; + for (final txHash in allTxHashes) { + Future<Map<String, dynamic>> 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<TransactionData> _fetchTransactionData() async { + final List<String> allAddresses = await _fetchAllOwnAddresses(); + + final changeAddresses = DB.instance.get<dynamic>( + boxName: walletId, key: 'changeAddressesP2WPKH') as List<dynamic>; + + final List<Map<String, dynamic>> allTxHashes = + await _fetchHistory(allAddresses); + + final cachedTransactions = + DB.instance.get<dynamic>(boxName: walletId, key: 'latest_tx_model') + as TransactionData?; + int latestTxnBlockHeight = + DB.instance.get<dynamic>(boxName: walletId, key: "storedTxnDataHeight") + as int? ?? + 0; + + final unconfirmedCachedTransactions = + cachedTransactions?.getAllTransactions() ?? {}; + unconfirmedCachedTransactions + .removeWhere((key, value) => value.confirmedStatus); + + if (cachedTransactions != null) { + for (final tx in allTxHashes.toList(growable: false)) { + final txHeight = tx["height"] as int; + if (txHeight > 0 && + txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { + if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { + allTxHashes.remove(tx); + } + } + } + } + + Set<String> hashes = {}; + for (var element in allTxHashes) { + hashes.add(element['tx_hash'] as String); + } + await fastFetch(hashes.toList()); + List<Map<String, dynamic>> allTransactions = []; + + for (final txHash in allTxHashes) { + final tx = await cachedElectrumXClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + ); + + // Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}"); + // TODO fix this for sent to self transactions? + if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { + tx["address"] = txHash["address"]; + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + + Logging.instance.log("addAddresses: $allAddresses", level: LogLevel.Info); + Logging.instance.log("allTxHashes: $allTxHashes", level: LogLevel.Info); + + Logging.instance.log("allTransactions length: ${allTransactions.length}", + level: LogLevel.Info); + + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List<Map<String, dynamic>> midSortedArray = []; + + Set<String> vHashes = {}; + for (final txObject in allTransactions) { + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"]![i] as Map; + final prevTxid = input["txid"] as String; + vHashes.add(prevTxid); + } + } + await fastFetch(vHashes.toList()); + + for (final txObject in allTransactions) { + List<String> sendersArray = []; + List<String> recipientsArray = []; + + // Usually only has value when txType = 'Send' + int inputAmtSentFromWallet = 0; + // Usually has value regardless of txType due to change addresses + int outputAmtAddressedToWallet = 0; + int fee = 0; + + Map<String, dynamic> midSortedTx = {}; + + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"]![i] as Map; + final prevTxid = input["txid"] as String; + final prevOut = input["vout"] as int; + + final tx = await _cachedElectrumXClient.getTransaction( + txHash: prevTxid, + coin: coin, + ); + + for (final out in tx["vout"] as List) { + if (prevOut == out["n"]) { + final address = out["scriptPubKey"]["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) { + // 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); + } + } + + 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(coin))) + .toBigInt() + .toInt(); + } + } + } + totalInput = inputAmtSentFromWallet; + int totalOutput = 0; + + Logging.instance.log("txObject: ${txObject}", level: LogLevel.Info); + + for (final output in txObject["vout"] as List) { + // 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); + 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 + Logging.instance.log( + "output is private (RingCT); cannot parse output values", + level: LogLevel.Info); + } else { + // TODO detect staking + Logging.instance.log("output type not detected; output: ${output}", + level: LogLevel.Info); + } + } + // 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(coin))) + .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(coin))) + .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(coin))) + .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(coin))) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + midSortedTx["worthNow"] = worthNow; + } + midSortedTx["aliens"] = <dynamic>[]; + midSortedTx["fees"] = fee; + midSortedTx["address"] = txObject["address"]; + midSortedTx["inputSize"] = txObject["vin"].length; + midSortedTx["outputSize"] = txObject["vout"].length; + midSortedTx["inputs"] = txObject["vin"]; + midSortedTx["outputs"] = txObject["vout"]; + + final int height = txObject["height"] as int; + midSortedTx["height"] = height; + + if (height >= latestTxnBlockHeight) { + latestTxnBlockHeight = height; + } + + midSortedArray.add(midSortedTx); + } + + // sort by date ---- //TODO not sure if needed + // shouldn't be any issues with a null timestamp but I got one at some point? + midSortedArray + .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); + // { + // final aT = a["timestamp"]; + // final bT = b["timestamp"]; + // + // if (aT == null && bT == null) { + // return 0; + // } else if (aT == null) { + // return -1; + // } else if (bT == null) { + // return 1; + // } else { + // return bT - aT; + // } + // }); + + // buildDateTimeChunks + final Map<String, dynamic> result = {"dateTimeChunks": <dynamic>[]}; + final dateArray = <dynamic>[]; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = extractDateFromTimestamp(txObject["timestamp"] as int); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp(chunk["timestamp"] as int) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = <Map<String, dynamic>>[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; + transactionsMap + .addAll(TransactionData.fromJson(result).getAllTransactions()); + + final txModel = TransactionData.fromMap(transactionsMap); + + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'storedTxnDataHeight', + value: latestTxnBlockHeight); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_tx_model', value: txModel); + + return txModel; + } + + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } + + /// The coinselection algorithm decides whether or not the user is eligible to make the transaction + /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return + /// a map containing the tx hex along with other important information. If not, then it will return + /// an integer (1 or 2) + dynamic coinSelection( + int satoshiAmountToSend, + int selectedTxFeeRate, + String _recipientAddress, + bool isSendAll, { + int additionalOutputs = 0, + List<UtxoObject>? utxos, + }) async { + Logging.instance + .log("Starting coinSelection ----------", level: LogLevel.Info); + final List<UtxoObject> availableOutputs = utxos ?? outputsList; + final List<UtxoObject> spendableOutputs = []; + int spendableSatoshiValue = 0; + 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 && + availableOutputs[i].status.confirmed == true) { + spendableOutputs.add(availableOutputs[i]); + spendableSatoshiValue += availableOutputs[i].value; + } + } + + // sort spendable by age (oldest first) + spendableOutputs.sort( + (a, b) => b.status.confirmations.compareTo(a.status.confirmations)); + + Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", + level: LogLevel.Info); + Logging.instance + .log("spendableOutputs: $spendableOutputs", level: LogLevel.Info); + Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue", + level: LogLevel.Info); + Logging.instance + .log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info); + // If the amount the user is trying to send is smaller than the amount that they have spendable, + // then return 1, which indicates that they have an insufficient balance. + if (spendableSatoshiValue < satoshiAmountToSend) { + return 1; + // If the amount the user wants to send is exactly equal to the amount they can spend, then return + // 2, which indicates that they are not leaving enough over to pay the transaction fee + } else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) { + return 2; + } + // If neither of these statements pass, we assume that the user has a spendable balance greater + // than the amount they're attempting to send. Note that this value still does not account for + // the added transaction fee, which may require an extra input and will need to be checked for + // later on. + + // Possible situation right here + int satoshisBeingUsed = 0; + int inputsBeingConsumed = 0; + List<UtxoObject> utxoObjectsToUse = []; + + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += spendableOutputs[i].value; + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; + inputsBeingConsumed += 1; + } + + Logging.instance + .log("satoshisBeingUsed: $satoshisBeingUsed", level: LogLevel.Info); + Logging.instance + .log("inputsBeingConsumed: $inputsBeingConsumed", level: LogLevel.Info); + Logging.instance + .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); + + // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray + List<String> recipientsArray = [_recipientAddress]; + List<int> recipientsAmtArray = [satoshiAmountToSend]; + + // gather required signing data + final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + + if (isSendAll) { + Logging.instance + .log("Attempting to send all $coin", level: LogLevel.Info); + + final int vSizeForOneOutput = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [_recipientAddress], + satoshiAmounts: [satoshisBeingUsed - 1], + ))["vSize"] as int; + int feeForOneOutput = estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ); + + 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<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": amount, + "fee": feeForOneOutput, + "vSize": txn["vSize"], + }; + return transactionObject; + } + + final int vSizeForOneOutput = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [_recipientAddress], + satoshiAmounts: [satoshisBeingUsed - 1], + ))["vSize"] as int; + final int vSizeForTwoOutPuts = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [ + _recipientAddress, + await _getCurrentAddressForChain(1, DerivePathType.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<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": feeBeingPaid, + "vSize": txn["vSize"], + }; + return transactionObject; + } else { + // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize + // is smaller than or equal to DUST_LIMIT. Revert to single output transaction. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": satoshisBeingUsed - satoshiAmountToSend, + "vSize": txn["vSize"], + }; + return transactionObject; + } + } else { + // No additional outputs needed since adding one would mean that it'd be smaller than 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<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": satoshisBeingUsed - satoshiAmountToSend, + "vSize": txn["vSize"], + }; + return transactionObject; + } + } else if (satoshisBeingUsed - satoshiAmountToSend == feeForOneOutput) { + // In this scenario, no additional change output is needed since inputs - outputs equal exactly + // what we need to pay for fees. Here, we pass data directly to instruct the wallet to begin + // crafting the transaction that the user requested. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Fee being paid: ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": feeForOneOutput, + "vSize": txn["vSize"], + }; + return transactionObject; + } else { + // Remember that returning 2 indicates that the user does not have a sufficient balance to + // pay for the transaction fee. Ideally, at this stage, we should check if the user has any + // additional outputs they're able to spend and then recalculate fees. + Logging.instance.log( + 'Cannot pay tx fee - checking for more outputs and trying again', + level: LogLevel.Warning); + // try adding more outputs + if (spendableOutputs.length > inputsBeingConsumed) { + return coinSelection(satoshiAmountToSend, selectedTxFeeRate, + _recipientAddress, isSendAll, + additionalOutputs: additionalOutputs + 1, utxos: utxos); + } + return 2; + } + } + + Future<Map<String, dynamic>> fetchBuildTxData( + List<UtxoObject> utxosToUse, + ) async { + // return data + Map<String, dynamic> results = {}; + Map<String, List<String>> addressTxid = {}; + + print("CALLING FETCH BUILD TX DATA"); + + // addresses to check + List<String> addressesP2PKH = []; + List<String> 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) { + print("SCRIPT PUB KEY IS ${output["scriptPubKey"]}"); + final address = output["scriptPubKey"]["address"] as String; + if (!addressTxid.containsKey(address)) { + addressTxid[address] = <String>[]; + } + (addressTxid[address] as List).add(txid); + switch (addressType(address: address)) { + case DerivePathType.bip44: + addressesP2PKH.add(address); + break; + 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, + ), + }; + } + } + } + } + } + + // 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; + } catch (e, s) { + Logging.instance + .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); + rethrow; + } + } + + /// Builds and signs a transaction + Future<Map<String, dynamic>> buildTransaction({ + required List<UtxoObject> utxosToUse, + required Map<String, dynamic> utxoSigningData, + required List<String> recipients, + required List<int> satoshiAmounts, + }) async { + Logging.instance + .log("Starting buildTransaction ----------", level: LogLevel.Info); + + 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++) { + 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], 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?); + } + } 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(); + + String hexBefore = builtTx.toHex(isParticl: true).toString(); + 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 + Future<void> fullRescan( + int maxUnusedAddressGap, + int maxNumberOfIndexesToCheck, + ) async { + Logging.instance.log("Starting full rescan!", level: LogLevel.Info); + longMutex = true; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + // clear cache + await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); + + // back up data + await _rescanBackup(); + + try { + final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); + await _recoverWalletFromBIP32SeedPhrase( + mnemonic: mnemonic!, + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, + ); + + longMutex = false; + Logging.instance.log("Full rescan complete!", level: LogLevel.Info); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + } catch (e, s) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + + // restore from backup + await _rescanRestore(); + + longMutex = false; + Logging.instance.log("Exception rethrown from fullRescan(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _rescanRestore() async { + Logging.instance.log("starting rescan restore", level: LogLevel.Info); + + // restore from backup + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2PKH_BACKUP'); + final tempChangeAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH_BACKUP'); + final tempReceivingIndexP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2PKH_BACKUP'); + final tempChangeIndexP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeIndexP2PKH_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: tempReceivingAddressesP2PKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: tempChangeAddressesP2PKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: tempReceivingIndexP2PKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2PKH', + value: tempChangeIndexP2PKH); + await DB.instance.delete<dynamic>( + key: 'receivingAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2PKH_BACKUP', boxName: walletId); + + // p2wpkh + final tempReceivingAddressesP2WPKH = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2WPKH_BACKUP'); + final tempChangeAddressesP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2WPKH_BACKUP'); + final tempReceivingIndexP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2WPKH_BACKUP'); + final tempChangeIndexP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeIndexP2WPKH_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2WPKH', + value: tempReceivingAddressesP2WPKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2WPKH', + value: tempChangeAddressesP2WPKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH', + value: tempReceivingIndexP2WPKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2WPKH', + value: tempChangeIndexP2WPKH); + await DB.instance.delete<dynamic>( + key: 'receivingAddressesP2WPKH_BACKUP', boxName: walletId); + await DB.instance.delete<dynamic>( + key: 'changeAddressesP2WPKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2WPKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(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"); + + // 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<dynamic>(boxName: walletId, key: 'latest_utxo_model_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_utxo_model', value: utxoData); + await DB.instance + .delete<dynamic>(key: 'latest_utxo_model_BACKUP', boxName: walletId); + + Logging.instance.log("rescan restore complete", level: LogLevel.Info); + } + + Future<void> _rescanBackup() async { + Logging.instance.log("starting rescan backup", level: LogLevel.Info); + + // backup current and clear data + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2PKH_BACKUP', + value: tempReceivingAddressesP2PKH); + await DB.instance + .delete<dynamic>(key: 'receivingAddressesP2PKH', boxName: walletId); + + final tempChangeAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2PKH_BACKUP', + value: tempChangeAddressesP2PKH); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2PKH', boxName: walletId); + + final tempReceivingIndexP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'receivingIndexP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2PKH_BACKUP', + value: tempReceivingIndexP2PKH); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2PKH', boxName: walletId); + + final tempChangeIndexP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeIndexP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2PKH_BACKUP', + value: tempChangeIndexP2PKH); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2PKH', boxName: walletId); + + // p2wpkh + final tempReceivingAddressesP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2WPKH_BACKUP', + value: tempReceivingAddressesP2WPKH); + await DB.instance + .delete<dynamic>(key: 'receivingAddressesP2WPKH', boxName: walletId); + + final tempChangeAddressesP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2WPKH_BACKUP', + value: tempChangeAddressesP2WPKH); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2WPKH', boxName: walletId); + + final tempReceivingIndexP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH_BACKUP', + value: tempReceivingIndexP2WPKH); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2WPKH', boxName: walletId); + + final tempChangeIndexP2WPKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeIndexP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2WPKH_BACKUP', + value: tempChangeIndexP2WPKH); + await DB.instance + .delete<dynamic>(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"); + + // 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<dynamic>(boxName: walletId, key: 'latest_utxo_model'); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_utxo_model_BACKUP', value: utxoData); + await DB.instance + .delete<dynamic>(key: 'latest_utxo_model', boxName: walletId); + + Logging.instance.log("rescan backup complete", level: LogLevel.Info); + } + + bool isActive = false; + + @override + void Function(bool)? get onIsActiveWalletChanged => + (isActive) => this.isActive = isActive; + + @override + Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); + + 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<bool> generateNewAddress() async { + try { + await _incrementAddressIndexForChain( + 0, DerivePathType.bip84); // First increment the receiving index + final newReceivingIndex = DB.instance.get<dynamic>( + 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); diff --git a/lib/services/price.dart b/lib/services/price.dart index 2514cc12a..43751c141 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -87,7 +87,7 @@ class PriceAPI { Map<Coin, Tuple2<Decimal, double>> result = {}; try { final uri = Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&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=${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"); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 65303b24d..a6cbb8b58 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: diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 27c8fe3b4..b02c6584e 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -180,6 +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/Particl.svg"; String get chevronRight => "assets/svg/chevron-right.svg"; String get minimize => "assets/svg/minimize.svg"; @@ -192,6 +193,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 +217,8 @@ class _SVG { return wownero; case Coin.namecoin: return namecoin; + case Coin.particl: + return particl; case Coin.bitcoinTestNet: return bitcoinTestnet; case Coin.bitcoincashTestnet: @@ -241,6 +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/particl.png"; String get glasses => "assets/images/glasses.png"; String get glassesHidden => "assets/images/glasses-hidden.png"; @@ -271,6 +277,8 @@ class _PNG { return wownero; case Coin.namecoin: return namecoin; + case Coin.particl: + return particl; } } } diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index 69afb4a83..4b406b704 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -35,5 +35,7 @@ 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"); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 3263d526e..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: @@ -103,6 +105,7 @@ abstract class Constants { case Coin.firoTestNet: case Coin.epicCash: case Coin.namecoin: + case Coin.particl: values.addAll([24, 21, 18, 15, 12]); break; @@ -150,6 +153,9 @@ abstract class Constants { case Coin.namecoin: return 600; + + case Coin.particl: + return 600; } } diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index c9e96fbac..f4eb41b7c 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, @@ -145,6 +146,17 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get particl => NodeModel( + host: "particl.stackwallet.com", + port: 58002, + name: defaultName, + id: _nodeId(Coin.particl), + useSSL: true, + enabled: true, + coinName: Coin.particl.name, + isFailover: true, + isDown: false); + static NodeModel get bitcoinTestnet => NodeModel( host: "electrumx-testnet.cypherstack.com", port: 51002, @@ -222,6 +234,9 @@ abstract class DefaultNodes { case Coin.namecoin: return namecoin; + case Coin.particl: + return particl; + case Coin.bitcoinTestNet: return bitcoinTestnet; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 543a193ee..e90509305 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -12,7 +12,11 @@ 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; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/services/coins/particl/particl_wallet.dart' + as particl; enum Coin { bitcoin, @@ -23,6 +27,7 @@ enum Coin { litecoin, monero, namecoin, + particl, wownero, /// @@ -56,6 +61,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: @@ -89,6 +96,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: @@ -123,6 +132,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: @@ -148,6 +159,7 @@ 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: @@ -190,6 +202,9 @@ extension CoinExt on Coin { case Coin.monero: return xmr.MINIMUM_CONFIRMATIONS; + case Coin.particl: + return particl.MINIMUM_CONFIRMATIONS; + case Coin.wownero: return wow.MINIMUM_CONFIRMATIONS; @@ -230,6 +245,10 @@ Coin coinFromPrettyName(String name) { case "monero": return Coin.monero; + case "Particl": + case "particl": + return Coin.particl; + case "Namecoin": case "namecoin": return Coin.namecoin; @@ -293,8 +312,12 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.monero; case "nmc": return Coin.namecoin; + case "part": + return Coin.particl; case "tltc": return Coin.litecoinTestNet; + case "part": + return Coin.particl; case "tbtc": return Coin.bitcoinTestNet; case "tbch": diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index e8b491e71..43012ffe6 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -220,6 +220,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(0xFF8175BD); Color forCoin(Coin coin) { switch (coin) { @@ -246,6 +247,8 @@ class CoinThemeColor { return namecoin; case Coin.wownero: return wownero; + case Coin.particl: + return particl; } } } diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 935fa03ae..9764128e4 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -1443,6 +1443,8 @@ class StackColors extends ThemeExtension<StackColors> { return _coin.namecoin; case Coin.wownero: return _coin.wownero; + case Coin.particl: + return _coin.particl; } } diff --git a/pubspec.lock b/pubspec.lock index ffd644d05..ecc0bdf8d 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: "65eb920719c8f7895c5402a07497647e7fc4b346" - 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: @@ -253,7 +253,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: @@ -372,7 +372,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: @@ -456,7 +456,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "5.2.1" + version: "5.2.3" fixnum: dependency: transitive description: @@ -543,7 +543,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: @@ -585,35 +585,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: @@ -627,7 +627,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 @@ -670,7 +670,7 @@ packages: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" hex: dependency: transitive description: @@ -712,7 +712,7 @@ packages: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: "direct main" description: @@ -836,7 +836,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: @@ -885,14 +885,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: @@ -920,7 +920,7 @@ packages: name: mutex url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" nm: dependency: transitive description: @@ -1018,7 +1018,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: @@ -1060,7 +1060,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: @@ -1144,7 +1144,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: @@ -1172,7 +1172,7 @@ packages: name: rational url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.2" riverpod: dependency: transitive description: @@ -1200,7 +1200,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: @@ -1228,7 +1228,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: @@ -1326,7 +1326,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 @@ -1403,7 +1403,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: @@ -1515,14 +1515,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: @@ -1571,7 +1571,7 @@ packages: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.6" + version: "3.0.7" vector_math: dependency: transitive description: @@ -1655,7 +1655,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 2397fbfbc..81f1a9e9a 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.24+97 environment: sdk: ">=2.17.0 <3.0.0" @@ -49,7 +49,7 @@ dependencies: bitcoindart: git: url: https://github.com/cypherstack/bitcoindart.git - ref: 65eb920719c8f7895c5402a07497647e7fc4b346 + ref: 004d6f82dff7389b561e5078b4649adcd2d9c10f stack_wallet_backup: git: @@ -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 @@ -310,6 +311,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 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 7a725d65c..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 --upgrade +#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 diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh index d24076666..b9ab666a9 100755 --- a/scripts/prebuild.sh +++ b/scripts/prebuild.sh @@ -2,5 +2,17 @@ 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 +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 + 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 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 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/price_test.dart b/test/price_test.dart index 89300122e..6b98b67d1 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: [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&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: [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( 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); @@ -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: [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 { @@ -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( @@ -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 { 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 new file mode 100644 index 000000000..048dc0b1f --- /dev/null +++ b/test/services/coins/particl/particl_history_sample_data.dart @@ -0,0 +1,152 @@ +final Map<String, List<dynamic>> historyBatchArgs0 = { + "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": [ + "aedac6f5e8f0e96c7a53d9b0460ba9e9397efbd9d15c46a28d7b0be70ffc6dac" + ], + "k_0_11": ["f66b687065339e2d4d567f9ea473947b8aab74c066bf00cdfdb5f918bbd159dc"] +}; + +final Map<String, List<dynamic>> historyBatchArgs1 = { + "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": [ + "f07285fe3a8eac625b2c5339cf9f068fccb2278f923a38a46a60c94c7179d4aa" + ], + "k_0_11": ["f8f09b8fe23da8435409c3e688002dcaa87c2b9f3707e17bc668db7392039dab"] +}; + +final Map<String, List<dynamic>> historyBatchArgs2 = { + "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": [ + "e04e7d94a880ccbd8ce473ce5e780fd86003137cef1e879e38971e4216a282e1" + ], + "k_0_11": ["f6a7b80c32f2568bebe37d6615ebfa602ec04207cd9edf304ff7f835b03c27d2"] +}; + +final Map<String, List<dynamic>> historyBatchArgs3 = { + "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": [ + "0c6c028ede10b0c3180e9541675c16b72f4443663dd7dbe9b45037b230d55917" + ], + "k_0_11": ["eb1ebeefa4bc5f754daabb0f783f3685bd839429ee0a287bd26d8717265c3d27"] +}; + +final Map<String, List<Map<String, dynamic>>> 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<String, List<Map<String, dynamic>>> 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<String> activeScriptHashes = [ + "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339", + "b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3", + "0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a", + "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" +]; 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..e90a90263 --- /dev/null +++ b/test/services/coins/particl/particl_transaction_data_samples.dart @@ -0,0 +1,372 @@ +import 'package:stackwallet/models/paymint/transactions_model.dart'; + +final transactionData = TransactionData.fromMap({ + "a51831f09072dc9edb3130f677a484ca03bced8f6d803e8df83a1ed84bc06c0a": tx1, + "39a9c37d54d04f9ac6ed45aaa1a02b058391b5d1fc0e2e1d67e50f36b1d82896": tx2, + "e53ef367a5f9d8493825400a291136870ea24a750f63897f559851ab80ea1020": tx3, + "10e14b1d34c18a563b476c4c36688eb7caebf6658e25753074471d2adef460ba": tx4, +}); + +final tx1 = Transaction( + txid: "a51831f09072dc9edb3130f677a484ca03bced8f6d803e8df83a1ed84bc06c0a", + confirmedStatus: true, + confirmations: 15447, + txType: "Received", + amount: 10000000, + fees: 53600, + height: 1299909, + address: "PtQCgwUx9mLmRDWxB3J7MPnNsWDcce7a5g", + timestamp: 1667814832, + worthNow: "0.00", + worthAtBlockTimestamp: "0.00", + inputSize: 2, + outputSize: 2, + inputs: [ + Input( + txid: "e53ef367a5f9d8493825400a291136870ea24a750f63897f559851ab80ea1020", + vout: 1, + ), + Input( + txid: "b255bf1b4b2f1a76eab45fd69e589b655261b049f238807b0acbf304d1b8195b", + vout: 0, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "PtQCgwUx9mLmRDWxB3J7MPnNsWDcce7a5g", + value: 10000000, + ), + Output( + scriptpubkeyAddress: "PsHtVuRCybcTpJQN6ckLFptPB7k9ZkqztA", + value: 9946400, + ) + ], +); + +final tx2 = Transaction( + txid: "39a9c37d54d04f9ac6ed45aaa1a02b058391b5d1fc0e2e1d67e50f36b1d82896", + confirmedStatus: true, + confirmations: 13927, + txType: "Sent", + amount: 50000000, + fees: 49500, + height: 1301433, + address: "PcKLXor8hqb3qSjtoHQThapJSbPapSDt4C", + timestamp: 1668010880, + worthNow: "0.00", + worthAtBlockTimestamp: "0.00", + inputSize: 1, + outputSize: 2, + inputs: [ + Input( + txid: "909bdf555736c272df0e1df52ca5fcce4f1090b74c0e5d9319bb40e02f4b3add", + vout: 1, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "PcKLXor8hqb3qSjtoHQThapJSbPapSDt4C", + value: 50000000, + ), + Output( + 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: "10e14b1d34c18a563b476c4c36688eb7caebf6658e25753074471d2adef460ba", + confirmedStatus: true, + confirmations: 493, + txType: "Sent", + amount: 9945773, + fees: 27414, + height: 1314873, + address: "PpqgMahyfqfasunUKfkmVfdpyhhrHa2ibY", + timestamp: 1669740960, + worthNow: "0.00", + worthAtBlockTimestamp: "0.00", + inputSize: 1, + outputSize: 1, + inputs: [ + Input( + txid: "aab01876c4db40b35ba00bbfb7c58aaec32cad7dc136214b7344a944606cbe73", + vout: 0, + ), + ], + outputs: [ + Output( + scriptpubkeyAddress: "PpqgMahyfqfasunUKfkmVfdpyhhrHa2ibY", + value: 9945773, + ), + ], +); + +final tx1Raw = { + "txid": "a51831f09072dc9edb3130f677a484ca03bced8f6d803e8df83a1ed84bc06c0a", + "hash": "46b7358ccbc018da4e144188f311657e8b694f056211d7511726c4259dca86b4", + "size": 374, + "vsize": 267, + "version": 160, + "locktime": 1299908, + "vin": [ + { + "txid": + "e53ef367a5f9d8493825400a291136870ea24a750f63897f559851ab80ea1020", + "vout": 1, + "scriptSig": {"asm": "", "hex": ""}, + "txinwitness": [ + "30440220336bf0952b543314ba37b1bb8866a65b2482b499c715d778e92e90d7d59c6a39022072cae4341ca8825bee8043ae91f18de5776edd069ed228142eca55a16c887d6b01", + "026b4ca62de9e8f63abd0a6cf176536fe8e6a64d6343b6396aa9fb35232520e4a7" + ], + "sequence": 4294967293 + }, + { + "txid": + "b255bf1b4b2f1a76eab45fd69e589b655261b049f238807b0acbf304d1b8195b", + "vout": 0, + "scriptSig": {"asm": "", "hex": ""}, + "txinwitness": [ + "304402205b914f31952958d54f0290d47eef6d9042259387c9493993882e24bd9acefe00022066b16f2f41885a85051c9bff4c119ecddc0209520e9a93d75866624f11b4e82d01", + "026b4ca62de9e8f63abd0a6cf176536fe8e6a64d6343b6396aa9fb35232520e4a7" + ], + "sequence": 4294967293 + } + ], + "vout": [ + { + "n": 0, + "type": "standard", + "value": 0.1, + "valueSat": 10000000, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 e0923d464a2c30438f0808e4af94868253b63ca0 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914e0923d464a2c30438f0808e4af94868253b63ca088ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["PtQCgwUx9mLmRDWxB3J7MPnNsWDcce7a5g"] + } + }, + { + "n": 1, + "type": "standard", + "value": 0.099464, + "valueSat": 9946400, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 d4686eee8cd127b50d28869627d61b38cc63fe4a OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914d4686eee8cd127b50d28869627d61b38cc63fe4a88ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["PsHtVuRCybcTpJQN6ckLFptPB7k9ZkqztA"] + } + } + ], + "blockhash": + "b7cb29eb9cb4fa73c4da32f5cf8dfd90194eb6b689d4e547fa9b3176a698a741", + "height": 1299909, + "confirmations": 15447, + "time": 1667814832, + "blocktime": 1667814832 +}; + +final tx2Raw = { + "txid": "39a9c37d54d04f9ac6ed45aaa1a02b058391b5d1fc0e2e1d67e50f36b1d82896", + "hash": "85130125ec9e37a48670fb5eb0a2780b94ea958cd700a1237ff75775d8a0edb0", + "size": 226, + "vsize": 173, + "version": 160, + "locktime": 1301432, + "vin": [ + { + "txid": + "909bdf555736c272df0e1df52ca5fcce4f1090b74c0e5d9319bb40e02f4b3add", + "vout": 1, + "scriptSig": {"asm": "", "hex": ""}, + "txinwitness": [ + "30440220486c87376122e2d3ca7154f41a45fdafa2865412ec90e4b3db791915eee1d13002204cca8520a655b43c3cddc216725cc8508cd9b326a39ed99ed893be59167289af01", + "03acc7ad6e2e9560db73f7ec7ef2f55a6115d85069cf0eacfe3ab663f33415573c" + ], + "sequence": 4294967293 + } + ], + "vout": [ + { + "n": 0, + "type": "standard", + "value": 0.5, + "valueSat": 50000000, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 3024b192883be45b197b548f71155829af980724 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9143024b192883be45b197b548f71155829af98072488ac", + "reqSigs": 1, + "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"] + } + } + ], + "blockhash": + "065c7328f1a768f3005ab7bfb322806bcc0cf88a96e89830b44991cc434c9955", + "height": 1301433, + "confirmations": 13927, + "time": 1668010880, + "blocktime": 1668010880 +}; + +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": "10e14b1d34c18a563b476c4c36688eb7caebf6658e25753074471d2adef460ba", + "hash": "cb0d83958db55c91fb9cd9cab65ee516e63aea68ae5650a692918779ceb46576", + "size": 191, + "vsize": 138, + "version": 160, + "locktime": 1314871, + "vin": [ + { + "txid": + "aab01876c4db40b35ba00bbfb7c58aaec32cad7dc136214b7344a944606cbe73", + "vout": 0, + "scriptSig": {"asm": "", "hex": ""}, + "txinwitness": [ + "304402202e33ab9c5bb6a50c24de9ebfd1b2f398b4c9027787fb9620fda515a25b62ffcf02205e8371aeeda3b3765fa1e2a5c7ebce5dffbf18932012670c1f5266992f9ed9c901", + "039ca6c697fed4daf1697f137e7d5b113ff7b6c48ea48d707addd9cfa51889a42a" + ], + "sequence": 4294967293 + } + ], + "vout": [ + { + "n": 0, + "type": "standard", + "value": 0.09945773, + "valueSat": 9945773, + "scriptPubKey": { + "asm": + "OP_DUP OP_HASH160 b9833ad924ab05567ea2b679a5c523c66a1da6d7 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914b9833ad924ab05567ea2b679a5c523c66a1da6d788ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": ["PpqgMahyfqfasunUKfkmVfdpyhhrHa2ibY"] + } + } + ], + "blockhash": + "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 new file mode 100644 index 000000000..57adb0357 --- /dev/null +++ b/test/services/coins/particl/particl_utxo_sample_data.dart @@ -0,0 +1,58 @@ +import 'package:stackwallet/models/paymint/utxo_model.dart'; + +final Map<String, List<Map<String, dynamic>>> batchGetUTXOResponse0 = { + "some id 0": [ + { + "tx_pos": 0, + "value": 9973187, + "tx_hash": + "7b932948c95cf483798011da3fc77b6d53ee26d3d2ba4d90748cd007bdce48e8", + "height": 1314869 + }, + { + "tx_pos": 0, + "value": 50000000, + "tx_hash": + "aae9e712e26e5ff77ac2258c47a845ad6e952d580c2ad805e2b5d7667f3d4e42", + "height": 1297229 + }, + ], + "some id 1": [], +}; + +final utxoList = [ + UtxoObject( + txid: "aab01876c4db40b35ba00bbfb7c58aaec32cad7dc136214b7344a944606cbe73", + vout: 0, + status: Status( + confirmed: true, + confirmations: 516, + blockHeight: 1314869, + blockTime: 1669740688, + blockHash: + "6146005e4b21b72d0e2afe5b0cce3abd6e9e9e71c6cf6a1e1150d33e33ba81d4", + ), + value: 9973187, + fiatWorth: "\$0", + txName: "pw1qj6t0kvsmx8qd95pdh4rwxaz5qp5qtfz0xq2rja", + blocked: false, + isCoinbase: false, + ), + UtxoObject( + txid: "909bdf555736c272df0e1df52ca5fcce4f1090b74c0e5d9319bb40e02f4b3add", + vout: 0, + status: Status( + confirmed: true, + confirmations: 18173, + blockHeight: 1297229, + blockTime: 1667469296, + blockHash: + "5c5c1a4e2d9cc77a1df4337359f901c92bb4907cff85312599b06141fd1d96d9", + ), + value: 50000000, + fiatWorth: "\$0", + 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 new file mode 100644 index 000000000..bbe34a5ec --- /dev/null +++ b/test/services/coins/particl/particl_wallet_test.dart @@ -0,0 +1,1664 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx.dart'; +import 'package:stackwallet/models/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 '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, + 1); // TODO confirm particl minimum confirmations + }); + test("particl dust limit", () async { + expect(DUST_LIMIT, 294); // TODO confirm particl dust limit + }); + test("particl mainnet genesis block hash", () async { + expect(GENESIS_HASH_MAINNET, + "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"); + }); + test("particl testnet genesis block hash", () async { + expect(GENESIS_HASH_TESTNET, + "0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90"); + }); + }); + + test("particl DerivePathType enum", () { + expect(DerivePathType.values.length, 2); + expect(DerivePathType.values.toString(), + "[DerivePathType.bip44, DerivePathType.bip84]"); + }); + + group("bip32 node/root", () { + test("getBip32Root", () { + final root = getBip32Root(TEST_MNEMONIC, particl); + expect(root.toWIF(), ROOT_WIF); + }); + + // test("getBip32NodeFromRoot", () { + // final root = getBip32Root(TEST_MNEMONIC, particl); + // // 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); + // }); + //TODO Testnet not setup + // test("basic getBip32Node", () { + // final node = + // getBip32Node(0, 0, TEST_MNEMONIC, testnet, DerivePathType.bip84); + // expect(node.toWIF(), NODE_WIF_84); + // }); + }); + + group("validate mainnet particl addresses", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; + + ParticlWallet? + mainnetWallet; // TODO reimplement testnet, see 9baa30c1a40b422bb5f4746efc1220b52691ace6 and sneurlax/stack_wallet#ec399ade0aef1d9ab2dd78876a2d20819dae4ba0 + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + mainnetWallet = ParticlWallet( + walletId: "validateAddressMainNet", + walletName: "validateAddressMainNet", + coin: Coin.particl, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + 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: "Pi9W46PhXkNRusar2KVMbXftYpGzEYGcSa"), + DerivePathType.bip44); + expect(secureStore.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet particl p2wpkh address", () { + expect( + 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("pw1qce3dhmmle4e0833mssj7ptta3ehydjf0tsa3ju"), + 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("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; + + ParticlWallet? part; + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + 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 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 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 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"; + final testWalletName = "ParticlWallet"; + + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + late FakeSecureStorage secureStore; + MockTransactionNotificationTracker? tracker; + + ParticlWallet? part; + + setUp(() async { + 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("get networkType main", () async { + 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 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); + part?.walletName = "new name"; + expect(part?.walletName, "new name"); + 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<FeeObject>()); + 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); + // 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 part?.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"; + 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, + ); + }); + //TODO - THis function definition has changed, possibly remove + // 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 Hive.openBox(testWalletId); + + await expectLater( + () => part?.initializeExisting(), throwsA(isA<Exception>())) + .then((_) { + expect(secureStore.interactions, 0); + 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<Exception>())) + .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 { + 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); + + List<dynamic> 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<dynamic>(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); + + for (final arg in dynamicArgValues) { + final map = Map<String, List<dynamic>>.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); + expect(secureStore.writes, 5); + expect(secureStore.reads, 5); + 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(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .thenAnswer((realInvocation) async {}); + + 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": []}); + + final wallet = await Hive.openBox<dynamic>(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 preReceivingAddressesP2WPKH = + await wallet.get('receivingAddressesP2WPKH'); + final preChangeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); + final preChangeAddressesP2WPKH = + await wallet.get('changeAddressesP2WPKH'); + final preReceivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); + final preReceivingIndexP2WPKH = await wallet.get('receivingIndexP2WPKH'); + final preChangeIndexP2PKH = await wallet.get('changeIndexP2PKH'); + 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 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( + 'receivingAddressesP2WPKH', ["some address", "some other address"]); + await wallet + .put('changeAddressesP2PKH', ["some address", "some other address"]); + await wallet + .put('changeAddressesP2WPKH', ["some address", "some other address"]); + await wallet.put('receivingIndexP2PKH', 123); + await wallet.put('receivingIndexP2WPKH', 123); + await wallet.put('changeIndexP2PKH', 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}_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 receivingAddressesP2WPKH = + await wallet.get('receivingAddressesP2WPKH'); + final changeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); + final changeAddressesP2WPKH = await wallet.get('changeAddressesP2WPKH'); + final receivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); + final receivingIndexP2WPKH = await wallet.get('receivingIndexP2WPKH'); + final changeIndexP2PKH = await wallet.get('changeIndexP2PKH'); + 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 receiveDerivationsStringP2WPKH = await secureStore.read( + key: "${testWalletId}_receiveDerivationsP2WPKH"); + final changeDerivationsStringP2WPKH = await secureStore.read( + key: "${testWalletId}_changeDerivationsP2WPKH"); + + expect(preReceivingAddressesP2PKH, receivingAddressesP2PKH); + expect(preReceivingAddressesP2WPKH, receivingAddressesP2WPKH); + expect(preChangeAddressesP2PKH, changeAddressesP2PKH); + expect(preChangeAddressesP2WPKH, changeAddressesP2WPKH); + expect(preReceivingIndexP2PKH, receivingIndexP2PKH); + expect(preReceivingIndexP2WPKH, receivingIndexP2WPKH); + expect(preChangeIndexP2PKH, changeIndexP2PKH); + expect(preChangeIndexP2WPKH, changeIndexP2WPKH); + expect(preUtxoData, utxoData); + expect(preReceiveDerivationsStringP2PKH, receiveDerivationsStringP2PKH); + expect(preChangeDerivationsStringP2PKH, changeDerivationsStringP2PKH); + 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: { + "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": [ + "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" + ] + })).called(2); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .called(1); + + expect(secureStore.writes, 17); + expect(secureStore.reads, 22); + expect(secureStore.deletes, 4); + + 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: { + "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<dynamic>(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")); + 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")); + + 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: { + "0": [ + "3fedd8a2d5fc355727afe353413dc1a0ef861ba768744d5b8193c33cbc829339" + ] + })).called(1); + verify(client?.getBatchHistory(args: { + "0": [ + "b6fce6c41154ccf70676c5c91acd9b6899ef0195e34b4c05c4920daa827c19a3" + ] + })).called(1); + verify(client?.getBatchHistory(args: { + "0": [ + "0e8b6756b404db5a381fd71ad79cb595a6c36c938cf9913c5a0494b667c2151a" + ] + })).called(1); + verify(client?.getBatchHistory(args: { + "0": [ + "c4b1d9cd4edb7c13eae863b1e4f8fd5acff29f1fe153c4f859906cbea26a3f2f" + ] + })).called(1); + verify(cachedClient?.clearSharedTransactionCache(coin: Coin.particl)) + .called(1); + + expect(secureStore.writes, 13); + expect(secureStore.reads, 26); + expect(secureStore.deletes, 8); + + 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); + + List<dynamic> 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<dynamic>(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<String, List<dynamic>>.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; + try { + await part + ?.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 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 + // // which what we want here as actually calling electrumx calls here is unwanted. + // // test("listen to NodesChangedEvent", () async { + // // nmc = ParticlWallet( + // // 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); + List<dynamic> 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<dynamic>(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); + + for (final arg in dynamicArgValues) { + final map = Map<String, List<dynamic>>.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); + expect(secureStore.writes, 5); + expect(secureStore.reads, 5); + 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<dynamic> dynamicArgValues = []; + + when(client?.getBatchHistory(args: anyNamed("args"))) + .thenAnswer((realInvocation) async { + dynamicArgValues.add(realInvocation.namedArguments.values.first); + return historyBatchResponse; + }); + + await Hive.openBox<dynamic>(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(3); + 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<String, List<dynamic>>.from(arg as Map); + + verify(client?.getBatchHistory(args: map)).called(1); + } + + expect(secureStore.interactions, 10); + expect(secureStore.writes, 5); + expect(secureStore.reads, 5); + expect(secureStore.deletes, 0); + + 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..fb0f50d79 --- /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/particl/particl_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<dynamic> request({ + required String? command, + List<dynamic>? 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<dynamic>.value(), + ) as _i6.Future<dynamic>); + @override + _i6.Future<List<Map<String, dynamic>>> batchRequest({ + required String? command, + required Map<String, List<dynamic>>? 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<List<Map<String, dynamic>>>.value( + <Map<String, dynamic>>[]), + ) as _i6.Future<List<Map<String, dynamic>>>); + @override + _i6.Future<bool> ping({ + String? requestID, + int? retryCount = 1, + }) => + (super.noSuchMethod( + Invocation.method( + #ping, + [], + { + #requestID: requestID, + #retryCount: retryCount, + }, + ), + returnValue: _i6.Future<bool>.value(false), + ) as _i6.Future<bool>); + @override + _i6.Future<Map<String, dynamic>> getBlockHeadTip({String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getBlockHeadTip, + [], + {#requestID: requestID}, + ), + returnValue: + _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), + ) as _i6.Future<Map<String, dynamic>>); + @override + _i6.Future<Map<String, dynamic>> getServerFeatures({String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getServerFeatures, + [], + {#requestID: requestID}, + ), + returnValue: + _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), + ) as _i6.Future<Map<String, dynamic>>); + @override + _i6.Future<String> broadcastTransaction({ + required String? rawTx, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #broadcastTransaction, + [], + { + #rawTx: rawTx, + #requestID: requestID, + }, + ), + returnValue: _i6.Future<String>.value(''), + ) as _i6.Future<String>); + @override + _i6.Future<Map<String, dynamic>> getBalance({ + required String? scripthash, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getBalance, + [], + { + #scripthash: scripthash, + #requestID: requestID, + }, + ), + returnValue: + _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), + ) as _i6.Future<Map<String, dynamic>>); + @override + _i6.Future<List<Map<String, dynamic>>> getHistory({ + required String? scripthash, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getHistory, + [], + { + #scripthash: scripthash, + #requestID: requestID, + }, + ), + returnValue: _i6.Future<List<Map<String, dynamic>>>.value( + <Map<String, dynamic>>[]), + ) as _i6.Future<List<Map<String, dynamic>>>); + @override + _i6.Future<Map<String, List<Map<String, dynamic>>>> getBatchHistory( + {required Map<String, List<dynamic>>? args}) => + (super.noSuchMethod( + Invocation.method( + #getBatchHistory, + [], + {#args: args}, + ), + returnValue: _i6.Future<Map<String, List<Map<String, dynamic>>>>.value( + <String, List<Map<String, dynamic>>>{}), + ) as _i6.Future<Map<String, List<Map<String, dynamic>>>>); + @override + _i6.Future<List<Map<String, dynamic>>> getUTXOs({ + required String? scripthash, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getUTXOs, + [], + { + #scripthash: scripthash, + #requestID: requestID, + }, + ), + returnValue: _i6.Future<List<Map<String, dynamic>>>.value( + <Map<String, dynamic>>[]), + ) as _i6.Future<List<Map<String, dynamic>>>); + @override + _i6.Future<Map<String, List<Map<String, dynamic>>>> getBatchUTXOs( + {required Map<String, List<dynamic>>? args}) => + (super.noSuchMethod( + Invocation.method( + #getBatchUTXOs, + [], + {#args: args}, + ), + returnValue: _i6.Future<Map<String, List<Map<String, dynamic>>>>.value( + <String, List<Map<String, dynamic>>>{}), + ) as _i6.Future<Map<String, List<Map<String, dynamic>>>>); + @override + _i6.Future<Map<String, dynamic>> getTransaction({ + required String? txHash, + bool? verbose = true, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getTransaction, + [], + { + #txHash: txHash, + #verbose: verbose, + #requestID: requestID, + }, + ), + returnValue: + _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), + ) as _i6.Future<Map<String, dynamic>>); + @override + _i6.Future<Map<String, dynamic>> getAnonymitySet({ + String? groupId = r'1', + String? blockhash = r'', + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getAnonymitySet, + [], + { + #groupId: groupId, + #blockhash: blockhash, + #requestID: requestID, + }, + ), + returnValue: + _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), + ) as _i6.Future<Map<String, dynamic>>); + @override + _i6.Future<dynamic> getMintData({ + dynamic mints, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getMintData, + [], + { + #mints: mints, + #requestID: requestID, + }, + ), + returnValue: _i6.Future<dynamic>.value(), + ) as _i6.Future<dynamic>); + @override + _i6.Future<Map<String, dynamic>> getUsedCoinSerials({ + String? requestID, + required int? startNumber, + }) => + (super.noSuchMethod( + Invocation.method( + #getUsedCoinSerials, + [], + { + #requestID: requestID, + #startNumber: startNumber, + }, + ), + returnValue: + _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), + ) as _i6.Future<Map<String, dynamic>>); + @override + _i6.Future<int> getLatestCoinId({String? requestID}) => (super.noSuchMethod( + Invocation.method( + #getLatestCoinId, + [], + {#requestID: requestID}, + ), + returnValue: _i6.Future<int>.value(0), + ) as _i6.Future<int>); + @override + _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getFeeRate, + [], + {#requestID: requestID}, + ), + returnValue: + _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), + ) as _i6.Future<Map<String, dynamic>>); + @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<Map<String, dynamic>> 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<Map<String, dynamic>>.value(<String, dynamic>{}), + ) as _i6.Future<Map<String, dynamic>>); + @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<Map<String, dynamic>> 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<Map<String, dynamic>>.value(<String, dynamic>{}), + ) as _i6.Future<Map<String, dynamic>>); + @override + _i6.Future<List<dynamic>> getUsedCoinSerials({ + required _i8.Coin? coin, + int? startNumber = 0, + }) => + (super.noSuchMethod( + Invocation.method( + #getUsedCoinSerials, + [], + { + #coin: coin, + #startNumber: startNumber, + }, + ), + returnValue: _i6.Future<List<dynamic>>.value(<dynamic>[]), + ) as _i6.Future<List<dynamic>>); + @override + _i6.Future<void> clearSharedTransactionCache({required _i8.Coin? coin}) => + (super.noSuchMethod( + Invocation.method( + #clearSharedTransactionCache, + [], + {#coin: coin}, + ), + returnValue: _i6.Future<void>.value(), + returnValueForMissingStub: _i6.Future<void>.value(), + ) as _i6.Future<void>); +} + +/// 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<Map<_i8.Coin, _i10.Tuple2<_i2.Decimal, double>>>.value( + <_i8.Coin, _i10.Tuple2<_i2.Decimal, double>>{}), + ) as _i6.Future<Map<_i8.Coin, _i10.Tuple2<_i2.Decimal, double>>>); +} + +/// 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<String> get pendings => (super.noSuchMethod( + Invocation.getter(#pendings), + returnValue: <String>[], + ) as List<String>); + @override + List<String> get confirmeds => (super.noSuchMethod( + Invocation.getter(#confirmeds), + returnValue: <String>[], + ) as List<String>); + @override + bool wasNotifiedPending(String? txid) => (super.noSuchMethod( + Invocation.method( + #wasNotifiedPending, + [txid], + ), + returnValue: false, + ) as bool); + @override + _i6.Future<void> addNotifiedPending(String? txid) => (super.noSuchMethod( + Invocation.method( + #addNotifiedPending, + [txid], + ), + returnValue: _i6.Future<void>.value(), + returnValueForMissingStub: _i6.Future<void>.value(), + ) as _i6.Future<void>); + @override + bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( + Invocation.method( + #wasNotifiedConfirmed, + [txid], + ), + returnValue: false, + ) as bool); + @override + _i6.Future<void> addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + Invocation.method( + #addNotifiedConfirmed, + [txid], + ), + returnValue: _i6.Future<void>.value(), + returnValueForMissingStub: _i6.Future<void>.value(), + ) as _i6.Future<void>); +} 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), ),