fetchMissingUTXOTransactions function

Future<UTXOTxInfo> fetchMissingUTXOTransactions({
  1. required Set<UTXOTransaction> cachedTransactions,
  2. required List<NodeWithAddress> cachedNodes,
  3. required Set<ElectrumTransactionInfo> allTxs,
  4. required Iterable<NodeWithAddress> nodes,
  5. required CoinEntity coin,
  6. required UTXONetworkType networkType,
  7. required Iterable<AddressType> addressTypes,
  8. required List<(String, int)> endpoints,
  9. required Stopwatch watch,
})

Implementation

Future<UTXOTxInfo> fetchMissingUTXOTransactions({
  required Set<UTXOTransaction> cachedTransactions,
  required List<NodeWithAddress> cachedNodes,
  required Set<ElectrumTransactionInfo> allTxs,
  required Iterable<NodeWithAddress> nodes,
  required CoinEntity coin,
  required UTXONetworkType networkType,
  required Iterable<AddressType> addressTypes,
  required List<(String, int)> endpoints,
  required Stopwatch watch,
}) async {
  /// Filter out all transactions that are already cached
  final newTxs = allTxs.where(
    (tx) {
      final isCached = cachedTransactions.any((cTx) => cTx.id == tx.hash);

      return isCached == false;
    },
  );

  final pendingTxs = allTxs.where(
    (tx) {
      final cTx = cachedTransactions.singleWhereOrNull(
        (cTx) => cTx.id == tx.hash,
      );
      if (cTx == null) return false;

      return cTx.isPending || cTx is NotAvaialableUTXOTransaction;
    },
  );
  Logger.log("Found ${pendingTxs.length} pending TXs for ${coin.symbol}");
  Logger.log("Found ${newTxs.length} new TXs for ${coin.symbol}");

  ///
  /// Fetch UTXO Details for all new transactions
  ///
  var newUtxoTxs = await computeMissingUTXODetails(
    txList: newTxs,
    nodes: nodes,
    type: networkType,
    endpoints: endpoints,
    addressTypes: addressTypes,
  );

  ///
  /// Fetch Parent Transactions of P2SH Inputs to get the redeem script and in hence the address
  ///
  final unsupported = newUtxoTxs.where(
    (tx) => tx.sender == ADDRESS_NOT_SUPPORTED,
  );
  final updatedUTXOs = <UTXOTransaction>[];
  for (final tx in unsupported) {
    final firstInput = tx.inputs.firstOrNull;
    if (firstInput == null) continue;

    /// Coinbase Parent TX
    if (firstInput.isCoinbase) {
      continue;
    }

    final outputIndex = firstInput.vout!;
    final parentTxHash = firstInput.txid;
    if (parentTxHash == null) continue;

    // Check if Parent TX is already in the list
    var parentTx = newUtxoTxs
        .singleWhereOrNull((element) => element.id == firstInput.txid);

    final (_tx, _, _) = await fetchFromRandomElectrumXNode(
      (client) {
        return client.getTransaction(
          addressTypes: addressTypes,
          nodes: nodes,
          type: networkType,
          txHash: parentTxHash,
        );
      },
      client: null,
      endpoints: networkType.endpoints,
      token: coin,
    );

    parentTx ??= _tx;

    if (parentTx == null) {
      Logger.logWarning(
        "Parent TX $parentTxHash not found for ${tx.hash}",
      );
      continue;
    }

    final output = parentTx.outputs[outputIndex];

    final address = output.getAddress(networkType);

    updatedUTXOs.add(tx.copyWith(sender: address));
  }
  // Replace the unsupported transactions with the updated ones
  newUtxoTxs = newUtxoTxs.map((tx) {
    final updatedTx = updatedUTXOs.singleWhereOrNull(
      (upTx) => upTx.id == tx.id,
    );
    return updatedTx ?? tx;
  });

  ///
  /// Fetch UTXO Details for all pending transactions and replace them
  ///
  final pendingUtxoTxs = await computeMissingUTXODetails(
    txList: pendingTxs,
    nodes: nodes,
    type: networkType,
    endpoints: endpoints,
    addressTypes: addressTypes,
  );

  var utxoTXs = {
    ...newUtxoTxs,
    ...cachedTransactions,
  };

  /// Replace the pending transactions with the updated ones
  utxoTXs = utxoTXs.map((tx) {
    final pendingTx = pendingUtxoTxs.singleWhereOrNull(
      (element) => element.id == tx.id,
    );
    return pendingTx ?? tx;
  }).toSet();

  ///
  /// Mark Spent Outputs
  ///
  for (final tx in utxoTXs) {
    for (final input in tx.inputs) {
      final outputs = utxoTXs
          .singleWhereOrNull((element) => element.id == input.txid)
          ?.outputs;

      if (input.isCoinbase) continue;
      final index = input.vout!;
      if (outputs == null || outputs.length <= index) {
        continue;
      }
      outputs[index] = outputs[index].copyWith(spent: true);
    }
  }

  watch.stop();
  Logger.logFetch("Fetched TXs in ${watch.elapsed}");

  final sortedTxs = utxoTXs.sortedBy<GenericTransaction>((tx) => tx).toSet();

  return (sortedTxs, nodes);
}