PHP WebShell

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

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

import * as _ from 'lodash';
import { AccountId, PrivateKey, PublicKey, TokenId, TransactionId } from '@hashgraph/sdk';
import { proto } from '@hashgraph/proto';
import BigNumber from 'bignumber.js';
import * as stellar from 'stellar-sdk';
import { AddressDetails } from './iface';
import url from 'url';
import { toHex, toUint8Array, UtilsError } from '@bitgo/sdk-core';
import { BaseCoin, coins, HederaToken } from '@bitgo/statics';
export { toHex, toUint8Array };

const MAX_TINYBARS_AMOUNT = new BigNumber(2).pow(63).minus(1);

/**
 * Returns whether the string is a valid Hedera account address
 *
 * In any form, `shard` and `realm` are assumed to be 0 if not provided.
 *
 * @param {string} address - The address to be validated
 * @returns {boolean} - The validation result
 */
export function isValidAddress(address: string): boolean {
  const addressArray = address.split('?memoId=');

  if (
    _.isEmpty(address) ||
    ![1, 2].includes(addressArray.length) ||
    !addressArray[0].match(/^\d+(?:(?=\.)(\.\d+){2}|(?!\.))$/) ||
    (addressArray[1] && !isValidMemo(addressArray[1]))
  ) {
    return false;
  }

  try {
    const acc = AccountId.fromString(addressArray[0]);
    return !_.isNaN(acc.num);
  } catch (e) {
    return false;
  }
}

/**
 * Returns whether the string is a valid Hedera transaction id
 *
 * @param {string} txId - The transaction id to be validated
 * @returns {boolean} - The validation result
 */
export function isValidTransactionId(txId: string): boolean {
  if (_.isEmpty(txId)) {
    return false;
  }
  try {
    const tx = TransactionId.fromString(txId);
    if (_.isNil(tx.accountId)) {
      return false;
    }
    return !_.isNaN(tx.accountId.num);
  } catch (e) {
    return false;
  }
}

/**
 Returns whether the string is a valid Hedera public key
 *
 * @param {string} key - The public key to be validated
 * @returns {boolean} - The validation result
 */
export function isValidPublicKey(key: string): boolean {
  if (_.isEmpty(key)) {
    return false;
  }
  try {
    const pubKey = PublicKey.fromString(key.toLowerCase());
    return !_.isNaN(pubKey.toString());
  } catch (e) {
    return false;
  }
}

/**
 * Checks whether nodeJS.process exist and if a node version is defined to determine if this is an nodeJS environment
 *
 * @returns {boolean} - The validation result
 */
export function isNodeEnvironment(): boolean {
  return typeof process !== 'undefined' && typeof process.versions.node !== 'undefined';
}

/**
 * Calculate the current time with nanoseconds precision
 *
 * @returns {string} - The current time in seconds
 */
export function getCurrentTime(): string {
  if (isNodeEnvironment()) {
    const nanos = process.hrtime()[1];
    const seconds = (Date.now() * 1000000 + nanos) / 1000000000;
    return seconds.toFixed(9);
  } else {
    return (performance.timeOrigin + performance.now()).toFixed(9);
  }
}

/**
 * Returns whether the string is a valid timestamp
 *
 * Nanoseconds are optional and can be passed after a dot, for example: 1595374723.356981689
 *
 * @param {string} time - The timestamp to be validated
 * @returns {boolean} - The validation result
 */
export function isValidTimeString(time: string): boolean {
  return /^\d+(\.\d+)?$/.test(time);
}

/**
 * Returns whether the string is a valid amount number
 *
 * @param {string} amount - The string to validate
 * @returns {boolean} - The validation result
 */
export function isValidAmount(amount: string): boolean {
  const bigNumberAmount = new BigNumber(amount);
  return (
    bigNumberAmount.isInteger() &&
    bigNumberAmount.isGreaterThanOrEqualTo(0) &&
    bigNumberAmount.isLessThanOrEqualTo(MAX_TINYBARS_AMOUNT)
  );
}

/**
 * Returns whether the provided raw transaction accommodates to bitgo's preferred format
 *
 * @param {any} rawTransaction - The raw transaction to be checked
 * @returns {boolean} - The validation result
 */
export function isValidRawTransactionFormat(rawTransaction: any): boolean {
  const isAlphaNumeric = typeof rawTransaction === 'string' && /^[\da-fA-F]+$/.test(rawTransaction);
  const isValidBuffer = Buffer.isBuffer(rawTransaction) && !!Uint8Array.from(rawTransaction);

  return isAlphaNumeric || isValidBuffer;
}

/**
 * Returns a string representation of an {proto.IAccountID} object
 *
 * @param {proto.IAccountID} accountId - Account id to be cast to string
 * @returns {string} - The string representation of the {proto.IAccountID}
 */
export function stringifyAccountId({ shardNum, realmNum, accountNum }: proto.IAccountID): string {
  return `${shardNum || 0}.${realmNum || 0}.${accountNum}`;
}

