PHP WebShell

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

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

import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { BaseKey, BaseTransaction, Entry, SigningError, toHex, toUint8Array, TransactionType } from '@bitgo/sdk-core';
import { hash } from '@stablelib/sha384';
import BigNumber from 'bignumber.js';
import { Writer } from 'protobufjs';
import * as nacl from 'tweetnacl';
import * as Long from 'long';
import { proto } from '@hashgraph/proto';
import { TxData, Recipient } from './iface';
import { stringifyAccountId, stringifyTxTime, stringifyTokenId, getHederaTokenNameFromId } from './utils';
import { KeyPair } from './';
import { HederaTransactionTypes } from './constants';

export class Transaction extends BaseTransaction {
  private _hederaTx: proto.Transaction;
  private _txBody: proto.TransactionBody;
  protected _type: TransactionType;

  constructor(_coinConfig: Readonly<CoinConfig>) {
    super(_coinConfig);
  }

  /** @inheritdoc */
  canSign(key: BaseKey): boolean {
    return true;
  }

  async sign(keyPair: KeyPair): Promise<void> {
    const keys = keyPair.getKeys(true);
    if (!keys.prv) {
      throw new SigningError('Missing private key');
    }
    const secretKey = toUint8Array(keys.prv + keys.pub);
    const signature = nacl.sign.detached(this._hederaTx.bodyBytes, secretKey);
    this.addSignature(toHex(signature), keyPair);
  }

  /**
   * Add a signature to this transaction
   *
   * @param {string} signature - The signature to add, in string hex format
   * @param {KeyPair} key - The key of the key that created the signature
   */
  addSignature(signature: string, key: KeyPair): void {
    const sigPair = new proto.SignaturePair();
    sigPair.pubKeyPrefix = toUint8Array(key.getKeys(true).pub);
    sigPair.ed25519 = toUint8Array(signature);

    const sigMap = this._hederaTx.sigMap || new proto.SignatureMap();
    sigMap.sigPair!.push(sigPair);
    this._hederaTx.sigMap = sigMap;
    this._signatures.push(signature);
  }

  /** @inheritdoc */
  toBroadcastFormat(): string {
    const encoder = proto.Transaction;
    return toHex(this.encode(this._hederaTx, encoder));
  }

  /**
   * Sets this transaction payload
   *
   * @param rawTransaction
   */
  fromRawTransaction(rawTransaction: Uint8Array | string): void {
    const buffer = typeof rawTransaction === 'string' ? toUint8Array(rawTransaction) : rawTransaction;
    this.bodyBytes(buffer);
    switch (this.txBody.data) {
      case HederaTransactionTypes.Transfer:
        this.setTransactionType(TransactionType.Send);
        break;
      case HederaTransactionTypes.CreateAccount:
        this.setTransactionType(TransactionType.WalletInitialization);
        break;
      case HederaTransactionTypes.TokenAssociateToAccount:
        this.setTransactionType(TransactionType.AssociatedTokenAccountInitialization);
        break;
    }
  }

  /** @inheritdoc */
  toJson(): TxData {
    const [acc, time] = this.getTxIdParts();
    const result: TxData = {
      id: acc + '@' + time,
      hash: this.getTxHash(), // TODO: Update once hedera-sdk release this functionality BGA-284
      data: toHex(this._hederaTx.bodyBytes),
      fee: new BigNumber(this._txBody.transactionFee!.toString()).toNumber(),
      from: acc,
      startTime: time,
      validDuration: this._txBody.transactionValidDuration!.seconds!.toString(),
      node: stringifyAccountId(this._txBody.nodeAccountID!),
      memo: this._txBody.memo,
    };

    switch (this._txBody.data) {
      case HederaTransactionTypes.Transfer:
        result.instructionsData = {
          type: HederaTransactionTypes.Transfer,
          params: this.getTransferData(),
        };
        result.to = result.instructionsData.params.recipients[0].address;
        result.amount = result.instructionsData.params.recipients[0].amount;
        break;
      case HederaTransactionTypes.TokenAssociateToAccount:
        result.instructionsData = {
          type: HederaTransactionTypes.TokenAssociateToAccount,
          params: this.getAccountAssociateData(),
        };
        break;
    }

    return result;
  }

  /**
   * Get the recipient account and the amount
   * transferred on this transaction
   *
   * @returns { tokenName, Recipient[]} is object consisting of tokenName if it's a token transfer and recipients consisting
   *  the recipient address, the transfer amount, and the token name for token transfer
   */
  private getTransferData(): { tokenName?: string; recipients: Recipient[] } {
    const [acc] = this.getTxIdParts();
    const transferData: Recipient[] = [];
    const tokenTransfers: proto.ITokenTransferList[] = this._txBody.cryptoTransfer?.tokenTransfers || [];
    const transfers: proto.IAccountAmount[] =
      tokenTransfers[0]?.transfers || this._txBody.cryptoTransfer?.transfers?.accountAmounts || [];
    const tokenName = tokenTransfers.length
      ? getHederaTokenNameFromId(stringifyTokenId(tokenTransfers[0].token!))?.name
      : undefined;

    transfers.forEach((transfer) => {
      const amount = Long.fromValue(transfer.amount!);
      if (amount.isPositive() && stringifyAccountId(transfer.accountID!) !== acc) {
        transferData.push({
          address: stringifyAccountId(transfer.accountID!),
          amount: amount.toString(),
          ...(tokenTransfers.length && {
            tokenName: tokenName,
          }),
        });
      }
    });

    return {
      ...(tokenTransfers.length && {
        tokenName: tokenName,
      }),
      recipients: transferData,
    };
  }

