PHP WebShell

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

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

import {
  BaseUtils,
  BuildTransactionError,
  InvalidTransactionError,
  isValidEd25519PublicKey,
  ParseTransactionError,
  Recipient,
  TransactionType,
} from '@bitgo/sdk-core';
import BigNumber from 'bignumber.js';
import { SUI_ADDRESS_LENGTH } from './constants';
import { isPureArg } from './mystenlab/types/sui-bcs';
import { BCS, fromB64 } from '@mysten/bcs';
import {
  MethodNames,
  RequestAddStake,
  RequestWalrusStakeWithPool,
  RequestWalrusWithdrawStake,
  StakingProgrammableTransaction,
  WalrusStakingProgrammableTransaction,
  WalrusWithdrawStakeProgrammableTransaction,
  SuiObjectInfo,
  SuiProgrammableTransaction,
  SuiTransaction,
  SuiTransactionType,
} from './iface';
import { Buffer } from 'buffer';
import {
  isValidSuiAddress,
  normalizeSuiAddress,
  normalizeSuiObjectId,
  SUI_TYPE_ARG,
  SuiJsonValue,
  SuiObjectRef,
} from './mystenlab/types';
import {
  builder,
  MergeCoinsTransaction,
  MoveCallTransaction,
  ObjectCallArg,
  SplitCoinsTransaction,
  TransactionBlockInput,
  TransactionType as TransactionCommandType,
} from './mystenlab/builder';
import { SIGNATURE_SCHEME_TO_FLAG } from './keyPair';
import blake2b from '@bitgo/blake2b';
import { TRANSACTION_DATA_MAX_SIZE } from './mystenlab/builder/TransactionDataBlock';
import { makeRPC } from './rpcClient';
import assert from 'assert';
import { BaseNetwork, coins, SuiCoin } from '@bitgo/statics';

export function isImmOrOwnedObj(obj: ObjectCallArg['Object']): obj is { ImmOrOwned: SuiObjectRef } {
  return 'ImmOrOwned' in obj;
}

export class Utils implements BaseUtils {
  /** @inheritdoc */
  isValidBlockId(hash: string): boolean {
    throw new Error('Method not implemented.');
  }

  /** @inheritdoc */
  isValidPrivateKey(key: string): boolean {
    throw new Error('Method not implemented.');
  }

  /** @inheritdoc */
  isValidPublicKey(key: string): boolean {
    return isValidEd25519PublicKey(key);
  }

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

  /** @inheritdoc */
  isValidTransactionId(txId: string): boolean {
    throw new Error('Method not implemented.');
  }

  /**
   * Checks if raw transaction can be deserialized
   *
   * @param {string} rawTransaction - transaction in base64 string format
   * @returns {boolean} - the validation result
   */
  isValidRawTransaction(rawTransaction: string): boolean {
    try {
      const data = fromB64(rawTransaction);
      const deserialized = builder.de('TransactionData', data);
      builder.ser('TransactionData', deserialized, { maxSize: TRANSACTION_DATA_MAX_SIZE });
      return true;
    } catch (e) {
      return false;
    }
  }

  /**
   * Check the raw transaction has a valid format in the blockchain context, throw otherwise.
   *
   * @param {string} rawTransaction - Transaction in base64 string  format
   */
  validateRawTransaction(rawTransaction: string): void {
    if (!rawTransaction) {
      throw new ParseTransactionError('Invalid raw transaction: Undefined');
    }
    if (!this.isValidRawTransaction(rawTransaction)) {
      throw new ParseTransactionError('Invalid raw transaction');
    }
  }

  /**
   * Validates addresses to check if all exist and are valid Sui public keys
   *
   * @param {string} addresses The address to be validated
   * @param {string} fieldName Name of the field to validate, its needed to return which field is failing on case of error.
   */
  validateAddresses(addresses: string[], fieldName: string): void {
    for (const address of addresses) {
      this.validateAddress(address, fieldName);
    }
  }

  /**
   * Validates address to check if it exists and is a valid Sui public key
   *
   * @param {string} address The address to be validated
   * @param {string} fieldName Name of the field to validate, its needed to return which field is failing on case of error.
   */
  validateAddress(address: string, fieldName: string): void {
    if (!address || !isValidSuiAddress(normalizeSuiAddress(address))) {
      throw new BuildTransactionError(`Invalid or missing ${fieldName}, got: ${address}`);
    }
  }