/**
 * Returns a string representation of an {proto.ITokenID} object
 *
 * @param {proto.ITokenID} - token id to be cast to string
 * @returns {string} - the string representation of the {proto.ITokenID}
 */
export function stringifyTokenId({ shardNum, realmNum, tokenNum }: proto.ITokenID): string {
  return `${shardNum || 0}.${realmNum || 0}.${tokenNum}`;
}

/**
 * Returns a string representation of an {proto.ITimestamp} object
 *
 * @param {proto.ITimestamp} timestamp - Timestamp to be cast to string
 * @returns {string} - The string representation of the {proto.ITimestamp}
 */
export function stringifyTxTime({ seconds, nanos }: proto.ITimestamp): string {
  return `${seconds}.${nanos}`;
}

/**
 * Remove the specified prefix from a string only if it starts with that prefix
 *
 * @param {string} prefix - The prefix to be removed
 * @param {string} key - The original string, usually a private or public key
 * @returns {string} - The string without prefix
 */
export function removePrefix(prefix: string, key: string): string {
  if (key.startsWith(prefix)) {
    return key.slice(prefix.length);
  }
  return key;
}

/**
 * Check if this is a valid memo
 *
 * @param {string} memo
 * @returns {boolean}
 */
export function isValidMemo(memo: string): boolean {
  return !(_.isEmpty(memo) || Buffer.from(memo).length > 100);
}

/**
 * Uses the native hashgraph SDK function to get a raw key.
 *
 * @param {string} prv - Private key
 * @returns {PrivateKey}
 */
export function createRawKey(prv: string): PrivateKey {
  return PrivateKey.fromString(prv);
}

/**
 * Converts a stellar public key to ed25519 hex format
 *
 * @param {string} stellarPub
 * @returns {string}
 */
export function convertFromStellarPub(stellarPub: string): string {
  if (!stellar.StrKey.isValidEd25519PublicKey(stellarPub)) {
    throw new Error('Not a valid stellar pub.');
  }

  const rawKey: Buffer = stellar.StrKey.decodeEd25519PublicKey(stellarPub);
  return rawKey.toString('hex');
}

/**
 * Checks if two addresses have the same base address
 *
 * @param {String} address
 * @param {String} baseAddress
 * @returns {boolean}
 */
export function isSameBaseAddress(address: string, baseAddress: string): boolean {
  if (!isValidAddressWithPaymentId(address)) {
    throw new UtilsError(`invalid address: ${address}`);
  }
  return getBaseAddress(address) === getBaseAddress(baseAddress);
}

/**
 * Returns the base address portion of an address
 *
 * @param {String} address
 * @returns {String} - the base address
 */
export function getBaseAddress(address: string): string {
  const addressDetails = getAddressDetails(address);
  return addressDetails.address;
}

/**
 * Process address into address and memo id
 *
 * @param {string} rawAddress
 * @returns {AddressDetails} - object containing address and memo id
 */
export function getAddressDetails(rawAddress: string): AddressDetails {
  const addressDetails = url.parse(rawAddress);
  const queryDetails = addressDetails.query ? new URLSearchParams(addressDetails.query) : undefined;
  const baseAddress = addressDetails.pathname as string;
  if (!isValidAddress(baseAddress)) {
    throw new UtilsError(`invalid address: ${rawAddress}`);
  }

  // address doesn't have a memo id or memoId is empty
  if (baseAddress === rawAddress) {
    return {
      address: rawAddress,
      memoId: undefined,
    };
  }

  if (!queryDetails || _.isNil(queryDetails.get('memoId'))) {
    // if there are more properties, the query details need to contain the memo id property
    throw new UtilsError(`invalid address with memo id: ${rawAddress}`);
  }
  const memoId = queryDetails.get('memoId') as string;
  if (!isValidMemo(memoId)) {
    throw new UtilsError(`invalid address: '${rawAddress}', memoId is not valid`);
  }

  return {
    address: baseAddress,
    memoId,
  };
}

/**
 * Validate and return address with appended memo id
 *
 * @param {AddressDetails} addressDetails - Address which to append memo id
 * @returns {string} - Address with appended memo id
 */
export function normalizeAddress({ address, memoId }: AddressDetails): string {
  if (memoId && isValidMemo(memoId)) {
    return `${address}?memoId=${memoId}`;
  }
  return address;
}

/**
 * Return boolean indicating whether input is a valid address with memo id
 *
 * @param {string} address - Address in the form <address>?memoId=<memoId>
 * @returns {boolean} - True if input is a valid address
 */
export function isValidAddressWithPaymentId(address: string): boolean {
  try {
    const addressDetails = getAddressDetails(address);
    return address === normalizeAddress(addressDetails);
  } catch (e) {
    return false;
  }
}

/**
 * Build hedera {proto.TokenID} object from token ID string
 *
 * @param {string} tokenID - The token ID to build
 * @returns {proto.TokenID} - The resulting proto TokenID object
 */
