All files / src/TxSubmitProvider BlockfrostTxSubmitProvider.ts

41.66% Statements 15/36
11.11% Branches 1/9
75% Functions 3/4
41.66% Lines 15/36

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 9917x   17x                                     17x                                 17x 1x   1x                                                     1x           17x   1x         1x 1x           1x 1x 1x       1x        
import { BlockfrostClient, BlockfrostProvider } from '../blockfrost';
import { Logger } from 'ts-log';
import {
  ProviderError,
  SubmitTxArgs,
  TxSubmissionError,
  TxSubmissionErrorCode,
  TxSubmitProvider,
  ValueNotConservedData
} from '@cardano-sdk/core';
 
type BlockfrostTxSubmissionErrorMessage = {
  contents: {
    contents: {
      contents: {
        error: [string];
      };
    };
  };
};
 
const tryParseBlockfrostTxSubmissionErrorMessage = (
  errorMessage: string
): BlockfrostTxSubmissionErrorMessage | null => {
  try {
    const error = JSON.parse(errorMessage);
    Iif (typeof error === 'object' && Array.isArray(error?.contents?.contents?.contents?.error)) {
      return error;
    }
  } catch {
    return null;
  }
  return null;
};
 
/**
 * @returns TxSubmissionError if sucessfully mapped, otherwise `null`
 */
const tryMapTxBlockfrostSubmissionError = (error: ProviderError): TxSubmissionError | null => {
  try {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const detail = JSON.parse(error.detail as any);
    Iif (typeof detail?.message === 'string') {
      const blockfrostTxSubmissionErrorMessage = tryParseBlockfrostTxSubmissionErrorMessage(detail.message);
      Iif (!blockfrostTxSubmissionErrorMessage) {
        return null;
      }
      const message = blockfrostTxSubmissionErrorMessage.contents.contents.contents.error[0];
      Iif (message.includes('OutsideValidityIntervalUTxO')) {
        // error also contains information about validity interval and actual slots,
        // but we're currently not using this info
        return new TxSubmissionError(TxSubmissionErrorCode.OutsideOfValidityInterval, null, message);
      }
      // eslint-disable-next-line wrap-regex
      const valueNotConservedMatch = /ValueNotConservedUTxO.+Coin (\d+).+Coin (\d+)/.exec(message);
      Iif (valueNotConservedMatch) {
        const consumed = BigInt(valueNotConservedMatch[1]);
        const produced = BigInt(valueNotConservedMatch[2]);
        const valueNotConservedData: ValueNotConservedData = {
          // error also contains information about consumed and produced native assets
          // but we're currently not using this info
          consumed: { coins: consumed },
          produced: { coins: produced }
        };
        return new TxSubmissionError(TxSubmissionErrorCode.ValueNotConserved, valueNotConservedData, message);
      }
    }
  } catch {
    return null;
  }
 
  return null;
};
 
export class BlockfrostTxSubmitProvider extends BlockfrostProvider implements TxSubmitProvider {
  constructor(client: BlockfrostClient, logger: Logger) {
    super(client, logger);
  }
 
  async submitTx({ signedTransaction }: SubmitTxArgs): Promise<void> {
    // @ todo handle context and resolutions
    try {
      await this.request<string>('tx/submit', {
        body: Buffer.from(signedTransaction, 'hex'),
        headers: { 'Content-Type': 'application/cbor' },
        method: 'POST'
      });
    } catch (error) {
      if (error instanceof ProviderError) {
        const submissionError = tryMapTxBlockfrostSubmissionError(error);
        Iif (submissionError) {
          throw new ProviderError(error.reason, submissionError, error.detail);
        }
      }
      throw error;
    }
  }
}