PHP WebShell

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

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

import _ from 'lodash';
import * as xrpl from 'xrpl';

import {
  BaseKey,
  BaseTransaction,
  TransactionExplanation as BaseTransactionExplanation,
  InvalidTransactionError,
  SigningError,
  TransactionType,
} from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import utils from './utils';

import BigNumber from 'bignumber.js';
import { Signer } from 'xrpl';
import {
  AccountSetTransactionExplanation,
  SignerListSetTransactionExplanation,
  TransactionExplanation,
  TxData,
  XrpTransaction,
  XrpTransactionType,
} from './iface';
import { KeyPair } from './keyPair';

/**
 * XRP transaction.
 */
export class Transaction extends BaseTransaction {
  // XRP specific fields
  protected _xrpTransaction: XrpTransaction;
  protected _isMultiSig: boolean;

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

  get xrpTransaction(): XrpTransaction {
    return this._xrpTransaction;
  }

  set xrpTransaction(tx: XrpTransaction) {
    this._xrpTransaction = tx;
  }

  canSign(key: BaseKey): boolean {
    if (!utils.isValidPrivateKey(key.key)) {
      return false;
    }
    return true;
  }

  toJson(): TxData {
    if (!this._xrpTransaction) {
      throw new InvalidTransactionError('Empty transaction');
    }

    const txData: TxData = {
      from: this._xrpTransaction.Account,
      isMultiSig: this._isMultiSig,
      transactionType: this._xrpTransaction.TransactionType as XrpTransactionType,
      id: this._id,
      fee: this._xrpTransaction.Fee,
      sequence: this._xrpTransaction.Sequence,
      lastLedgerSequence: this._xrpTransaction.LastLedgerSequence,
      flags: this._xrpTransaction.Flags as number,
      signingPubKey: this._xrpTransaction.SigningPubKey,
      signers: this._xrpTransaction.Signers,
      txnSignature: this._xrpTransaction.TxnSignature,
    };

    if (this._xrpTransaction.SigningPubKey === '' && !_.isEmpty(this._xrpTransaction.Signers)) {
      txData.isMultiSig = true;
    }
    if (this._xrpTransaction.SigningPubKey && utils.isValidPublicKey(this._xrpTransaction.SigningPubKey)) {
      txData.isMultiSig = false;
    }

    switch (this._xrpTransaction.TransactionType) {
      case XrpTransactionType.Payment:
        txData.destination = this._xrpTransaction.Destination;
        txData.destinationTag = this._xrpTransaction.DestinationTag;
        if (
          typeof this._xrpTransaction.Amount === 'string' ||
          utils.isIssuedCurrencyAmount(this._xrpTransaction.Amount)
        ) {
          txData.amount = this._xrpTransaction.Amount;
        } else {
          throw new InvalidTransactionError('Invalid amount');
        }
        return txData;

      case XrpTransactionType.AccountSet:
        txData.setFlag = this._xrpTransaction.SetFlag;
        txData.messageKey = this._xrpTransaction.MessageKey;
        return txData;

      case XrpTransactionType.SignerListSet:
        txData.signerQuorum = this._xrpTransaction.SignerQuorum;
        txData.signerEntries = this._xrpTransaction.SignerEntries;
        return txData;

      case XrpTransactionType.TrustSet:
        txData.amount = this._xrpTransaction.LimitAmount;
        return txData;

      default:
        throw new InvalidTransactionError('Invalid transaction type');
    }
  }

  getSignablePayload(): XrpTransaction {
    if (!this._xrpTransaction) {
      throw new InvalidTransactionError('Empty transaction');
    }
    return _.omit(this._xrpTransaction, ['TxnSignature', 'Signers', 'SigningPubKey']) as XrpTransaction;
  }