export function buildHederaTokenID(tokenID: string): proto.TokenID {
  const tokenData = TokenId.fromString(tokenID);
  return new proto.TokenID({
    tokenNum: tokenData.num,
    realmNum: tokenData.realm,
    shardNum: tokenData.shard,
  });
}

/**
 * Build hedera {proto.AccountID} object from account ID string
 *
 * @param {string} accountID - The account ID to build
 * @returns {proto} - The resulting proto AccountID object
 */
export function buildHederaAccountID(accountID: string): proto.AccountID {
  const accountId = AccountId.fromString(accountID);
  return new proto.AccountID({
    shardNum: accountId.shard,
    realmNum: accountId.realm,
    accountNum: accountId.num,
  });
}

/**
 * Check if Hedera token ID is valid and supported
 *
 * @param {string} tokenId - The token ID to validate
 * @returns {boolean} - True if tokenId is valid and supported
 */
export function isValidHederaTokenID(tokenId: string): boolean {
  const isFormatValid = !_.isEmpty(tokenId) && !!tokenId.match(/^\d+(?:(?=\.)(\.\d+){2}|(?!\.))$/);
  const isTokenSupported = getHederaTokenNameFromId(tokenId) !== undefined;

  return isFormatValid && isTokenSupported;
}

/**
 * Get the associated hedera token ID from token name, if supported
 *
 * @param {string} tokenName - The hedera token name
 * @returns {boolean} - The associated token ID or undefined if not supported
 */
export function getHederaTokenIdFromName(tokenName: string): string | undefined {
  if (coins.has(tokenName)) {
    const token = coins.get(tokenName);
    if (token.isToken && token instanceof HederaToken) {
      return token.tokenId;
    }
  }

  return undefined;
}

/**
 * Get the associated hedera token from token ID, if supported
 *
 * @param tokenId - The token address
 * @returns {BaseCoin} - BaseCoin object for the matching token
 */
export function getHederaTokenNameFromId(tokenId: string): Readonly<BaseCoin> | undefined {
  const tokensArray = coins
    .filter((coin) => {
      return coin instanceof HederaToken && coin.tokenId === tokenId;
    })
    .map((token) => token); // flatten coin map to array

  return tokensArray.length > 0 ? tokensArray[0] : undefined;
}

/**
 * Return boolean indicating whether input is a valid token transfer transaction
 *
 * @param {proto.ICryptoTransferTransactionBody | null} transferTxBody is a transfer transaction body
 * @returns {boolean} true is input is a valid token transfer transaction
 */
export function isTokenTransfer(transferTxBody: proto.ICryptoTransferTransactionBody | null): boolean {
  return !!transferTxBody && !!transferTxBody.tokenTransfers && transferTxBody.tokenTransfers.length > 0;
}

/** validates a startTime string to be a valid timestamp and in the future
 * @param {string} startTime - The startTime to be validated
 * @throws {Error} - if startTime is not a valid timestamp or is in the past
 * @returns {void}
 * */
export function validateStartTime(startTime: string): void {
  if (!isValidTimeString(startTime)) {
    throw new Error('invalid startTime, got: ' + startTime);
  }
  const currentTime = getCurrentTime();
  const startTimeFixed = normalizeStarttime(startTime);
  const result = new BigNumber(startTimeFixed).isLessThanOrEqualTo(currentTime);
  if (result) {
    throw new Error('startTime must be a future timestamp, got: ' + startTime);
  }
}

export function normalizeStarttime(startTime: string): string {
  return new BigNumber(startTime).toFixed(9);
}

/**
 * Await for a given amount of time in milliseconds
 * @param ms - The amount of time to wait in milliseconds
 * @returns {Promise<void>} - A promise that resolves after the given amount of time
 */
export function sleep(ms: number): Promise<void> {
  console.log(`sleeping for ${ms} ms`);
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Check if the startTime is within the broadcast window (5 seconds after and 175 seconds after the startTime)
 */
export function shouldBroadcastNow(startTime: string): boolean {
  const startTimeFixed = normalizeStarttime(startTime);
  const currentTime = getCurrentTime();
  // startTime plus 5 seconds
  const startingTimeWindow = new BigNumber(startTimeFixed).plus(5).toFixed(9);
  // startTime plus 170 seconds
  const endingTimeWindow = new BigNumber(startTimeFixed).plus(175).toFixed(9);

  if (new BigNumber(currentTime).isGreaterThan(endingTimeWindow)) {
    throw new Error(
      'startTime window expired, got: ' +
        startTimeFixed +
        ' - currentTime: ' +
        currentTime +
        ' - endingTimeWindow ' +
        endingTimeWindow
    );
  }

  return (
    new BigNumber(currentTime).isGreaterThanOrEqualTo(startingTimeWindow) &&
    new BigNumber(currentTime).isLessThanOrEqualTo(endingTimeWindow)
  );
}

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


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