  /**
   * Get the recipient account and the amount
   * transferred on this transaction
   *
   * @returns { accountId: string; tokenNames[]} is an object consisting of accountId for the token owner
   *  and list of tokenNames that will be enabled
   */
  private getAccountAssociateData(): { accountId: string; tokenNames: string[] } {
    const tokens: proto.ITokenID[] = this._txBody.tokenAssociate!.tokens || [];
    return {
      accountId: stringifyAccountId(this._txBody.tokenAssociate!.account!),
      tokenNames: tokens.map((token: proto.ITokenID) => getHederaTokenNameFromId(stringifyTokenId(token))!.name),
    };
  }

  // region getters & setters
  get txBody(): proto.TransactionBody {
    return this._txBody;
  }

  get hederaTx(): proto.Transaction {
    return this._hederaTx;
  }

  /**
   * Sets this transaction body components
   *
   * @param {proto.Transaction} tx - Body Transaction
   */
  body(tx: proto.Transaction): void {
    this._txBody = proto.TransactionBody.decode(tx.bodyBytes);
    this._hederaTx = tx;
    this.loadInputsAndOutputs();
  }

  /**
   * Set the transaction type
   *
   * @param {TransactionType} transactionType - The transaction type to be set
   */
  setTransactionType(transactionType: TransactionType): void {
    this._type = transactionType;
  }

  /**
   * Decode previous signatures from the inner hedera transaction
   * and save them into the base transaction signature list.
   */
  loadPreviousSignatures(): void {
    if (this._hederaTx.sigMap && this._hederaTx.sigMap.sigPair) {
      const sigPairs = this._hederaTx.sigMap.sigPair;
      sigPairs.forEach((sigPair) => {
        const signature = sigPair.ed25519;
        if (signature) {
          this._signatures.push(toHex(signature));
        }
      });
    }
  }

  /**
   * Load the input and output data on this transaction using the transaction json
   * if there are outputs. For transactions without outputs (e.g. wallet initializations),
   * this function will not do anything
   */
  loadInputsAndOutputs(): void {
    const txJson = this.toJson();
    const instruction = txJson.instructionsData;
    const outputs: Entry[] = [];
    const inputs: Entry[] = [];

    switch (instruction?.type) {
      case HederaTransactionTypes.Transfer:
        let totalAmount = new BigNumber(0);
        instruction.params.recipients.forEach((recipient) => {
          totalAmount = totalAmount.plus(recipient.amount);
          outputs.push({
            address: recipient.address,
            value: recipient.amount,
            coin: recipient.tokenName || this._coinConfig.name,
          });
        });
        inputs.push({
          address: txJson.from,
          value: totalAmount.toString(),
          coin: instruction.params.tokenName || this._coinConfig.name,
        });
        break;

      case HederaTransactionTypes.TokenAssociateToAccount:
        instruction.params.tokenNames.forEach((tokenName) => {
          const tokenEntry: Entry = {
            address: instruction.params.accountId,
            value: '0',
            coin: tokenName,
          };
          inputs.push(tokenEntry);
          outputs.push(tokenEntry);
        });
        break;
    }
    this._inputs = inputs;
    this._outputs = outputs;
  }

  /**
   * Sets this transaction body components
   *
   * @param {Uint8Array} bytes - Encoded body transaction
   */
  bodyBytes(bytes: Uint8Array): void {
    this.body(proto.Transaction.decode(bytes));
  }
  // endregion

  // region helpers
  /**
   * Returns this hedera transaction id components in a readable format
   *
   * @returns {[string, string]} - Transaction id parts [<account id>, <startTime in seconds>]
   */
  getTxIdParts(): [string, string] {
    if (
      this._txBody &&
      this._txBody.transactionID &&
      this._txBody.transactionID.accountID &&
      this._txBody.transactionID.transactionValidStart
    ) {
      return [
        stringifyAccountId(this._txBody.transactionID.accountID),
        stringifyTxTime(this._txBody.transactionID.transactionValidStart),
      ];
    }
    throw new Error('Missing transaction id information');
  }

  /**
   * Returns this transaction hash
   *
   * @returns {string} - The transaction hash
   */
  getTxHash(): string {
    if (!this._txBody.nodeAccountID) {
      throw new Error('Missing transaction node id');
    }
    const _signedTx = new proto.SignedTransaction();
    _signedTx.sigMap = this._hederaTx.sigMap;
    _signedTx.bodyBytes = this._hederaTx.bodyBytes;

    const encoder = proto.SignedTransaction;
    return this.sha(this.encode(_signedTx, encoder));
  }

  /**
   * Encode an object using the given encoder class
   *
   * @param {proto} obj - The object to be encoded, must be in proto namespace
   * @param encoder - Object encoder
   * @returns {Uint8Array} - Encoded object byte array
   */
  private encode<CtorFn extends { new (): T }, T extends { constructor: CtorFn }>(
    obj: T,
    encoder: { encode(arg: T): Writer }
  ): Uint8Array {
    return encoder.encode(obj).finish();
  }

  /**
   * Returns a sha-384 hash
   *
   * @param {Uint8Array} bytes - Bytes to be hashed
   * @returns {string} - The resulting hash string
   */
  sha(bytes: Uint8Array): string {
    return toHex(hash(bytes));
  }
}

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


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