PHP WebShell

Текущая директория: /opt/BitGoJS/modules/abstract-cosmos/src/lib

Просмотр файла: utils.ts

import {
  BaseUtils,
  InvalidTransactionError,
  NotImplementedError,
  NotSupported,
  ParseTransactionError,
  TransactionType,
} from '@bitgo/sdk-core';
import { encodeSecp256k1Pubkey, encodeSecp256k1Signature } from '@cosmjs/amino';
import { fromBase64, fromBech32, fromHex, toBech32, toHex } from '@cosmjs/encoding';
import {
  DecodedTxRaw,
  decodePubkey,
  decodeTxRaw,
  EncodeObject,
  encodePubkey,
  makeAuthInfoBytes,
  makeSignDoc,
  Registry,
} from '@cosmjs/proto-signing';
import { Coin, defaultRegistryTypes } from '@cosmjs/stargate';
import BigNumber from 'bignumber.js';
import { SignDoc, TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
import { Any } from 'cosmjs-types/google/protobuf/any';
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx';
import { createHash, Hash } from 'crypto';
import * as constants from './constants';
import {
  CosmosLikeTransaction,
  DelegateOrUndelegeteMessage,
  ExecuteContractMessage,
  FeeData,
  MessageData,
  RedelegateMessage,
  SendMessage,
  WithdrawDelegatorRewardsMessage,
} from './iface';
import { CosmosKeyPair as KeyPair } from './keyPair';

const { MsgSend } = require('../../resources/MsgCompiled').types;

export class CosmosUtils<CustomMessage = never> implements BaseUtils {
  protected registry;

  constructor() {
    this.registry = new Registry([...defaultRegistryTypes]);
    this.registry.register(constants.executeContractMsgTypeUrl, MsgExecuteContract);
    this.registry.register('/types.MsgSend', MsgSend);
  }

  /** @inheritdoc */
  isValidBlockId(hash: string): boolean {
    return this.validateBlake2b(hash);
  }

  /** @inheritdoc */
  isValidPrivateKey(key: string): boolean {
    try {
      new KeyPair({ prv: key });
      return true;
    } catch {
      return false;
    }
  }

  /** @inheritdoc */
  isValidPublicKey(key: string): boolean {
    try {
      new KeyPair({ pub: key });
      return true;
    } catch {
      return false;
    }
  }

  /** @inheritdoc */
  isValidSignature(signature: string): boolean {
    throw new NotImplementedError('isValidSignature not implemented');
  }

  /** @inheritdoc */
  isValidTransactionId(txId: string): boolean {
    return this.validateBlake2b(txId);
  }

  /**
   * Checks if transaction hash is in valid black2b format
   */
  validateBlake2b(hash: string): boolean {
    if (hash?.length !== 64) {
      return false;
    }
    return hash.match(/^[a-zA-Z0-9]+$/) !== null;
  }

  /**
   * Validates whether amounts are in range
   *
   * @param {number[]} amounts - the amounts to validate
   * @returns {boolean} - the validation result
   */
  isValidAmounts(amounts: number[]): boolean {
    for (const amount of amounts) {
      if (!this.isValidAmount(amount)) {
        return false;
      }
    }
    return true;
  }

  /**
   * Validates whether amount is in range
   * @param {number} amount
   * @returns {boolean} the validation result
   */
  isValidAmount(amount: number): boolean {
    const bigNumberAmount = new BigNumber(amount);
    if (!bigNumberAmount.isInteger() || bigNumberAmount.isLessThanOrEqualTo(0)) {
      return false;
    }
    return true;
  }

  /**
   * Decodes raw tx data into messages, signing info, and fee data
   * @param {string} txHex - raw base64 tx
   * @returns {DecodedTxRaw} Decoded transaction
   */
  getDecodedTxFromRawBase64(txRaw: string): DecodedTxRaw {
    try {
      return decodeTxRaw(fromBase64(txRaw));
    } catch (e) {
      throw new ParseTransactionError('Error decoding TxRaw base64 encoded string: ' + e.message);
    }
  }

  /**
   * Returns the array of messages in the body of the decoded transaction
   * @param {DecodedTxRaw} decodedTx
   * @returns {EncodeObject[]} messages along with type url
   */
  private getEncodedMessagesFromDecodedTx(decodedTx: DecodedTxRaw): EncodeObject[] {
    return decodedTx.body.messages;
  }

  /**
   * Checks the txn sequence is valid or not
   * @param {number} sequence
   */
  validateSequence(sequence: number) {
    if (sequence < 0) {
      throw new InvalidTransactionError('Invalid sequence: less than zero');
    }
  }

  /**
   * Pulls the sequence number from a DecodedTxRaw AuthInfo property
   * @param {DecodedTxRaw} decodedTx
   * @returns {number} sequence
   */
  getSequenceFromDecodedTx(decodedTx: DecodedTxRaw): number {
    return Number(decodedTx.authInfo.signerInfos[0].sequence);
  }

  /**
   * Pulls the typeUrl from the encoded message of a DecodedTxRaw
   * @param {DecodedTxRaw} decodedTx
   * @returns {string} cosmos proto type url
   */
  getTypeUrlFromDecodedTx(decodedTx: DecodedTxRaw): string {
    const encodedMessage = this.getEncodedMessagesFromDecodedTx(decodedTx)[0];
    return encodedMessage.typeUrl;
  }

  /**
   * Returns the fee data from the decoded transaction
   * @param {DecodedTxRaw} decodedTx
   * @returns {FeeData} fee data
   */
  getGasBudgetFromDecodedTx(decodedTx: DecodedTxRaw): FeeData {
    return {
      amount: decodedTx.authInfo.fee?.amount as Coin[],
      gasLimit: Number(decodedTx.authInfo.fee?.gasLimit),
    };
  }

  /**
   * Returns the publicKey from the decoded transaction
   * @param {DecodedTxRaw} decodedTx
   * @returns {string | undefined} publicKey in hex format if it exists, undefined otherwise
   */
  getPublicKeyFromDecodedTx(decodedTx: DecodedTxRaw): string | undefined {
    const publicKeyUInt8Array = decodedTx.authInfo.signerInfos?.[0].publicKey?.value;
    if (publicKeyUInt8Array) {
      return toHex(fromBase64(decodePubkey(decodedTx.authInfo.signerInfos?.[0].publicKey)?.value));
    }
    return undefined;
  }

  /**
   * Returns the array of MessageData[] from the decoded transaction
   * @param {DecodedTxRaw} decodedTx
   * @returns {MessageData[]} Send transaction message data
   */
  protected getSendMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData<CustomMessage>[] {
    return decodedTx.body.messages.map((message) => {
      const value = this.registry.decode(message);
      return {
        value: {
          fromAddress: value.fromAddress,
          toAddress: value.toAddress,
          amount: value.amount,
        },
        typeUrl: message.typeUrl,
      };
    });
  }

  /**
   * Returns the array of MessageData[] from the decoded transaction
   * @param {DecodedTxRaw} decodedTx
   * @returns {MessageData[]} Delegate of undelegate transaction message data
   */
  getDelegateOrUndelegateMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData<CustomMessage>[] {
    return decodedTx.body.messages.map((message) => {
      const value = this.registry.decode(message);
      return {
        value: {
          delegatorAddress: value.delegatorAddress,
          validatorAddress: value.validatorAddress,
          amount: value.amount,
        },
        typeUrl: message.typeUrl,
      };
    });
  }

  /**
   * Returns the array of MessageData[] from the decoded transaction
   * @param {DecodedTxRaw} decodedTx
   * @returns {MessageData[]} Redelegate transaction message data
   */
  getRedelegateMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData<CustomMessage>[] {
    return decodedTx.body.messages.map((message) => {
      const value = this.registry.decode(message);
      return {
        value: {
          delegatorAddress: value.delegatorAddress,
          validatorSrcAddress: value.validatorSrcAddress,
          validatorDstAddress: value.validatorDstAddress,
          amount: value.amount,
        },
        typeUrl: message.typeUrl,
      };
    });
  }

  /**
   * Returns the array of MessageData[] from the decoded transaction
   * @param {DecodedTxRaw} decodedTx
   * @returns {MessageData[]} WithdrawDelegatorRewards transaction message data
   */
  getWithdrawRewardsMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData<CustomMessage>[] {
    return decodedTx.body.messages.map((message) => {
      const value = this.registry.decode(message);
      return {
        value: {
          delegatorAddress: value.delegatorAddress,
          validatorAddress: value.validatorAddress,
        },
        typeUrl: message.typeUrl,
      };
    });
  }

  /**
   * Returns the array of MessageData[] from the decoded transaction
   * @param {DecodedTxRaw} decodedTx
   * @returns {MessageData[]} Delegate of undelegate transaction message data
   */
  getWithdrawDelegatorRewardsMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData<CustomMessage>[] {
    return decodedTx.body.messages.map((message) => {
      const value = this.registry.decode(message);
      return {
        value: {
          delegatorAddress: value.delegatorAddress,
          validatorAddress: value.validatorAddress,
        },
        typeUrl: message.typeUrl,
      };
    });
  }

  /**
   * Get a cosmos chain address from its equivalent hex
   * @param {string} prefix
   * @param {string} addressHex
   * @returns {string}
   */
  getCosmosLikeAddressFromHex(prefix: string, addressHex: string): string {
    if (addressHex.indexOf('0x') === 0) {
      addressHex = addressHex.slice(2);
    }
    return toBech32(prefix, fromHex(addressHex));
  }

  /**
   * Get a EVM chain address from its equivalent hex
   * @param {string} prefix
   * @param {string} addressHex
   * @returns {string}
   */
  getEvmLikeAddressFromCosmos(cosmosLikeAddress: string): string {
    return '0x' + toHex(fromBech32(cosmosLikeAddress).data);
  }

  /**
   * Returns the array of MessageData[] from the decoded transaction
   * @param {DecodedTxRaw} decodedTx
   * @returns {MessageData[]} Execute contract transaction message data
   */
  getExecuteContractMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData<CustomMessage>[] {
    return decodedTx.body.messages.map((message) => {
      const value = this.registry.decode(message);
      return {
        value: {
          sender: value.sender,
          contract: value.contract,
          msg: value.msg,
          funds: value.funds,
        },
        typeUrl: message.typeUrl,
      };
    });
  }

  /**
   * Returns the array of MessageData[] from the decoded transaction
   * @param {DecodedTxRaw} decodedTx
   * @returns {MessageData[]} Custom transaction message data
   */
  getCustomMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData<CustomMessage>[] {
    throw new NotSupported('Custom message data not supported');
  }

  /**
   * Determines bitgo transaction type based on cosmos proto type url
   * @param {string} typeUrl
   * @returns {TransactionType | undefined} TransactionType if url is supported else undefined
   */
  getTransactionTypeFromTypeUrl(typeUrl: string): TransactionType | undefined {
    switch (typeUrl) {
      case constants.sendMsgTypeUrl:
        return TransactionType.Send;
      case constants.sendMsgType:
        return TransactionType.Send;
      case constants.delegateMsgTypeUrl:
        return TransactionType.StakingActivate;
      case constants.undelegateMsgTypeUrl:
        return TransactionType.StakingDeactivate;
      case constants.withdrawDelegatorRewardMsgTypeUrl:
        return TransactionType.StakingWithdraw;
      case constants.executeContractMsgTypeUrl:
        return TransactionType.ContractCall;
      case constants.redelegateTypeUrl:
        return TransactionType.StakingRedelegate;
      default:
        return undefined;
    }
  }

  /**
   * Takes a hex encoded pubkey, converts it to the Amino JSON representation (type/value wrapper)
   * and returns it as protobuf `Any`
   * @param {string} pubkey hex encoded compressed secp256k1 public key
   * @returns {Any} pubkey encoded as protobuf `Any`
   */
  getEncodedPubkey(pubkey: string): Any {
    return encodePubkey(encodeSecp256k1Pubkey(fromHex(pubkey)));
  }

  /**
   * Gets the send messages used in the final step of encoding a transaction. This allows for any final processing needed.
   * @param {CosmosLikeTransaction} cosmosLikeTransaction transaction to get send messages from
   * @returns {Any[]} processed send messages
   */
  getSendMessagesForEncodingTx(cosmosLikeTransaction: CosmosLikeTransaction<CustomMessage>): Any[] {
    return cosmosLikeTransaction.sendMessages as unknown as Any[];
  }

  /**
   * Creates a txRaw from an cosmos like transaction @see CosmosLikeTransaction
   * @Precondition cosmosLikeTransaction.publicKey must be defined
   * @param {CosmosLikeTransaction} cosmosLikeTransaction
   * @returns {TxRaw} Unsigned raw transaction
   */
  createTxRawFromCosmosLikeTransaction(cosmosLikeTransaction: CosmosLikeTransaction<CustomMessage>): TxRaw {
    if (!cosmosLikeTransaction.publicKey) {
      throw new Error('publicKey is required to create a txRaw');
    }
    const encodedPublicKey: Any = this.getEncodedPubkey(cosmosLikeTransaction.publicKey);
    const messages = this.getSendMessagesForEncodingTx(cosmosLikeTransaction);
    let txBodyValue;
    if (cosmosLikeTransaction.memo) {
      txBodyValue = {
        memo: cosmosLikeTransaction.memo,
        messages: messages,
      };
    } else {
      txBodyValue = {
        messages: messages,
      };
    }

    const txBodyBytes = this.registry.encodeTxBody(txBodyValue);
    const sequence = cosmosLikeTransaction.sequence;
    const authInfoBytes = makeAuthInfoBytes(
      [{ pubkey: encodedPublicKey, sequence }],
      cosmosLikeTransaction.gasBudget.amount,
      cosmosLikeTransaction.gasBudget.gasLimit,
      undefined,
      undefined,
      undefined
    );
    return TxRaw.fromPartial({
      bodyBytes: txBodyBytes,
      authInfoBytes: authInfoBytes,
    });
  }

  /**
   * Encodes a signature into a txRaw
   * @param {string} publicKeyHex publicKey in hex encoded string format
   * @param {string} signatureHex signature in hex encoded string format
   * @param {TxRaw} unsignedTx raw transaction
   * @returns {TxRaw} Signed raw transaction
   */
  createSignedTxRaw(
    publicKeyHex: string,
    signatureHex: string,
    unsignedTx: { bodyBytes: Uint8Array; authInfoBytes: Uint8Array }
  ): TxRaw {
    const stdSignature = encodeSecp256k1Signature(fromHex(publicKeyHex), fromHex(signatureHex));
    return TxRaw.fromPartial({
      bodyBytes: unsignedTx.bodyBytes,
      authInfoBytes: unsignedTx.authInfoBytes,
      signatures: [fromBase64(stdSignature.signature)],
    });
  }

  /**
   * Decodes a raw transaction into a DecodedTxRaw and checks if it has non empty signatures
   * @param {string} rawTransaction
   * @returns {boolean} true if transaction is signed else false
   */
  isSignedRawTx(rawTransaction: string): boolean {
    const decodedTx = this.getDecodedTxFromRawBase64(rawTransaction);
    if (decodedTx.signatures.length > 0) {
      return true;
    }
    return false;
  }

  /**
   * Returns whether or not the string is a valid protocol public key
   * @param {string | undefined} publicKey - the  public key to be validated
   */
  validatePublicKey(publicKey: string | undefined) {
    if (publicKey !== undefined) {
      try {
        new KeyPair({ pub: publicKey });
      } catch {
        throw new InvalidTransactionError(`Invalid Public Key`);
      }
    }
  }

  /**
   * Creates a sign doc from an cosmos like transaction @see CosmosLikeTransaction
   * @Precondition cosmosLikeTransaction.accountNumber and cosmosLikeTransaction.chainId must be defined
   * @param {CosmosLikeTransaction} cosmosLikeTransaction
   * @returns {SignDoc} sign doc
   */
  createSignDoc(
    cosmosLikeTransaction: CosmosLikeTransaction<CustomMessage>,
    accountNumber: number | undefined,
    chainId: string | undefined
  ): SignDoc {
    if (!accountNumber) {
      throw new Error('accountNumber is required to create a sign doc');
    }
    if (!chainId) {
      throw new Error('chainId is required to create a sign doc');
    }
    if (!cosmosLikeTransaction) {
      throw new Error('cosmosLikeTransaction is required to create a sign doc');
    }
    const txRaw = this.createTxRawFromCosmosLikeTransaction(cosmosLikeTransaction);
    return makeSignDoc(txRaw.bodyBytes, txRaw.authInfoBytes, chainId, accountNumber);
  }

  /**
   * Returns whether or not the string is a valid hex
   * @param hexString - hex string format
   * @returns {boolean} true if string is hex else false
   */
  isValidHexString(hexString: string): boolean {
    return /^[0-9A-Fa-f]*$/.test(hexString);
  }

  /**
   * Validates the WithdrawDelegatorRewardsMessage
   * @param {WithdrawDelegatorRewardsMessage} withdrawRewardsMessage - The WithdrawDelegatorRewardsMessage to validate.
   * @throws {InvalidTransactionError} Throws an error if the validatorAddress or delegatorAddress is invalid or missing.
   */
  validateWithdrawRewardsMessage(withdrawRewardsMessage: WithdrawDelegatorRewardsMessage) {
    if (
      !withdrawRewardsMessage.validatorAddress ||
      !this.isValidValidatorAddress(withdrawRewardsMessage.validatorAddress)
    ) {
      throw new InvalidTransactionError(
        `Invalid WithdrawDelegatorRewardsMessage validatorAddress: ` + withdrawRewardsMessage.validatorAddress
      );
    }
    if (!withdrawRewardsMessage.delegatorAddress || !this.isValidAddress(withdrawRewardsMessage.delegatorAddress)) {
      throw new InvalidTransactionError(
        `Invalid WithdrawDelegatorRewardsMessage delegatorAddress: ` + withdrawRewardsMessage.delegatorAddress
      );
    }
  }

  /**
   * Helper method to check if the specified properties in an object are missing or null.
   * @param {Object} obj - The object to check.
   * @param {string[]} keys - An array of property keys to check.
   * @throws {Error} Throws an error if any of the specified properties are missing or null.
   */
  isObjPropertyNull(obj: { [key: string]: any }, keys: Array<string>) {
    for (const key of keys) {
      if (obj[key] == null) {
        throw new Error(`Missing or null value for property ${key}`);
      }
    }
  }

  /**
   * Validates the DelegateOrUndelegeteMessage
   * @param {DelegateOrUndelegeteMessage} delegateMessage - The DelegateOrUndelegeteMessage to validate.
   * @throws {InvalidTransactionError} Throws an error if the validatorAddress, delegatorAddress, or amount is invalid or missing.
   */
  validateDelegateOrUndelegateMessage(delegateMessage: DelegateOrUndelegeteMessage) {
    this.isObjPropertyNull(delegateMessage, ['validatorAddress', 'delegatorAddress']);

    if (!this.isValidValidatorAddress(delegateMessage.validatorAddress)) {
      throw new InvalidTransactionError(
        `Invalid DelegateOrUndelegeteMessage validatorAddress: ` + delegateMessage.validatorAddress
      );
    }
    if (!this.isValidAddress(delegateMessage.delegatorAddress)) {
      throw new InvalidTransactionError(
        `Invalid DelegateOrUndelegeteMessage delegatorAddress: ` + delegateMessage.delegatorAddress
      );
    }
    this.validateAmount(delegateMessage.amount);
  }

  /**
   * Validates the RedelegateMessage
   * @param {DelegateOrUndelegeteMessage} redelegateMessage - The RedelegateMessage to validate.
   * @throws {InvalidTransactionError} Throws an error if the validatorSrcAddress, validatorDstAddress, delegatorAddress, or amount is invalid or missing.
   */
  validateRedelegateMessage(redelegateMessage: RedelegateMessage) {
    this.isObjPropertyNull(redelegateMessage, ['validatorSrcAddress', 'validatorDstAddress', 'delegatorAddress']);

    if (!this.isValidValidatorAddress(redelegateMessage.validatorSrcAddress)) {
      throw new InvalidTransactionError(
        `Invalid RedelegateMessage validatorSrcAddress: ` + redelegateMessage.validatorSrcAddress
      );
    }
    if (!this.isValidValidatorAddress(redelegateMessage.validatorDstAddress)) {
      throw new InvalidTransactionError(
        `Invalid RedelegateMessage validatorDstAddress: ` + redelegateMessage.validatorDstAddress
      );
    }
    if (!this.isValidAddress(redelegateMessage.delegatorAddress)) {
      throw new InvalidTransactionError(
        `Invalid DelegateOrUndelegeteMessage delegatorAddress: ` + redelegateMessage.delegatorAddress
      );
    }
    this.validateAmount(redelegateMessage.amount);
  }

  /**
   * Validates the CustomMessage
   * @param {CustomMessage} customMessage - The CustomMessage to validate.
   * @throws {InvalidTransactionError} Throws an error if the custom message is invalid or missing required fields.
   * @throws {NotSupported} Throws an error if the custom message data is not supported.
   */
  validateCustomMessage(customMessage: CustomMessage) {
    throw new NotSupported('Custom message data not supported');
  }

  /**
   * Validates the MessageData
   * @param {MessageData} messageData - The MessageData to validate.
   * @throws {InvalidTransactionError} Throws an error if the messageData is invalid or missing required fields.
   */
  validateMessageData(messageData: MessageData<CustomMessage>): void {
    if (messageData == null) {
      throw new InvalidTransactionError(`Invalid MessageData: undefined`);
    }
    if (messageData.typeUrl == null || this.getTransactionTypeFromTypeUrl(messageData.typeUrl) == null) {
      throw new InvalidTransactionError(`Invalid MessageData typeurl: ` + messageData.typeUrl);
    }

    const type = this.getTransactionTypeFromTypeUrl(messageData.typeUrl);
    switch (type) {
      case TransactionType.Send: {
        const value = messageData.value as SendMessage;
        this.validateSendMessage(value);
        break;
      }
      case TransactionType.StakingActivate:
      case TransactionType.StakingDeactivate: {
        const value = messageData.value as DelegateOrUndelegeteMessage;
        this.validateDelegateOrUndelegateMessage(value);
        break;
      }
      case TransactionType.StakingWithdraw: {
        const value = messageData.value as WithdrawDelegatorRewardsMessage;
        this.validateWithdrawRewardsMessage(value);
        break;
      }
      case TransactionType.ContractCall: {
        const value = messageData.value as ExecuteContractMessage;
        this.validateExecuteContractMessage(value, TransactionType.ContractCall);
        break;
      }
      case TransactionType.StakingRedelegate: {
        const value = messageData.value as RedelegateMessage;
        this.validateRedelegateMessage(value);
        break;
      }
      case TransactionType.CustomTx: {
        const value = messageData.value as CustomMessage;
        this.validateCustomMessage(value);
        break;
      }
      default:
        throw new InvalidTransactionError(`Invalid MessageData TypeUrl is not supported: ` + messageData.typeUrl);
    }
  }

  /**
   * Validates the Cosmos-like transaction.
   * @param {CosmosLikeTransaction} tx - The transaction to validate.
   * @throws {InvalidTransactionError} Throws an error if the transaction is invalid or missing required fields.
   */
  validateTransaction(tx: CosmosLikeTransaction<CustomMessage>): void {
    this.validateSequence(tx.sequence);
    this.validateGasBudget(tx.gasBudget);
    this.validatePublicKey(tx.publicKey);
    if (tx.sendMessages === undefined || tx.sendMessages.length === 0) {
      throw new InvalidTransactionError('Invalid transaction: messages is required');
    } else {
      tx.sendMessages.forEach((message) => this.validateMessageData(message));
    }
  }

  /**
   * Creates a Cosmos-like transaction.
   * @param {number} sequence - The sender address sequence number for the transaction.
   * @param {MessageData[]} messages - The array of message data for the transaction.
   * @param {FeeData} gasBudget - The fee data for the transaction.
   * @param {string} [publicKey] - The public key associated with the sender.
   * @param {string} [memo] - The memo for the transaction.
   * @returns {CosmosLikeTransaction} Returns the created Cosmos-like transaction.
   * @throws {InvalidTransactionError} Throws an error if the created transaction is invalid.
   */
  createTransaction(
    sequence: number,
    messages: MessageData<CustomMessage>[],
    gasBudget: FeeData,
    publicKey?: string,
    memo?: string
  ): CosmosLikeTransaction<CustomMessage> {
    const cosmosLikeTxn = {
      sequence: sequence,
      sendMessages: messages,
      gasBudget: gasBudget,
      publicKey: publicKey,
      memo: memo,
    };
    this.validateTransaction(cosmosLikeTxn);
    return cosmosLikeTxn;
  }

  /**
   * Creates a Cosmos-like transaction with a hash.
   * @param {number} sequence - The sender address sequence number for the transaction.
   * @param {MessageData[]} messages - The array of message data for the transaction.
   * @param {FeeData} gasBudget - The fee data for the transaction.
   * @param {string} [publicKey] - The public key associated with the transaction.
   * @param {Buffer} [signature] - The signature for the transaction.
   * @param {string} [memo] - The memo for the transaction.
   * @returns {CosmosLikeTransaction} Returns the created Cosmos-like transaction with the hash and signature if provided.
   */
  createTransactionWithHash(
    sequence: number,
    messages: MessageData<CustomMessage>[],
    gasBudget: FeeData,
    publicKey?: string,
    signature?: Buffer,
    memo?: string
  ): CosmosLikeTransaction<CustomMessage> {
    const cosmosLikeTxn = this.createTransaction(sequence, messages, gasBudget, publicKey, memo);
    let hash = constants.UNAVAILABLE_TEXT;
    if (signature !== undefined) {
      const unsignedTx = this.createTxRawFromCosmosLikeTransaction(cosmosLikeTxn);
      const signedTx = TxRaw.fromPartial({
        bodyBytes: unsignedTx.bodyBytes,
        authInfoBytes: unsignedTx.authInfoBytes,
        signatures: [signature],
      });
      hash = createHash('sha256')
        .update(TxRaw.encode(signedTx).finish())
        .digest()
        .toString('hex')
        .toLocaleUpperCase('en-US');
      return { ...cosmosLikeTxn, hash: hash, signature: signature };
    }
    return { ...cosmosLikeTxn, hash: hash };
  }

  /**
   * Deserializes base64 enocded raw transaction string into @see CosmosLikeTransaction
   * @param {string} rawTx base64 enocded raw transaction string
   * @returns {CosmosLikeTransaction} Deserialized cosmosLikeTransaction
   */
  deserializeTransaction(rawTx: string): CosmosLikeTransaction<CustomMessage> {
    const decodedTx = this.getDecodedTxFromRawBase64(rawTx);
    const typeUrl = this.getTypeUrlFromDecodedTx(decodedTx);
    const type: TransactionType | undefined = this.getTransactionTypeFromTypeUrl(typeUrl);
    let sendMessageData: MessageData<CustomMessage>[];
    if (type === TransactionType.Send) {
      sendMessageData = this.getSendMessageDataFromDecodedTx(decodedTx);
    } else if (type === TransactionType.StakingActivate || type === TransactionType.StakingDeactivate) {
      sendMessageData = this.getDelegateOrUndelegateMessageDataFromDecodedTx(decodedTx);
    } else if (type === TransactionType.StakingWithdraw) {
      sendMessageData = this.getWithdrawRewardsMessageDataFromDecodedTx(decodedTx);
    } else if (type === TransactionType.ContractCall) {
      sendMessageData = this.getExecuteContractMessageDataFromDecodedTx(decodedTx);
    } else if (type === TransactionType.StakingRedelegate) {
      sendMessageData = this.getRedelegateMessageDataFromDecodedTx(decodedTx);
    } else if (type === TransactionType.CustomTx) {
      sendMessageData = this.getCustomMessageDataFromDecodedTx(decodedTx);
    } else {
      throw new Error('Transaction type not supported: ' + typeUrl);
    }
    const sequence = this.getSequenceFromDecodedTx(decodedTx);
    const gasBudget = this.getGasBudgetFromDecodedTx(decodedTx);
    const publicKey = this.getPublicKeyFromDecodedTx(decodedTx);
    const signature = decodedTx.signatures?.[0] !== undefined ? Buffer.from(decodedTx.signatures[0]) : undefined;
    return this.createTransactionWithHash(
      sequence,
      sendMessageData,
      gasBudget,
      publicKey,
      signature,
      decodedTx.body?.memo
    );
  }

  /**
   * Validates an array of coin amounts.
   * @param {Coin[]} amountArray - The array of coin amounts to validate.
   * @param {TransactionType} transactionType - optional field for transaction type
   */
  validateAmountData(amountArray: Coin[], transactionType?: TransactionType): void {
    amountArray.forEach((coinAmount) => {
      this.validateAmount(coinAmount, transactionType);
    });
  }

  /**
   * Validates the gas limit and gas amount for a transaction.
   * @param {FeeData} gasBudget - The gas budget to validate.
   * @throws {InvalidTransactionError} Throws an error if the gas budget is invalid.
   */
  validateGasBudget(gasBudget: FeeData): void {
    if (gasBudget.gasLimit <= 0) {
      throw new InvalidTransactionError('Invalid gas limit ' + gasBudget.gasLimit);
    }
    this.validateAmountData(gasBudget.amount);
  }

  /**
   * Validates a send message for a transaction.
   * @param {SendMessage} sendMessage - The send message to validate.
   * @throws {InvalidTransactionError} Throws an error if the send message is invalid.
   */
  validateSendMessage(sendMessage: SendMessage) {
    if (!sendMessage.toAddress || !this.isValidAddress(sendMessage.toAddress)) {
      throw new InvalidTransactionError(`Invalid SendMessage toAddress: ` + sendMessage.toAddress);
    }
    if (!sendMessage.fromAddress || !this.isValidAddress(sendMessage.fromAddress)) {
      throw new InvalidTransactionError(`Invalid SendMessage fromAddress: ` + sendMessage.fromAddress);
    }
    this.validateAmountData(sendMessage.amount);
  }

  /**
   * Validates a coin amount.
   * @param {Coin} amount - The coin amount to validate.
   * @param {TransactionType} transactionType - optional field for transaction type
   * @throws {InvalidTransactionError} Throws an error if the coin amount is invalid.
   */
  validateAmount(amount: Coin, transactionType?: TransactionType): void {
    throw new NotImplementedError('validateAmount not implemented');
  }

  /**
   * Checks if a cosmos like Bech32 address matches given regular expression and
   * validates memoId if present
   * @param {string} address
   * @param {RegExp} regExp Regular expression to validate the root address against after trimming the memoId
   * @returns {boolean} true if address is valid
   */
  protected isValidCosmosLikeAddressWithMemoId(address: string, regExp: RegExp): boolean {
    if (typeof address !== 'string') return false;
    const addressArray = address.split('?memoId=');
    if (
      ![1, 2].includes(addressArray.length) || // should have at most one occurrence of 'memoId='
      !this.isValidBech32AddressMatchingRegex(addressArray[0], regExp) ||
      (addressArray[1] && !this.isValidMemoId(addressArray[1]))
    ) {
      return false;
    }
    return true;
  }

  /**
   * Checks if address is valid Bech32 and matches given regular expression
   * @param {string} address
   * @param {RegExp} regExp Regular expression to validate the address against
   * @returns {boolean} true if address is valid
   */
  protected isValidBech32AddressMatchingRegex(address: string, regExp: RegExp): boolean {
    try {
      fromBech32(address);
    } catch (e) {
      return false;
    }
    return regExp.test(address);
  }

  /**
   * Return boolean indicating whether a memo id is valid
   *
   * @param memoId memo id
   * @returns true if memo id is valid
   */
  isValidMemoId(memoId: string): boolean {
    // Allow alphanumeric memo IDs (including uppercase and lowercase letters)
    const alphanumericRegex = /^[0-9a-zA-Z]+$/;

    // Check if the memoId is alphanumeric
    if (!alphanumericRegex.test(memoId)) {
      return false;
    }

    // If the memoId is purely numeric, ensure it is a positive integer
    if (/^\d+$/.test(memoId)) {
      const memoIdNumber = new BigNumber(memoId);
      return memoIdNumber.gte(0) && memoIdNumber.isInteger();
    }

    return true;
  }

  /**
   * Validates if the address matches with regex @see accountAddressRegex
   * @param {string} address
   * @returns {boolean} - the validation result
   */
  isValidValidatorAddress(address: string): boolean {
    throw new NotImplementedError('isValidValidatorAddress not implemented');
  }

  /**
   * Validates if the address matches with regex @see accountAddressRegex
   * @param {string} address
   * @returns {boolean} - the validation result
   */
  isValidAddress(address: string): boolean {
    throw new NotImplementedError('isValidAddress not implemented');
  }

  /**
   * Validates if the address matches with regex @see contractAddressRegex
   * @param {string} address
   * @returns {boolean} - the validation result
   */
  isValidContractAddress(address: string): boolean {
    throw new NotImplementedError('isValidContractAddress not implemented');
  }

  /**
   * Validates a execute contract message
   * @param {ExecuteContractMessage} message - The execute contract message to validate
   * @param {TransactionType} transactionType - optional field for transaction type
   * @throws {InvalidTransactionError} Throws an error if the message is invalid
   */
  validateExecuteContractMessage(message: ExecuteContractMessage, transactionType?: TransactionType) {
    if (!message.contract || !this.isValidContractAddress(message.contract)) {
      throw new InvalidTransactionError(`Invalid ExecuteContractMessage contract address: ` + message.contract);
    }
    if (!message.sender || !this.isValidAddress(message.sender)) {
      throw new InvalidTransactionError(`Invalid ExecuteContractMessage sender address: ` + message.sender);
    }
    if (!message.msg) {
      throw new InvalidTransactionError(`Invalid ExecuteContractMessage msg: ` + message.msg);
    }
    if (message.funds) {
      this.validateAmountData(message.funds, transactionType);
    }
  }

  /**
   * Get coin specific hash function
   * @returns {Hash} The hash function
   */
  getHashFunction(): Hash {
    return createHash('sha256');
  }
}

const utils = new CosmosUtils();

export default utils;

Выполнить команду


Для локальной разработки. Не используйте в интернете!