  /** @inheritdoc */
  isValidAddress(address: string): boolean {
    return this.isHex(address) && this.getHexByteLength(address) === SUI_ADDRESS_LENGTH;
  }

  isHex(value: string): boolean {
    return /^(0x|0X)?[a-fA-F0-9]+$/.test(value) && value.length % 2 === 0;
  }

  getHexByteLength(value: string): number {
    // return /^(0x|0X)/.test(value) ? (value.length - 2) / 2 : value.length / 2;
    return /^(0x|0X)/.test(value) ? (value.length - 2) / 2 : value.length / 2;
  }

  /**
   * Returns whether or not the string is a valid amount
   *
   * @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;
  }

  /**
   * Returns whether or not the string is a valid amount
   *
   * @param {number} amounts - the amount to validate
   * @returns {boolean} - the validation result
   */
  isValidAmount(amount: string | number): boolean {
    const bigNumberAmount = new BigNumber(Number(amount));
    if (!bigNumberAmount.isInteger() || bigNumberAmount.isLessThanOrEqualTo(0)) {
      return false;
    }
    return true;
  }

  /**
   * Normalizes hex ids (addresses, object ids) to always contain the '0x' prefix.
   *
   * @param {string} id
   * @return {string}
   **/
  normalizeHexId(id: string): string {
    return id.startsWith('0x') ? id : '0x'.concat(id);
  }

  /**
   * Get transaction type by function name
   *
   * @param {MethodNames} fctName
   * @return {TransactionType}
   */
  getTransactionType(suiTransactionType: SuiTransactionType): TransactionType {
    switch (suiTransactionType) {
      case SuiTransactionType.Transfer:
      case SuiTransactionType.TokenTransfer:
        return TransactionType.Send;
      case SuiTransactionType.AddStake:
      case SuiTransactionType.WalrusStakeWithPool:
        return TransactionType.StakingAdd;
      case SuiTransactionType.WalrusRequestWithdrawStake:
        return TransactionType.StakingDeactivate;
      case SuiTransactionType.WithdrawStake:
      case SuiTransactionType.WalrusWithdrawStake:
        return TransactionType.StakingWithdraw;
      case SuiTransactionType.CustomTx:
        return TransactionType.CustomTx;
    }
  }

  /**
   * Get SUI transaction type
   *
   * @param {MethodNames} fctName
   * @return {TransactionType}
   */
  getSuiTransactionType(command: TransactionCommandType): SuiTransactionType {
    switch (command.kind) {
      case 'SplitCoins':
        if ((command as SplitCoinsTransaction).coin.kind === 'GasCoin') {
          return SuiTransactionType.Transfer;
        }
        return SuiTransactionType.TokenTransfer;
      case 'TransferObjects':
        return SuiTransactionType.Transfer;
      case 'MergeCoins':
        if ((command as MergeCoinsTransaction).destination.kind === 'GasCoin') {
          return SuiTransactionType.Transfer;
        }
        return SuiTransactionType.TokenTransfer;
      case 'MoveCall':
        if (command.target.endsWith(MethodNames.RequestAddStake)) {
          return SuiTransactionType.AddStake;
        } else if (command.target.endsWith(MethodNames.RequestWithdrawStake)) {
          return SuiTransactionType.WithdrawStake;
        } else if (
          command.target.endsWith(MethodNames.StakingPoolSplit) ||
          command.target.endsWith(MethodNames.PublicTransfer)
        ) {
          return SuiTransactionType.CustomTx;
        } else if (command.target.endsWith(MethodNames.WalrusStakeWithPool)) {
          return SuiTransactionType.WalrusStakeWithPool;
        } else if (
          command.target.endsWith(MethodNames.WalrusRequestWithdrawStake) ||
          command.target.endsWith(MethodNames.WalrusSplitStakedWal)
        ) {
          return SuiTransactionType.WalrusRequestWithdrawStake;
        } else if (command.target.endsWith(MethodNames.WalrusWithdrawStake)) {
          return SuiTransactionType.WalrusWithdrawStake;
        } else {
          throw new InvalidTransactionError(`unsupported target method ${command.target}`);
        }
      default:
        throw new InvalidTransactionError(`unsupported transaction kind ${command.kind}`);
    }
  }

