LCOV - code coverage report
Current view: top level - crypto/utxo - utxo_analyzer.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 90.9 % 309 281
Test Date: 2025-01-30 01:10:00 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:typed_data';
       2              : import 'package:collection/collection.dart';
       3              : import 'package:walletkit_dart/src/common/isolate_service.dart';
       4              : import 'package:walletkit_dart/src/common/logger.dart';
       5              : import 'package:walletkit_dart/src/crypto/utxo/entities/payments/p2h.dart';
       6              : import 'package:walletkit_dart/src/crypto/utxo/repositories/electrum_json_rpc_client.dart';
       7              : import 'package:walletkit_dart/src/crypto/utxo/utils/endpoint_utils.dart';
       8              : import 'package:walletkit_dart/src/crypto/utxo/entities/transactions/electrum_transaction.dart';
       9              : import 'package:bip32/bip32.dart' as bip32;
      10              : import 'package:walletkit_dart/walletkit_dart.dart';
      11              : 
      12              : const kAddressesUpperLimit = 10000;
      13              : const kEmptyLimit = 20;
      14              : 
      15              : /// Txs; Receive Addresses; Change Addresses
      16              : typedef UTXOTxInfo = (Set<UTXOTransaction>, Iterable<NodeWithAddress>);
      17              : 
      18              : typedef ElectrumXResult = (Set<ElectrumTransactionInfo>?, ElectrumXClient);
      19              : 
      20              : typedef SearchTransactionResult = (
      21              :   Set<ElectrumTransactionInfo>,
      22              :   Set<NodeWithAddress>
      23              : );
      24              : 
      25            7 : Future<UTXOTxInfo> fetchMissingUTXOTransactions({
      26              :   required Set<UTXOTransaction> cachedTransactions,
      27              :   required List<NodeWithAddress> cachedNodes,
      28              :   required Set<ElectrumTransactionInfo> allTxs,
      29              :   required Iterable<NodeWithAddress> nodes,
      30              :   required CoinEntity coin,
      31              :   required UTXONetworkType networkType,
      32              :   required Iterable<AddressType> addressTypes,
      33              :   required List<(String, int)> endpoints,
      34              :   required Stopwatch watch,
      35              : }) async {
      36              :   /// Filter out all transactions that are already cached
      37            7 :   final newTxs = allTxs.where(
      38            7 :     (tx) {
      39            7 :       final isCached = cachedTransactions.any((cTx) => cTx.id == tx.hash);
      40              : 
      41            7 :       return isCached == false;
      42              :     },
      43              :   );
      44              : 
      45            7 :   final pendingTxs = allTxs.where(
      46            7 :     (tx) {
      47            7 :       final cTx = cachedTransactions.singleWhereOrNull(
      48            0 :         (cTx) => cTx.id == tx.hash,
      49              :       );
      50              :       if (cTx == null) return false;
      51              : 
      52            0 :       return cTx.isPending || cTx is NotAvaialableUTXOTransaction;
      53              :     },
      54              :   );
      55           28 :   Logger.log("Found ${pendingTxs.length} pending TXs for ${coin.symbol}");
      56           28 :   Logger.log("Found ${newTxs.length} new TXs for ${coin.symbol}");
      57              : 
      58              :   ///
      59              :   /// Fetch UTXO Details for all new transactions
      60              :   ///
      61            7 :   var newUtxoTxs = await computeMissingUTXODetails(
      62              :     txList: newTxs,
      63              :     nodes: nodes,
      64              :     type: networkType,
      65              :     endpoints: endpoints,
      66              :     addressTypes: addressTypes,
      67              :   );
      68              : 
      69              :   ///
      70              :   /// Fetch Parent Transactions of P2SH Inputs to get the redeem script and in hence the address
      71              :   ///
      72            7 :   final unsupported = newUtxoTxs.where(
      73           21 :     (tx) => tx.sender == ADDRESS_NOT_SUPPORTED,
      74              :   );
      75            7 :   final updatedUTXOs = <UTXOTransaction>[];
      76           11 :   for (final tx in unsupported) {
      77            8 :     final firstInput = tx.inputs.firstOrNull;
      78              :     if (firstInput == null) continue;
      79              : 
      80              :     /// Coinbase Parent TX
      81            4 :     if (firstInput.isCoinbase) {
      82              :       continue;
      83              :     }
      84              : 
      85            4 :     final outputIndex = firstInput.vout!;
      86            4 :     final parentTxHash = firstInput.txid;
      87              :     if (parentTxHash == null) continue;
      88              : 
      89              :     // Check if Parent TX is already in the list
      90              :     var parentTx = newUtxoTxs
      91           20 :         .singleWhereOrNull((element) => element.id == firstInput.txid);
      92              : 
      93            4 :     final (_tx, _, _) = await fetchFromRandomElectrumXNode(
      94            4 :       (client) {
      95            4 :         return client.getTransaction(
      96              :           addressTypes: addressTypes,
      97              :           nodes: nodes,
      98              :           type: networkType,
      99              :           txHash: parentTxHash,
     100              :         );
     101              :       },
     102              :       client: null,
     103            4 :       endpoints: networkType.endpoints,
     104              :       token: coin,
     105              :     );
     106              : 
     107              :     parentTx ??= _tx;
     108              : 
     109              :     if (parentTx == null) {
     110            0 :       Logger.logWarning(
     111            0 :         "Parent TX $parentTxHash not found for ${tx.hash}",
     112              :       );
     113              :       continue;
     114              :     }
     115              : 
     116            8 :     final output = parentTx.outputs[outputIndex];
     117              : 
     118            4 :     final address = output.getAddress(networkType);
     119              : 
     120            8 :     updatedUTXOs.add(tx.copyWith(sender: address));
     121              :   }
     122              :   // Replace the unsupported transactions with the updated ones
     123           14 :   newUtxoTxs = newUtxoTxs.map((tx) {
     124            7 :     final updatedTx = updatedUTXOs.singleWhereOrNull(
     125           16 :       (upTx) => upTx.id == tx.id,
     126              :     );
     127              :     return updatedTx ?? tx;
     128              :   });
     129              : 
     130              :   ///
     131              :   /// Fetch UTXO Details for all pending transactions and replace them
     132              :   ///
     133            7 :   final pendingUtxoTxs = await computeMissingUTXODetails(
     134              :     txList: pendingTxs,
     135              :     nodes: nodes,
     136              :     type: networkType,
     137              :     endpoints: endpoints,
     138              :     addressTypes: addressTypes,
     139              :   );
     140              : 
     141            7 :   var utxoTXs = {
     142              :     ...newUtxoTxs,
     143            7 :     ...cachedTransactions,
     144              :   };
     145              : 
     146              :   /// Replace the pending transactions with the updated ones
     147           14 :   utxoTXs = utxoTXs.map((tx) {
     148            7 :     final pendingTx = pendingUtxoTxs.singleWhereOrNull(
     149            0 :       (element) => element.id == tx.id,
     150              :     );
     151              :     return pendingTx ?? tx;
     152            7 :   }).toSet();
     153              : 
     154              :   ///
     155              :   /// Mark Spent Outputs
     156              :   ///
     157           14 :   for (final tx in utxoTXs) {
     158           14 :     for (final input in tx.inputs) {
     159              :       final outputs = utxoTXs
     160           35 :           .singleWhereOrNull((element) => element.id == input.txid)
     161            7 :           ?.outputs;
     162              : 
     163            7 :       if (input.isCoinbase) continue;
     164            7 :       final index = input.vout!;
     165           14 :       if (outputs == null || outputs.length <= index) {
     166              :         continue;
     167              :       }
     168           21 :       outputs[index] = outputs[index].copyWith(spent: true);
     169              :     }
     170              :   }
     171              : 
     172            7 :   watch.stop();
     173           21 :   Logger.logFetch("Fetched TXs in ${watch.elapsed}");
     174              : 
     175           21 :   final sortedTxs = utxoTXs.sortedBy<GenericTransaction>((tx) => tx).toSet();
     176              : 
     177              :   return (sortedTxs, nodes);
     178              : }
     179              : 
     180              : /// Fetches UTXO Transactions for a given ePubKey
     181              : /// if [purpose] is not provied the returned [nodes] cant be used to derive the master node (used in Proof of Payment)
     182            4 : Future<UTXOTxInfo> fetchUTXOTransactionsFromEpubKey({
     183              :   required Iterable<AddressType> addressTypes,
     184              :   required UTXONetworkType networkType,
     185              :   required String ePubKey,
     186              :   required HDWalletPurpose purpose,
     187              :   Set<UTXOTransaction> cachedTransactions = const {},
     188              :   List<NodeWithAddress> cachedNodes = const [],
     189              :   int minEndpoints = 2,
     190              :   Duration maxLatency = const Duration(milliseconds: 800),
     191              : }) async {
     192            8 :   final watch = Stopwatch()..start();
     193              : 
     194            4 :   final endpoints = await getBestHealthEndpointsWithRetry(
     195            4 :     endpointPool: networkType.endpoints,
     196            4 :     token: networkType.coin,
     197              :     maxLatency: maxLatency,
     198              :     min: minEndpoints,
     199              :   );
     200              : 
     201            4 :   final isolateManager = IsolateManager();
     202              : 
     203              :   ///
     204              :   /// Search for Receive and Change Addresses
     205              :   ///
     206              : 
     207            4 :   final masterNode = await isolateManager.executeTask(
     208            4 :     IsolateTask(
     209            4 :       task: (arg) {
     210            4 :         return deriveMasterNodeFromExtendedKeyWithCheck(
     211              :           ePubKey: arg.$1,
     212              :           networkType: arg.$2,
     213              :           purpose: arg.$3,
     214              :         );
     215              :       },
     216              :       argument: (ePubKey, networkType, purpose),
     217              :     ),
     218              :   );
     219              : 
     220            4 :   final (allTxs, nodes) = await searchTransactionsForWalletType(
     221              :     masterNode: masterNode,
     222              :     purpose: purpose,
     223              :     addressTypes: addressTypes,
     224              :     networkType: networkType,
     225              :     endpoints: endpoints,
     226              :     cachedNodes: cachedNodes,
     227              :     isolateManager: isolateManager,
     228              :   );
     229              : 
     230            4 :   isolateManager.dispose();
     231              : 
     232            4 :   return fetchMissingUTXOTransactions(
     233              :     allTxs: allTxs,
     234              :     nodes: nodes,
     235              :     cachedNodes: cachedNodes,
     236              :     cachedTransactions: cachedTransactions,
     237              :     addressTypes: addressTypes,
     238            4 :     coin: networkType.coin,
     239              :     endpoints: endpoints,
     240              :     networkType: networkType,
     241              :     watch: watch,
     242              :   );
     243              : }
     244              : 
     245            5 : Future<UTXOTxInfo> fetchUTXOTransactions({
     246              :   required Iterable<HDWalletPath> walletTypes,
     247              :   required Iterable<AddressType> addressTypes,
     248              :   required UTXONetworkType networkType,
     249              :   required Uint8List seed,
     250              :   Set<UTXOTransaction> cachedTransactions = const {},
     251              :   List<NodeWithAddress> cachedNodes = const [],
     252              :   int minEndpoints = 2,
     253              :   Duration maxLatency = const Duration(milliseconds: 800),
     254              : }) async {
     255           10 :   final watch = Stopwatch()..start();
     256              : 
     257            5 :   final endpoints = await getBestHealthEndpointsWithRetry(
     258            5 :     endpointPool: networkType.endpoints,
     259            5 :     token: networkType.coin,
     260              :     maxLatency: maxLatency,
     261              :     min: minEndpoints,
     262              :   );
     263              : 
     264            5 :   print(
     265            5 :     "Selected ${endpoints.map(
     266           10 :       (e) => "$e",
     267            5 :     )}",
     268              :   );
     269              : 
     270            5 :   final isolateManager = IsolateManager();
     271              : 
     272              :   ///
     273              :   /// Search for Receive and Change Addresses
     274              :   ///
     275              : 
     276           10 :   final (allTxs, nodes) = await Future.wait([
     277            5 :     for (final walletType in walletTypes)
     278            5 :       () async {
     279            5 :         final masterNode = await isolateManager.executeTask(
     280            5 :           IsolateTask(
     281            5 :             task: (arg) {
     282            5 :               return deriveMasterNodeFromSeed(seed: arg.$1, walletPath: arg.$2);
     283              :             },
     284              :             argument: (seed, walletType),
     285              :           ),
     286              :         );
     287            5 :         return searchTransactionsForWalletType(
     288              :           masterNode: masterNode,
     289            5 :           purpose: walletType.purpose,
     290              :           addressTypes: addressTypes,
     291              :           networkType: networkType,
     292              :           endpoints: endpoints,
     293              :           cachedNodes: cachedNodes,
     294              :           isolateManager: isolateManager,
     295              :         );
     296            5 :       }.call()
     297           10 :   ]).then((value) => (
     298           15 :         value.expand((element) => element.$1).toSet(),
     299           15 :         value.expand((element) => element.$2).toSet()
     300              :       ));
     301              : 
     302            5 :   isolateManager.dispose();
     303              : 
     304            5 :   return fetchMissingUTXOTransactions(
     305              :     cachedTransactions: cachedTransactions,
     306              :     cachedNodes: cachedNodes,
     307              :     allTxs: allTxs,
     308              :     nodes: nodes,
     309            5 :     coin: networkType.coin,
     310              :     networkType: networkType,
     311              :     addressTypes: addressTypes,
     312              :     endpoints: endpoints,
     313              :     watch: watch,
     314              :   );
     315              : }
     316              : 
     317            7 : Future<(Set<ElectrumTransactionInfo>, Set<NodeWithAddress>)>
     318              :     searchTransactionsForWalletType({
     319              :   required BipNode masterNode,
     320              :   required HDWalletPurpose? purpose,
     321              :   required Iterable<AddressType> addressTypes,
     322              :   required UTXONetworkType networkType,
     323              :   required List<(String, int)> endpoints,
     324              :   required List<NodeWithAddress> cachedNodes,
     325              :   required IsolateManager isolateManager,
     326              : }) async {
     327            7 :   final receiveTxsFuture = searchForTransactions(
     328              :     masterNode: masterNode,
     329              :     chainIndex: EXTERNAL_CHAIN_INDEX,
     330              :     addressTypes: addressTypes,
     331              :     walletPurpose: purpose,
     332              :     networkType: networkType,
     333              :     endpoints: endpoints,
     334              :     cachedNodes: cachedNodes,
     335              :     isolateManager: isolateManager,
     336              :   );
     337              : 
     338            7 :   final changeTxsFuture = searchForTransactions(
     339              :     masterNode: masterNode,
     340              :     chainIndex: INTERNAL_CHAIN_INDEX,
     341              :     addressTypes: addressTypes,
     342              :     walletPurpose: purpose,
     343              :     networkType: networkType,
     344              :     endpoints: endpoints,
     345              :     cachedNodes: cachedNodes,
     346              :     isolateManager: isolateManager,
     347              :   );
     348              : 
     349           14 :   final allTxsResult = await Future.wait([receiveTxsFuture, changeTxsFuture]);
     350           21 :   final allTxs = allTxsResult.expand((element) => element.$1).toSet();
     351           21 :   final nodes = allTxsResult.expand((element) => element.$2).toSet();
     352              : 
     353              :   return (allTxs, nodes);
     354              : }
     355              : 
     356            7 : Future<(Set<ElectrumTransactionInfo>, List<NodeWithAddress>)>
     357              :     searchForTransactions({
     358              :   required bip32.BIP32 masterNode,
     359              :   required int chainIndex,
     360              :   required Iterable<AddressType> addressTypes,
     361              :   required HDWalletPurpose? walletPurpose,
     362              :   required UTXONetworkType networkType,
     363              :   required List<(String, int)> endpoints,
     364              :   required List<NodeWithAddress> cachedNodes,
     365              :   required IsolateManager isolateManager,
     366              :   int emptyLimit = kEmptyLimit,
     367              : }) async {
     368           14 :   final watch = Stopwatch()..start();
     369              : 
     370              :   int emptyCount = 0;
     371              : 
     372              :   final txs0 = <ElectrumTransactionInfo>{};
     373              : 
     374            7 :   final clients = await createClients(
     375              :     endpoints: endpoints,
     376            7 :     token: networkType.coin,
     377              :   );
     378            7 :   final batchSize = clients.length;
     379            7 :   final nodes = <NodeWithAddress>[];
     380              : 
     381            7 :   Logger.logFetch(
     382            7 :     "Fetching transactions from $batchSize ElectrumX Nodes",
     383              :   );
     384              : 
     385            7 :   if (batchSize == 0) {
     386            0 :     Logger.logWarning("No ElectrumX Nodes available for $networkType");
     387              :     return (txs0, nodes);
     388              :   }
     389              : 
     390           21 :   for (var index = 0; index * batchSize < kAddressesUpperLimit; index++) {
     391           28 :     final indexes = List.generate(batchSize, (i) => index * batchSize + i);
     392            7 :     final newIndexes = indexes.where(
     393           14 :       (i) => !cachedNodes.any(
     394            0 :         (cNode) => cNode.index == i && cNode.chainIndex == chainIndex,
     395              :       ),
     396              :     );
     397              : 
     398              :     final List<NodeWithAddress> newNodes;
     399            7 :     if (newIndexes.isEmpty) {
     400            0 :       newNodes = [];
     401              :     } else {
     402            7 :       newNodes = await isolateManager.executeTask(
     403            7 :         IsolateTask(
     404           14 :           task: (arg) => [
     405            7 :             for (var index in indexes)
     406            7 :               deriveChildNode(
     407              :                 masterNode: masterNode,
     408              :                 chainIndex: chainIndex,
     409              :                 index: index,
     410              :                 networkType: networkType,
     411              :                 addressTypes: addressTypes,
     412              :                 walletPurpose: walletPurpose,
     413              :               ),
     414              :           ],
     415              :           argument: null,
     416              :         ),
     417              :       );
     418              :     }
     419              : 
     420            7 :     final batchNodes = [
     421              :       ...newNodes,
     422            7 :       ...cachedNodes.where(
     423            0 :         (cNode) =>
     424            0 :             indexes.contains(cNode.index) && cNode.chainIndex == chainIndex,
     425              :       ),
     426              :     ];
     427              : 
     428            7 :     final futures = [
     429           14 :       for (int i = 0; i < batchSize; i++)
     430            7 :         fetchFromRandomElectrumXNode(
     431            7 :           (client) async {
     432            7 :             final txsInfos = [
     433            7 :               for (final type in addressTypes)
     434           21 :                 if (batchNodes[i].addresses.containsKey(type))
     435           14 :                   await client.getHistory(
     436           35 :                     P2Hash(batchNodes[i].addresses[type]!).publicKeyScriptHash,
     437              :                   )
     438              :             ];
     439              : 
     440              :             return txsInfos
     441           14 :                 .expand((e) => e ?? <ElectrumTransactionInfo>{})
     442            7 :                 .whereType<ElectrumTransactionInfo>()
     443            7 :                 .toSet();
     444              :           },
     445            7 :           endpoints: networkType.endpoints,
     446            7 :           client: clients[i],
     447            7 :           token: networkType.coin,
     448              :         ),
     449              :     ];
     450              : 
     451            7 :     final results = await Future.wait(futures);
     452              :     final batchTxs = <ElectrumTransactionInfo>{};
     453              : 
     454           14 :     for (int i = 0; i < batchSize; i++) {
     455            7 :       final (txs, client, error) = results[i];
     456              :       if (error != null) continue;
     457            7 :       if (txs == null || txs.isEmpty) continue;
     458            7 :       if (client != null) clients[i] = client;
     459              : 
     460            7 :       batchTxs.addAll(txs);
     461              :     }
     462              : 
     463            7 :     nodes.addAll(batchNodes);
     464              : 
     465            7 :     if (batchTxs.isEmpty) {
     466            7 :       emptyCount += batchSize;
     467            7 :       if (emptyCount >= kEmptyLimit) {
     468           21 :         Logger.log("Abort Search after ${index + batchSize} addresses");
     469              :         break;
     470              :       }
     471              :       continue;
     472              :     }
     473              : 
     474            7 :     txs0.addAll(batchTxs);
     475              :     emptyCount = 0;
     476              :   }
     477              : 
     478              :   ///
     479              :   /// Disconnect Clients
     480              :   ///
     481           14 :   await Future.wait([
     482           14 :     for (final client in clients) client.disconnect(),
     483              :   ]);
     484              : 
     485            7 :   watch.stop();
     486            7 :   Logger.logFetch(
     487           21 :     "Fetched ${txs0.length} TXs in ${watch.elapsed} $chainIndex",
     488              :   );
     489              : 
     490              :   return (txs0, nodes);
     491              : }
     492              : 
     493            1 : Future<Amount> estimateFeeForPriority({
     494              :   required int blocks,
     495              :   required UTXONetworkType network,
     496              :   required ElectrumXClient? initalClient,
     497              : }) async {
     498            1 :   final (fee, _, _) = await fetchFromRandomElectrumXNode(
     499            2 :     (client) => client.estimateFee(blocks: blocks),
     500              :     client: initalClient,
     501            1 :     endpoints: network.endpoints,
     502            1 :     token: network.coin,
     503              :   );
     504              : 
     505            0 :   if (fee == null) throw Exception("Fee estimation failed");
     506              : 
     507            1 :   final feePerKb = Amount.convert(value: fee, decimals: 8);
     508              : 
     509            2 :   final feePerB = feePerKb / Amount.from(value: 1000, decimals: 0);
     510              : 
     511              :   return feePerB;
     512              : }
     513              : 
     514            1 : Future<UtxoNetworkFees> getNetworkFees({
     515              :   required UTXONetworkType network,
     516              :   double multiplier = 1.0,
     517              : }) async {
     518            2 :   final blockInOneHour = 3600 ~/ network.blockTime;
     519            3 :   final blocksTillTomorrow = 24 * 3600 ~/ network.blockTime;
     520              : 
     521            1 :   final client = await getBestHealthEndpointsWithRetry(
     522            1 :     endpointPool: network.endpoints,
     523            1 :     token: network.coin,
     524              :     max: 1,
     525              :     min: 1,
     526              :   )
     527            1 :       .then(
     528            2 :         (endpoints) => endpoints.first,
     529              :       )
     530            1 :       .then(
     531            2 :         (endpoint) => createElectrumXClient(
     532              :           endpoint: endpoint.$1,
     533              :           port: endpoint.$2,
     534            1 :           token: network.coin,
     535              :         ),
     536              :       );
     537              : 
     538            1 :   final next = await estimateFeeForPriority(
     539              :     blocks: 1,
     540              :     network: network,
     541              :     initalClient: client,
     542              :   );
     543              : 
     544            1 :   final second = await estimateFeeForPriority(
     545              :     blocks: 2,
     546              :     network: network,
     547              :     initalClient: client,
     548              :   );
     549              : 
     550            1 :   final hour = await estimateFeeForPriority(
     551              :     blocks: blockInOneHour,
     552              :     network: network,
     553              :     initalClient: client,
     554              :   );
     555              : 
     556            1 :   final day = await estimateFeeForPriority(
     557              :     blocks: blocksTillTomorrow,
     558              :     network: network,
     559              :     initalClient: client,
     560              :   );
     561              : 
     562            1 :   client?.disconnect();
     563              : 
     564            1 :   return UtxoNetworkFees(
     565            1 :     nextBlock: next.multiplyAndCeil(multiplier),
     566            1 :     secondBlock: second.multiplyAndCeil(multiplier),
     567            1 :     hour: hour.multiplyAndCeil(multiplier),
     568            1 :     day: day.multiplyAndCeil(multiplier),
     569              :   );
     570              : }
     571              : 
     572            7 : Future<Iterable<UTXOTransaction>> computeMissingUTXODetails({
     573              :   required Iterable<ElectrumTransactionInfo> txList,
     574              :   required Iterable<NodeWithAddress> nodes,
     575              :   required Iterable<AddressType> addressTypes,
     576              :   required UTXONetworkType type,
     577              :   required List<(String, int)> endpoints,
     578              : }) async {
     579            7 :   final coin = type.coin;
     580           14 :   if (txList.isEmpty) return [];
     581              : 
     582           14 :   final watch = Stopwatch()..start();
     583            7 :   final clients = await createClients(endpoints: endpoints, token: coin);
     584              : 
     585            7 :   final batchSize = clients.length;
     586              : 
     587            7 :   if (batchSize == 0) {
     588            0 :     throw Exception(
     589            0 :       "No clients available for fetching UTXO details. for token: ${coin.symbol}",
     590              :     );
     591              :   }
     592              : 
     593            7 :   final pool = List<ElectrumTransactionInfo>.from(txList);
     594              : 
     595            7 :   final nodeMap = <String, int>{};
     596              : 
     597            7 :   Future<Iterable<UTXOTransaction>> fetchFromPool(
     598              :     ElectrumXClient initalClient,
     599              :   ) async {
     600            7 :     final txs = <UTXOTransaction>[];
     601              :     var client = initalClient;
     602            7 :     while (pool.isNotEmpty) {
     603            7 :       final txInfo = pool.removeLast();
     604              : 
     605            7 :       final (tx, newClient, _) = await fetchFromRandomElectrumXNode(
     606            7 :         (client) {
     607            7 :           return client.getTransaction(
     608            7 :             txHash: txInfo.hash,
     609              :             addressTypes: addressTypes,
     610              :             nodes: nodes,
     611              :             type: type,
     612              :           );
     613              :         },
     614              :         client: client,
     615              :         endpoints: endpoints,
     616              :         token: coin,
     617            7 :         timeout: Duration(seconds: 5),
     618              :       );
     619              : 
     620              :       if (tx == null) {
     621            0 :         Logger.logWarning(
     622            0 :             "Failed to fetch TX ${txInfo.hash} from ${client.host}");
     623            0 :         txs.add(txInfo.getNotAvailableUTXOTransaction(type.coin));
     624              :         continue;
     625              :       }
     626              : 
     627              :       if (newClient != null) client = newClient;
     628           35 :       nodeMap[client.host] = (nodeMap[client.host] ?? 0) + 1;
     629            7 :       txs.add(tx);
     630              :     }
     631              : 
     632              :     return txs;
     633              :   }
     634              : 
     635            7 :   final futures = [
     636           14 :     for (final client in clients) fetchFromPool(client),
     637              :   ];
     638              : 
     639            7 :   final results = await Future.wait(futures);
     640              : 
     641           21 :   final txs = results.expand((e) => e).toList();
     642              : 
     643            7 :   watch.stop();
     644            7 :   Logger.logFetch(
     645           21 :     "Fetched ${txs.length} transactions in ${watch.elapsed}",
     646              :   );
     647              : 
     648              :   ///
     649              :   /// Disconnect Clients
     650              :   ///
     651           14 :   await Future.wait([
     652           14 :     for (final client in clients) client.disconnect(),
     653              :   ]);
     654              : 
     655              :   return txs;
     656              : }
     657              : 
     658              : ///
     659              : /// Returns a map of UTXOs which belong to us and are unspent and their corresponding transactions
     660              : ///
     661            4 : Map<ElectrumOutput, UTXOTransaction> extractUTXOs({
     662              :   required Iterable<UTXOTransaction> txList,
     663              : }) {
     664            4 :   Map<ElectrumOutput, UTXOTransaction> utxoMap = {};
     665            8 :   for (final tx in txList) {
     666            8 :     for (final ElectrumOutput output in tx.outputs) {
     667           16 :       if (output.spent == false && output.belongsToUs == true) {
     668            2 :         utxoMap[output] = tx;
     669              :       }
     670              :     }
     671              :   }
     672              :   return utxoMap;
     673              : }
     674              : 
     675              : ///
     676              : /// Returns a map of UTXOs which belong to us and are unspent and their corresponding transactions
     677              : ///
     678            0 : Map<ElectrumOutput, UTXOTransaction> extractAllUTXOs({
     679              :   required Iterable<UTXOTransaction> txList,
     680              :   bool own = true,
     681              : }) {
     682            0 :   Map<ElectrumOutput, UTXOTransaction> utxoMap = {};
     683            0 :   for (final tx in txList) {
     684            0 :     for (final ElectrumOutput output in tx.outputs) {
     685              :       if (own) {
     686            0 :         if (output.belongsToUs == true) {
     687            0 :           utxoMap[output] = tx;
     688              :         }
     689              :       } else {
     690            0 :         utxoMap[output] = tx;
     691              :       }
     692              :     }
     693              :   }
     694              :   return utxoMap;
     695              : }
     696              : 
     697              : ///
     698              : /// Returns a map of UTXOs which belong to us and are unspent and their corresponding transactions
     699              : ///
     700            0 : List<ElectrumOutput> getSpendableOutputs(
     701              :         {required List<UTXOTransaction> txList}) =>
     702            0 :     [
     703            0 :       for (final tx in txList)
     704            0 :         for (final ElectrumOutput output in tx.outputs)
     705            0 :           if (output.spent == false && output.belongsToUs == true) output
     706              :     ];
     707              : 
     708            4 : BigInt computeBalanceFromUTXOs({
     709              :   required Iterable<UTXOTransaction> txList,
     710              : }) {
     711            4 :   BigInt balance = BigInt.zero;
     712            8 :   for (final tx in txList) {
     713            8 :     for (final ElectrumOutput output in tx.outputs) {
     714           16 :       if (output.spent == false && output.belongsToUs == true) {
     715            4 :         balance += output.value;
     716              :       }
     717              :     }
     718              :   }
     719              :   return balance;
     720              : }
     721              : 
     722            4 : BigInt computeBalanceFromVisualList({
     723              :   required Iterable<UTXOTransaction> txList,
     724              : }) {
     725            4 :   BigInt balance = BigInt.zero;
     726            8 :   for (final tx in txList) {
     727            8 :     if (tx.transferMethod == TransactionTransferMethod.receive) {
     728            8 :       balance += tx.value;
     729              :     }
     730              : 
     731            8 :     if (tx.transferMethod == TransactionTransferMethod.send) {
     732            8 :       balance -= tx.value;
     733              :     }
     734              :   }
     735              :   return balance;
     736              : }
     737              : 
     738            8 : TransactionTransferMethod determineSendDirection({
     739              :   required List<ElectrumInput> inputs,
     740              :   required List<ElectrumOutput> outputs,
     741              :   required Iterable<NodeWithAddress> nodes,
     742              :   required UTXONetworkType type,
     743              :   required Iterable<AddressType> addressTypes,
     744              : }) {
     745              :   bool anyInputsAreOurs;
     746              :   try {
     747           16 :     anyInputsAreOurs = inputs.any((input) {
     748            8 :       final inputAddress = input.getAddresses(
     749              :         addressTypes: addressTypes,
     750              :         networkType: type,
     751              :       );
     752           16 :       return nodes.addresses.any(
     753           16 :         (nodeAddress) => inputAddress.contains(nodeAddress),
     754              :       );
     755              :     });
     756              :   } catch (e) {
     757              :     anyInputsAreOurs = false;
     758              :   }
     759              : 
     760           24 :   final anyOutputsAreOurs = outputs.any((output) => output.belongsToUs);
     761              : 
     762           16 :   final outputsHaveReceive = outputs.any((output) {
     763            8 :     final outputAddresses = output.getAddresses(
     764              :       addressTypes: addressTypes,
     765              :       networkType: type,
     766              :     );
     767           24 :     return nodes.receiveNodes.addresses.any(
     768           16 :       (nodeAddress) => outputAddresses.contains(nodeAddress),
     769              :     );
     770              :   });
     771              : 
     772           16 :   final outputsHaveChange = outputs.any((output) {
     773            8 :     final outputAddresses = output.getAddresses(
     774              :       addressTypes: addressTypes,
     775              :       networkType: type,
     776              :     );
     777           24 :     return nodes.changeNodes.addresses.any(
     778           16 :       (nodeAddress) => outputAddresses.contains(nodeAddress),
     779              :     );
     780              :   });
     781              : 
     782              :   return switch ((anyInputsAreOurs, anyOutputsAreOurs)) {
     783           16 :     (true, true) when outputsHaveReceive => TransactionTransferMethod.own,
     784           14 :     (true, true) when outputsHaveChange && outputs.length == 1 =>
     785              :       TransactionTransferMethod.own,
     786              :     (true, true) => TransactionTransferMethod.send,
     787            6 :     (true, false) => TransactionTransferMethod.send,
     788            8 :     (false, true) => TransactionTransferMethod.receive,
     789              :     _ => TransactionTransferMethod.unknown,
     790              :   };
     791              : }
     792              : 
     793            8 : List<ElectrumOutput> findOurOwnCoins(
     794              :   List<ElectrumOutput> outputs,
     795              :   Iterable<NodeWithAddress> nodes,
     796              :   Iterable<AddressType> addressTypes,
     797              :   UTXONetworkType type,
     798              : ) {
     799            8 :   final outputs0 = <ElectrumOutput>[];
     800           16 :   for (final vout in outputs) {
     801            8 :     final outputAddresses = vout.getAddresses(
     802              :       networkType: type,
     803              :       addressTypes: addressTypes,
     804              :     );
     805              : 
     806            8 :     final node = nodes.singleWhereOrNull(
     807           24 :       (node) => node.addressesList.any(
     808           16 :         (nodeAddress) => outputAddresses.contains(nodeAddress),
     809              :       ),
     810              :     );
     811              :     final belongsToUs = node != null;
     812            8 :     outputs0.add(
     813            8 :       vout.copyWith(
     814              :         belongsToUs: belongsToUs,
     815              :         node: node,
     816              :       ),
     817              :     );
     818              :   }
     819              :   return outputs0;
     820              : }
     821              : 
     822            8 : (BigInt, BigInt) determineTransactionValue(
     823              :   List<ElectrumOutput> outputs,
     824              :   TransactionTransferMethod transferMethod,
     825              :   Iterable<NodeWithAddress> nodes,
     826              :   UTXONetworkType type,
     827              : ) {
     828              :   final ourValue = switch (transferMethod) {
     829           16 :     TransactionTransferMethod.receive => outputs.fold(
     830            8 :         BigInt.zero,
     831            8 :         (prev, output) {
     832            8 :           if (output.belongsToUs) {
     833           16 :             return prev + output.value;
     834              :           }
     835              :           return prev;
     836              :         },
     837              :       ),
     838            7 :     TransactionTransferMethod.own => outputs
     839            7 :             .singleWhereOrNull(
     840           21 :               (output) => output.node is ReceiveNode,
     841              :             )
     842            7 :             ?.value ??
     843            6 :         BigInt.from(-1),
     844           14 :     TransactionTransferMethod.send => outputs.fold(
     845            7 :         BigInt.zero,
     846            7 :         (prev, output) {
     847            7 :           if (!output.belongsToUs) {
     848           14 :             return prev + output.value;
     849              :           }
     850              :           return prev;
     851              :         },
     852              :       ),
     853           15 :     TransactionTransferMethod.unknown => BigInt.from(-1),
     854              :   };
     855            8 :   final totalValue = outputs.fold(
     856            8 :     BigInt.zero,
     857           24 :     (prev, output) => prev + output.value,
     858              :   );
     859              : 
     860              :   return (ourValue, totalValue);
     861              : }
     862              : 
     863            8 : String? determineTransactionTarget(
     864              :   List<ElectrumOutput> outputs,
     865              :   TransactionTransferMethod transferMethod,
     866              :   UTXONetworkType type,
     867              :   AddressType addressType,
     868              : ) {
     869            8 :   final mainOutput = _findMainOutput(outputs, transferMethod);
     870            8 :   return mainOutput?.getAddress(type, addressType: addressType);
     871              : }
     872              : 
     873            8 : ElectrumOutput? _findMainOutput(
     874              :   List<ElectrumOutput> outputs,
     875              :   TransactionTransferMethod transferMethod,
     876              : ) {
     877              :   final voutListFull = outputs;
     878              : 
     879              :   final isMainOutputOurOwn =
     880            8 :       transferMethod == TransactionTransferMethod.receive ||
     881            7 :           transferMethod == TransactionTransferMethod.own;
     882              :   final voutList =
     883           40 :       voutListFull.where((v) => v.belongsToUs == isMainOutputOurOwn).toList();
     884            8 :   if (voutList.isEmpty) {
     885              :     return null;
     886              :   }
     887              : 
     888            8 :   ElectrumOutput highestVout = voutList[0];
     889           16 :   for (final v in voutList) {
     890            8 :     final valueCoin = v.value;
     891           16 :     if (valueCoin > highestVout.value) {
     892              :       highestVout = v;
     893              :     }
     894              :   }
     895              :   return highestVout;
     896              : }
        

Generated by: LCOV version 2.0-1