All files / src/UtxoProvider BlockfrostUtxoProvider.ts

94.59% Statements 35/37
100% Branches 5/5
100% Functions 14/14
94.11% Lines 32/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 8217x 17x                     17x       2x 2x       5x 5x   5x 202x 202x 202x     5x       201x   1x       200x       202x 202x   201x   1x 1x 1x     200x 200x     201x 200x     1x 1x       3x 3x   3x 5x       3x            
import { BlockfrostClient, BlockfrostProvider, BlockfrostToCore, fetchSequentially } from '../blockfrost';
import { Cardano, Serialization, UtxoByAddressesArgs, UtxoProvider } from '@cardano-sdk/core';
import { Logger } from 'ts-log';
import type { Cache } from '@cardano-sdk/util';
import type { Responses } from '@blockfrost/blockfrost-js';
 
type BlockfrostUtxoProviderDependencies = {
  client: BlockfrostClient;
  cache: Cache<Cardano.Tx>;
  logger: Logger;
};
 
export class BlockfrostUtxoProvider extends BlockfrostProvider implements UtxoProvider {
  private readonly cache: Cache<Cardano.Tx>;
 
  constructor({ cache, client, logger }: BlockfrostUtxoProviderDependencies) {
    super(client, logger);
    this.cache = cache;
  }
 
  protected async fetchUtxos(addr: Cardano.PaymentAddress, paginationQueryString: string): Promise<Cardano.Utxo[]> {
    const queryString = `addresses/${addr.toString()}/utxos?${paginationQueryString}`;
    const utxos = await this.request<Responses['address_utxo_content']>(queryString);
 
    const utxoPromises = utxos.map((utxo) =>
      this.fetchDetailsFromCBOR(utxo.tx_hash).then((tx) => {
        const txOut = tx ? tx.body.outputs.find((output) => output.address === utxo.address) : undefined;
        return BlockfrostToCore.addressUtxoContent(addr.toString(), utxo, txOut);
      })
    );
    return Promise.all(utxoPromises);
  }
 
  async fetchCBOR(hash: string): Promise<string> {
    return this.request<Responses['tx_content_cbor']>(`txs/${hash}/cbor`)
      .then((response) => {
        if (response.cbor) return response.cbor;
        throw new Error('CBOR is null');
      })
      .catch((_error) => {
        throw new Error('CBOR fetch failed');
      });
  }
  protected async fetchDetailsFromCBOR(hash: string) {
    const cached = await this.cache.get(hash);
    if (cached) return cached;
 
    const result = await this.fetchCBOR(hash)
      .then((cbor) => {
        const tx = Serialization.Transaction.fromCbor(Serialization.TxCBOR(cbor)).toCore();
        this.logger.debug('Fetched details from CBOR for tx', hash);
        return tx;
      })
      .catch((error) => {
        this.logger.warn('Failed to fetch details from CBOR for tx', hash, error);
        return null;
      });
 
    if (!result) {
      return null;
    }
 
    void this.cache.set(hash, result);
    return result;
  }
 
  public async utxoByAddresses({ addresses }: UtxoByAddressesArgs): Promise<Cardano.Utxo[]> {
    try {
      const utxoResults = await Promise.all(
        addresses.map(async (address) =>
          fetchSequentially<Cardano.Utxo, Cardano.Utxo>({
            request: async (paginationQueryString) => await this.fetchUtxos(address, paginationQueryString)
          })
        )
      );
      return utxoResults.flat(1);
    } catch (error) {
      throw this.toProviderError(error);
    }
  }
}