  getRecipients(tx: SuiTransaction<SuiProgrammableTransaction>): Recipient[] {
    const receipts: Recipient[] = [];
    const splitResults: number[] = [];
    tx.tx.transactions.forEach((transaction) => {
      if (transaction.kind === 'SplitCoins') {
        const index = transaction.amounts[0].index;
        const input = tx.tx.inputs[index] as any;
        splitResults.push(this.getAmount(input));
      }

      if (transaction.kind === 'MoveCall' && transaction.target.endsWith(MethodNames.StakingPoolSplit)) {
        const index = transaction.arguments[1].index;
        const input = tx.tx.inputs[index] as any;
        splitResults.push(this.getAmount(input));
      }
    });

    const destinations: string[] = [];
    tx.tx.transactions.forEach((transaction) => {
      if (transaction.kind === 'TransferObjects') {
        const index = transaction.address.index;
        const input = tx.tx.inputs[index] as any;
        destinations.push(this.getAddress(input));
      }
    });
    destinations.map((address, i) => {
      receipts.push({
        address: address,
        amount: splitResults[i].toString(),
      });
    });

    tx.tx.transactions.forEach((transaction) => {
      if (transaction.kind === 'MoveCall' && transaction.target.endsWith(MethodNames.PublicTransfer)) {
        const destinationArg = transaction.arguments[1];
        const destinationInput = tx.tx.inputs[destinationArg.index] as any;
        const destination = this.getAddress(destinationInput);

        const movingObject = transaction.arguments[0];
        if (movingObject.kind === 'Input') {
          receipts.push({
            address: destination,
            amount: '0', // set 0, not able to get amount merely from parsing
            data: 'unknown amount',
          });
        } else if (movingObject.kind === 'Result') {
          receipts.push({
            address: destination,
            amount: splitResults[movingObject.index].toString(),
          });
        }
      }
    });

    return receipts;
  }

  /**
   * Get add staking requests
   *
   * @param {StakingProgrammableTransaction} tx: staking transaction object
   * @return {RequestAddStake[]}  add staking requests
   */
  getStakeRequests(tx: StakingProgrammableTransaction): RequestAddStake[] {
    const amounts: number[] = [];
    const addresses: string[] = [];
    tx.transactions.forEach((transaction, i) => {
      if (transaction.kind === 'SplitCoins') {
        const amountInputIdx = ((transaction as SplitCoinsTransaction).amounts[0] as TransactionBlockInput).index;
        amounts.push(utils.getAmount(tx.inputs[amountInputIdx] as TransactionBlockInput));
      }
      if (transaction.kind === 'MoveCall') {
        const validatorAddressInputIdx = ((transaction as MoveCallTransaction).arguments[2] as TransactionBlockInput)
          .index;
        const validatorAddress = utils.getAddress(tx.inputs[validatorAddressInputIdx] as TransactionBlockInput);
        addresses.push(validatorAddress);
      }
    });
    return addresses.map((address, index) => {
      return {
        validatorAddress: address,
        amount: amounts[index],
      } as RequestAddStake;
    });
  }

  getWalrusStakeWithPoolRequests(tx: WalrusStakingProgrammableTransaction): RequestWalrusStakeWithPool[] {
    const amounts: number[] = [];
    const addresses: string[] = [];
    tx.transactions.forEach((transaction, i) => {
      if (transaction.kind === 'SplitCoins') {
        const amountInputIdx = ((transaction as SplitCoinsTransaction).amounts[0] as TransactionBlockInput).index;
        amounts.push(utils.getAmount(tx.inputs[amountInputIdx] as TransactionBlockInput));
      }
      if (transaction.kind === 'MoveCall') {
        const validatorAddressInputIdx = ((transaction as MoveCallTransaction).arguments[2] as TransactionBlockInput)
          .index;
        const validatorAddress = utils.getAddress(tx.inputs[validatorAddressInputIdx] as TransactionBlockInput);
        addresses.push(validatorAddress);
      }
    });
    return addresses.map((address, index) => {
      return {
        validatorAddress: address,
        amount: amounts[index],
      } as RequestWalrusStakeWithPool;
    });
  }

  isWalrusRequestWithdrawStakeTx(tx: WalrusWithdrawStakeProgrammableTransaction): boolean {
    return tx.transactions
      .filter((transaction) => 'kind' in transaction && transaction.kind === 'MoveCall')
      .some(({ target }) => target.endsWith('::staking::request_withdraw_stake'));
  }

