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

Generated by: LCOV version 2.0-1