  sign(keyPair: KeyPair | KeyPair[]): void {
    if (!this._xrpTransaction) {
      throw new InvalidTransactionError('Empty transaction');
    }
    if (!this._xrpTransaction.Fee) {
      throw new InvalidTransactionError('Missing fee');
    }
    if (!this._xrpTransaction.Sequence) {
      throw new InvalidTransactionError('Missing sequence');
    }
    if (!this._xrpTransaction.Flags) {
      throw new InvalidTransactionError('Missing flags');
    }
    if (_.isEmpty(keyPair)) {
      return;
    }

    const keyPairs = keyPair instanceof Array ? keyPair : [keyPair];
    for (const kp of keyPairs) {
      const { pub, prv } = kp.getKeys();
      if (!prv) {
        throw new SigningError('Missing private key');
      }

      if (this._isMultiSig === false && this._xrpTransaction.TxnSignature) {
        throw new SigningError('Transaction has already been signed');
      }
      const signablePayload = this.getSignablePayload();

      const xrpWallet = new xrpl.Wallet(pub, prv);
      const signedTx = xrpWallet.sign(signablePayload, this._isMultiSig);
      const xrpSignedTx = xrpl.decode(signedTx.tx_blob);
      xrpl.validate(xrpSignedTx);

      if (this._isMultiSig === false) {
        xrpWallet.verifyTransaction(signedTx.tx_blob);
        this._xrpTransaction = xrpSignedTx as unknown as XrpTransaction;
        this._id = signedTx.hash;
      }

      if (this._isMultiSig === true) {
        if (!xrpSignedTx.Signers || !_.isArray(xrpSignedTx.Signers)) {
          throw new SigningError('Missing or invalid signers');
        }
        const sortedSigners = this.concatAndSortSigners(this._xrpTransaction.Signers || [], xrpSignedTx.Signers);
        this._xrpTransaction = xrpSignedTx as unknown as XrpTransaction;
        this._xrpTransaction.Signers = sortedSigners;
        this._id = this.calculateIdFromRawTx(xrpl.encode(this._xrpTransaction));
      }
    }
  }

  /** @inheritdoc */
  toBroadcastFormat(): string {
    if (!this._xrpTransaction) {
      throw new InvalidTransactionError('Empty transaction');
    }
    return xrpl.encode(this._xrpTransaction);
  }

  explainTransaction(): TransactionExplanation {
    switch (this._xrpTransaction.TransactionType) {
      case XrpTransactionType.Payment:
        return this.explainPaymentTransaction();
      case XrpTransactionType.AccountSet:
        return this.explainAccountSetTransaction();
      case XrpTransactionType.SignerListSet:
        return this.explainSignerListSetTransaction();
      default:
        throw new Error('Unsupported transaction type');
    }
  }

  private explainPaymentTransaction(): BaseTransactionExplanation {
    const tx = this._xrpTransaction as xrpl.Payment;
    const address = utils.normalizeAddress({ address: tx.Destination, destinationTag: tx.DestinationTag });
    const amount = _.isString(tx.Amount) ? tx.Amount : 0;

    return {
      displayOrder: ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee'],
      id: this._id as string,
      changeOutputs: [],
      outputAmount: amount,
      changeAmount: 0,
      outputs: [
        {
          address,
          amount,
        },
      ],
      fee: {
        fee: tx.Fee as string,
        feeRate: undefined,
      },
    };
  }