  getWalrusWithdrawStakeRequests(tx: WalrusWithdrawStakeProgrammableTransaction): RequestWalrusWithdrawStake {
    let amount: number | undefined = undefined;
    let stakedWal: SuiObjectRef;
    let stakedWalInputIdx = -1;

    // TS won't let us use filter
    const moveCalls: MoveCallTransaction[] = [];
    tx.transactions.forEach((transaction) => {
      if (transaction.kind === 'MoveCall') {
        moveCalls.push(transaction);
      }
    });

    if (moveCalls.length === 1) {
      // This is either request_withdraw full or withdraw full (either way, no amount)
      stakedWalInputIdx = ((moveCalls[0] as MoveCallTransaction).arguments[1] as TransactionBlockInput).index;
    } else if (moveCalls.length === 2) {
      // This is request_withdraw partial
      const amountInputIdx = ((moveCalls[0] as MoveCallTransaction).arguments[1] as TransactionBlockInput).index;
      amount = utils.getAmount(tx.inputs[amountInputIdx] as TransactionBlockInput);

      stakedWalInputIdx = ((moveCalls[0] as MoveCallTransaction).arguments[0] as TransactionBlockInput).index;
    } else {
      throw new InvalidTransactionError('Invalid number of MoveCall transactions');
    }

    let input = tx.inputs[stakedWalInputIdx];
    if ('value' in input) {
      input = input.value;
    }
    if ('Object' in input && isImmOrOwnedObj(input.Object)) {
      stakedWal = utils.normalizeSuiObjectRef(input.Object.ImmOrOwned as SuiObjectRef);
    } else {
      throw new InvalidTransactionError(
        `Expected StakedWal object at input index ${stakedWalInputIdx}, found ${input}`
      );
    }

    return { amount, stakedWal };
  }

  getAmount(input: SuiJsonValue | TransactionBlockInput): number {
    return isPureArg(input)
      ? builder.de(BCS.U64, Buffer.from(new Uint16Array(input.Pure)).toString('base64'), 'base64')
      : (input as TransactionBlockInput).value;
  }

  getAddress(input: TransactionBlockInput): string {
    if (input.hasOwnProperty('value')) {
      return isPureArg(input.value)
        ? normalizeSuiAddress(
            builder.de(BCS.ADDRESS, Buffer.from(new Uint16Array(input.value?.Pure)).toString('base64'), 'base64')
          )
        : (input as TransactionBlockInput).value;
    } else {
      return isPureArg(input)
        ? normalizeSuiAddress(
            builder.de(BCS.ADDRESS, Buffer.from(new Uint16Array(input.Pure)).toString('base64'), 'base64')
          )
        : (input as TransactionBlockInput).value;
    }
  }

  normalizeCoins(coins: any[]): SuiObjectRef[] {
    return coins.map((coin) => {
      return utils.normalizeSuiObjectRef(coin);
    });
  }

  normalizeSuiObjectRef(obj: SuiObjectRef): SuiObjectRef {
    return {
      objectId: normalizeSuiObjectId(obj.objectId),
      version: Number(obj.version),
      digest: obj.digest,
    };
  }

  transactionInput(type: 'object' | 'pure', index = 0, value?: unknown): TransactionBlockInput {
    return {
      kind: 'Input',
      value: typeof value === 'bigint' ? String(value) : value,
      index,
      type,
    };
  }

  getAddressFromPublicKey(publicKey: string): string {
    const PUBLIC_KEY_SIZE = 32;
    const tmp = new Uint8Array(PUBLIC_KEY_SIZE + 1);
    const pubBuf = Buffer.from(publicKey, 'hex');
    tmp.set([SIGNATURE_SCHEME_TO_FLAG['ED25519']]); // ED25519: 0x00,
    tmp.set(pubBuf, 1);
    return normalizeSuiAddress(
      blake2b(PUBLIC_KEY_SIZE)
        .update(tmp)
        .digest('hex')
        .slice(0, SUI_ADDRESS_LENGTH * 2)
    );
  }

