All files / src/PgBoss stakePoolMetadataHandler.ts

57.97% Statements 40/69
75% Branches 12/16
66.66% Functions 4/6
53.96% Lines 34/63

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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 15536x 36x 36x   36x 36x   36x 36x   36x 2x     2x         2x                     36x 3x 3x 3x             3x 3x     1x           36x             3x 1x 2x 1x     1x         36x       4x 2x   2x 1x   1x 2x 1x   1x   36x                                                                                                                                            
import { Cardano, CardanoNodeUtil, NotImplementedError, ProviderFailure } from '@cardano-sdk/core';
import { CustomError } from 'ts-custom-error';
import { DataSource, MoreThan } from 'typeorm';
import { Hash32ByteBase16 } from '@cardano-sdk/crypto';
import { PoolMetadataEntity, PoolRegistrationEntity, StakePoolMetadataJob } from '@cardano-sdk/projection-typeorm';
import { StakePoolMetadataFetchMode, checkProgramOptions } from '../Program/options';
import { WorkerHandlerFactory } from './types';
import { createHttpStakePoolMetadataService } from '../StakePool';
import { isErrorWithConstraint } from './util';
 
export const isUpdateOutdated = async (dataSource: DataSource, poolId: Cardano.PoolId, poolRegistrationId: string) => {
  const repos = dataSource.getRepository(PoolRegistrationEntity);
  // TODO: Improve this check to take in account stability window
  // Ref: LW-6492
  const res = await repos.countBy({
    id: MoreThan(poolRegistrationId as unknown as bigint),
    stakePool: { id: poolId }
  });
 
  return res > 0;
};
 
interface SavePoolMetadataArguments {
  dataSource: DataSource;
  hash: Hash32ByteBase16;
  metadata: Cardano.StakePoolMetadata;
  poolId: Cardano.PoolId;
  poolRegistrationId: string;
}
 
export const savePoolMetadata = async (args: SavePoolMetadataArguments) => {
  const { dataSource, hash, metadata, poolId, poolRegistrationId } = args;
  const repos = dataSource.getRepository(PoolMetadataEntity);
  const entity = repos.create({
    ...metadata,
    hash,
    poolUpdate: { id: BigInt(poolRegistrationId) },
    stakePool: { id: poolId }
  });
 
  try {
    await repos.upsert(entity, ['poolUpdate']);
  } catch (error) {
    // If no poolRegistration record is present, it was rolled back: do nothing
    if (isErrorWithConstraint(error) && error.constraint === 'FK_pool_metadata_pool_update_id') return;
 
    throw error;
  }
};
 
export const getUrlToFetch = (
  metadataFetchMode: StakePoolMetadataFetchMode,
  smashUrl: string | undefined,
  directUrl: string,
  poolRegistrationId: string,
  metadataHash: string
) => {
  if (metadataFetchMode === StakePoolMetadataFetchMode.SMASH) {
    return `${smashUrl}/metadata/${poolRegistrationId}/${metadataHash}`;
  } else if (metadataFetchMode === StakePoolMetadataFetchMode.DIRECT) {
    return directUrl;
  }
 
  throw new NotImplementedError(
    `There is no implementation to handle the fetch mode (--metadata-fetch-mode): ${metadataFetchMode}`
  );
};
 
export const attachExtendedMetadata = (
  metadataWithoutExt: Cardano.StakePoolMetadata,
  extMetadata: Cardano.ExtendedStakePoolMetadata | CustomError | undefined
): Cardano.StakePoolMetadata => {
  if (extMetadata instanceof CustomError) {
    const error = extMetadata;
 
    if (CardanoNodeUtil.isProviderError(error) && error.reason === ProviderFailure.NotFound) {
      return { ...metadataWithoutExt!, ext: null };
    }
    return metadataWithoutExt;
  } else if (extMetadata === undefined) {
    return metadataWithoutExt;
  }
  return { ...metadataWithoutExt!, ext: extMetadata };
};
export const stakePoolMetadataHandlerFactory: WorkerHandlerFactory = (options) => {
  const { dataSource, logger, metadataFetchMode, smashUrl } = options;
  const service = createHttpStakePoolMetadataService(logger);
 
  checkProgramOptions(metadataFetchMode, smashUrl);
 
  return async (task: StakePoolMetadataJob) => {
    const { metadataJson, poolId, poolRegistrationId } = task;
    const { hash, url } = metadataJson;
 
    logger.info(`Checking if pool update ${poolRegistrationId} is outdated by a more recent update`);
 
    // If there is a newer pool update in the chain...
    Iif (await isUpdateOutdated(dataSource, poolId, poolRegistrationId)) {
      logger.info('Pool update is outdated, metadata no longer needed');
 
      return;
    }
 
    const urlToFetch: string = getUrlToFetch(metadataFetchMode, smashUrl, url, poolId, hash);
 
    logger.info('Resolving stake pool metadata...', { metadataFetchMode, poolId, poolRegistrationId });
 
    const metadataResponse: Cardano.StakePoolMetadata | CustomError = await service.getStakePoolMetadata(
      hash,
      urlToFetch
    );
 
    if (metadataResponse instanceof CustomError) {
      logger.info('Stake pool metadata NOT resolved with errors', {
        metadataResponse,
        poolId,
        poolRegistrationId,
        url
      });
      // In case of errors the handler throws in order to let pg-boss to retry the job.
      logger.info('StakePoolMetadataJob failed to fetch stake pool metadata.');
      throw metadataResponse;
    } else {
      const metadataWithoutExt: Cardano.StakePoolMetadata = metadataResponse;
 
      logger.info('Stake pool metadata resolved successfully', { metadataWithoutExt, poolId, poolRegistrationId, url });
 
      logger.info('Resolving extended stake pool metadata...', { metadataFetchMode, poolId, poolRegistrationId });
 
      const extendedMetadata = await service.getValidateStakePoolExtendedMetadata(metadataWithoutExt);
 
      logger.info('Stake pool extended metadata resolved', {
        extendedMetadata,
        metadataFetchMode,
        poolId,
        poolRegistrationId
      });
 
      const metadata: Cardano.StakePoolMetadata = attachExtendedMetadata(metadataWithoutExt, extendedMetadata);
 
      await savePoolMetadata({ dataSource, hash, metadata, poolId, poolRegistrationId });
 
      logger.info('Stake pool metadata saved');
 
      Iif (extendedMetadata instanceof CustomError) {
        logger.info('StakePoolMetadataJob failed to fetch extended stake pool metadata.');
        throw extendedMetadata;
      }
    }
  };
 
  // TODO: Store the error in a dedicated table
  // Ref: LW-6409
};