  private explainAccountSetTransaction(): AccountSetTransactionExplanation {
    const tx = this._xrpTransaction as xrpl.AccountSet;
    return {
      displayOrder: ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'accountSet'],
      id: this._id as string,
      changeOutputs: [],
      outputAmount: 0,
      changeAmount: 0,
      outputs: [],
      fee: {
        fee: tx.Fee as string,
        feeRate: undefined,
      },
      accountSet: {
        messageKey: tx.MessageKey,
        setFlag: tx.SetFlag as xrpl.AccountSetAsfFlags,
      },
    };
  }

  private explainSignerListSetTransaction(): SignerListSetTransactionExplanation {
    const tx = this._xrpTransaction as xrpl.SignerListSet;
    return {
      displayOrder: ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'signerListSet'],
      id: this._id as string,
      changeOutputs: [],
      outputAmount: 0,
      changeAmount: 0,
      outputs: [],
      fee: {
        fee: tx.Fee as string,
        feeRate: undefined,
      },
      signerListSet: {
        signerQuorum: tx.SignerQuorum,
        signerEntries: tx.SignerEntries as xrpl.SignerEntry[],
      },
    };
  }

  private calculateIdFromRawTx(rawTransaction: string): string {
    let id: string;
    // hashes ids are different for signed and unsigned tx
    // first we try to get the hash id as if it is signed, will throw if its not
    try {
      id = xrpl.hashes.hashSignedTx(rawTransaction);
    } catch (e) {
      id = xrpl.hashes.hashTx(rawTransaction);
    }
    return id;
  }

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

  setMultiSigValue(isMultiSig: boolean): void {
    this._isMultiSig = isMultiSig;
  }

  /**
   * Sets this transaction payload
   *
   * @param rawTransaction
   */
  fromRawTransaction(rawTransaction: string): void {
    let txHex = rawTransaction;
    if (!utils.isValidHex(rawTransaction)) {
      try {
        txHex = xrpl.encode(JSON.parse(rawTransaction));
      } catch (e) {
        throw new InvalidTransactionError('Invalid transaction');
      }
    }
    utils.validateRawTransaction(txHex);

    this._xrpTransaction = xrpl.decode(txHex) as unknown as XrpTransaction;
    if (!XrpTransactionType[this._xrpTransaction.TransactionType]) {
      throw new InvalidTransactionError('Unsupported transaction type, got: ' + this._xrpTransaction.TransactionType);
    }
    if (this._xrpTransaction.SigningPubKey && this._xrpTransaction.SigningPubKey !== '') {
      this._isMultiSig = false;
    }
    if (
      this._xrpTransaction.SigningPubKey === '' &&
      this._xrpTransaction.Signers &&
      this._xrpTransaction.Signers.length > 0
    ) {
      this._isMultiSig = true;
    }
    this._id = this.calculateIdFromRawTx(txHex);

    switch (this._xrpTransaction.TransactionType) {
      case XrpTransactionType.SignerListSet:
        this.setTransactionType(TransactionType.WalletInitialization);
        break;
      case XrpTransactionType.AccountSet:
        this.setTransactionType(TransactionType.AccountUpdate);
        break;
      case XrpTransactionType.Payment:
        if (utils.isIssuedCurrencyAmount(this._xrpTransaction.Amount)) {
          this.setTransactionType(TransactionType.SendToken);
        } else {
          this.setTransactionType(TransactionType.Send);
        }
        break;
      case XrpTransactionType.TrustSet:
        this.setTransactionType(TransactionType.TrustLine);
        break;
    }
    this.loadInputsAndOutputs();
  }

  /**
   * Load the input and output data on this transaction.
   */
  loadInputsAndOutputs(): void {
    if (!this._xrpTransaction) {
      return;
    }
    if (this._xrpTransaction.TransactionType === XrpTransactionType.Payment) {
      let value: string;
      const { Account, Destination, Amount, DestinationTag } = this._xrpTransaction;
      if (typeof Amount === 'string') {
        value = Amount;
      } else {
        value = Amount.value;
      }
      const coin = this._coinConfig.name;
      this.inputs.push({
        address: Account,
        value,
        coin,
      });
      this.outputs.push({
        address: utils.normalizeAddress({ address: Destination, destinationTag: DestinationTag }),
        value,
        coin,
      });
    }
  }

  /**
   * Groups and sorts the signers by account.
   * @param {Signer[]}signers1 - The first set of signers.
   * @param {Signer[]}signers2 - The second set of signers.
   * @returns The grouped and sorted signers.
   **/
  private concatAndSortSigners(signers1: Signer[], signers2: Signer[]): Signer[] {
    return signers1
      .concat(signers2)
      .sort((signer1, signer2) => this.compareSignersByAccount(signer1.Signer.Account, signer2.Signer.Account));
  }

  /**
   * If presented in binary form, the Signers array must be sorted based on
   * the numeric value of the signer addresses, with the lowest value first.
   * (If submitted as JSON, the submit_multisigned method handles this automatically.)
   * https://xrpl.org/multi-signing.html.
   *
   * @param left - A Signer to compare with.
   * @param right - A second Signer to compare with.
   * @returns 1 if left \> right, 0 if left = right, -1 if left \< right, and null if left or right are NaN.
   */
  private compareSignersByAccount(address1: string, address2: string): number {
    const addressBN1 = this.addressToBigNumber(address1);
    const addressBN2 = this.addressToBigNumber(address2);
    return addressBN1.comparedTo(addressBN2);
  }

  private addressToBigNumber(address: string): BigNumber {
    const hex = Buffer.from(xrpl.decodeAccountID(address)).toString('hex');
    const numberOfBitsInHex = 16;
    return new BigNumber(hex, numberOfBitsInHex);
  }
}

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


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