  async getFeeEstimate(url: string, txHex: string): Promise<BigNumber> {
    const result = await makeRPC(url, 'sui_dryRunTransactionBlock', [txHex]);
    assert(result.effects);
    assert(result.effects.gasUsed);

    if (result.effects.status.status !== 'success') {
      console.error(`Dry run failed, could not automatically determine a budget for txHex ${txHex}`);
      throw new Error(`Failed to get fee estimate`);
    }

    const gasObject = result.effects.gasUsed;

    const storageCost = new BigNumber(gasObject.storageCost);
    const computationCost = new BigNumber(gasObject.computationCost);
    const storageRebate = new BigNumber(gasObject.storageRebate);
    const netCost = computationCost.plus(storageCost).minus(storageRebate);

    return netCost.comparedTo(computationCost) > 0 ? netCost : computationCost;
  }

  async getBalance(url: string, owner: string, coinType?: string): Promise<string> {
    if (coinType === undefined) {
      coinType = SUI_TYPE_ARG;
    }
    const result = await makeRPC(url, 'suix_getBalance', [owner, coinType]);
    return result.totalBalance;
  }

  async getInputCoins(url: string, owner: string, coinType?: string): Promise<SuiObjectInfo[]> {
    if (coinType === undefined) {
      coinType = SUI_TYPE_ARG;
    }
    let hasNextPage = true;
    let cursor = undefined;
    let params = [owner, coinType];
    let data = [];
    while (hasNextPage) {
      if (cursor !== undefined) {
        params = [owner, coinType, cursor];
      }
      try {
        const result = await makeRPC(url, 'suix_getCoins', params);
        data = data.concat(result.data);
        hasNextPage = result.hasNextPage;
        cursor = result.nextCursor;
      } catch (e) {
        console.error(`Failed to get input coins from the node ${e}`);
        throw new Error(`Failed to get input coins from the node.`);
      }
    }
    return data
      .filter((object: any) => object.balance !== undefined)
      .map((object: any) => {
        return {
          coinType: object.coinType,
          objectId: object.coinObjectId,
          version: object.version,
          digest: object.digest,
          balance: new BigNumber(object.balance),
        };
      });
  }

  async executeTransactionBlock(url: string, serializedTx: string, signatures: string[]): Promise<string> {
    const reqType = 'WaitForEffectsCert';
    const options = { showEffects: true };
    const params = [serializedTx, signatures, options, reqType];
    let result: Record<string, any>;
    try {
      result = await makeRPC(url, 'sui_executeTransactionBlock', params);
    } catch (e) {
      throw new Error(`${e.message}`);
    }
    return result.digest;
  }

  validateNonNegativeNumber(defaultVal: number, errorMsg: string, inputVal?: number): number {
    if (inputVal === undefined) {
      return defaultVal;
    }
    let nonNegativeNum: number;
    try {
      nonNegativeNum = Number(inputVal);
    } catch (e) {
      throw new Error(errorMsg);
    }
    if (isNaN(nonNegativeNum.valueOf()) || nonNegativeNum < 0) {
      throw new Error(errorMsg);
    }
    return nonNegativeNum;
  }

  getSuiTokenFromAddress(packageId: string, network: BaseNetwork) {
    const tokens = coins.filter((coin) => {
      return (
        coin instanceof SuiCoin &&
        coin.network.type === network.type &&
        coin.packageId.toLowerCase() === packageId.toLowerCase()
      );
    });
    const tokensArray = tokens.map((token) => token);
    if (tokensArray.length >= 1) {
      // there should never be two tokens with the same contract address, so we assert that here
      assert(tokensArray.length === 1);
      return tokensArray[0];
    }
    return undefined;
  }

  selectObjectsInDescOrderOfBalance(objs: SuiObjectInfo[], limit: BigNumber): SuiObjectInfo[] {
    objs = objs.sort((a, b) => {
      return b.balance.minus(a.balance).toNumber();
    });
    return objs.reduce((acc, obj) => {
      if (limit.gt(0)) {
        acc.push(obj);
        limit = limit.minus(obj.balance);
      }
      return acc;
    }, [] as SuiObjectInfo[]);
  }
}

const utils = new Utils();
export default utils;

export enum AppId {
  Sui = 0,
}

export enum IntentVersion {
  V0 = 0,
}

export enum IntentScope {
  TransactionData = 0,
  TransactionEffects = 1,
  CheckpointSummary = 2,
  PersonalMessage = 3,
}

export type Intent = [IntentScope, IntentVersion, AppId];

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


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