transaction.js

import xdr from './xdr';
import { hash } from './hashing';

import { StrKey } from './strkey';
import { Operation } from './operation';
import { Memo } from './memo';
import { TransactionBase } from './transaction_base';
import {
  extractBaseAddress,
  encodeMuxedAccountToAddress
} from './util/decode_encode_muxed_account';

/**
 * Use {@link TransactionBuilder} to build a transaction object. If you have an
 * object or base64-encoded string of the transaction envelope XDR, use {@link
 * TransactionBuilder.fromXDR}.
 *
 * Once a Transaction has been created, its attributes and operations should not
 * be changed. You should only add signatures (using {@link Transaction#sign})
 * to a Transaction object before submitting to the network or forwarding on to
 * additional signers.
 *
 * @constructor
 *
 * @param {string|xdr.TransactionEnvelope} envelope - transaction envelope
 *     object or base64 encoded string
 * @param {string}  [networkPassphrase] - passphrase of the target stellar
 *     network (e.g. "Public Global Stellar Network ; September 2015")
 *
 * @extends TransactionBase
 */
export class Transaction extends TransactionBase {
  constructor(envelope, networkPassphrase) {
    if (typeof envelope === 'string') {
      const buffer = Buffer.from(envelope, 'base64');
      envelope = xdr.TransactionEnvelope.fromXDR(buffer);
    }

    const envelopeType = envelope.switch();
    if (
      !(
        envelopeType === xdr.EnvelopeType.envelopeTypeTxV0() ||
        envelopeType === xdr.EnvelopeType.envelopeTypeTx()
      )
    ) {
      throw new Error(
        `Invalid TransactionEnvelope: expected an envelopeTypeTxV0 or envelopeTypeTx but received an ${envelopeType.name}.`
      );
    }

    const txEnvelope = envelope.value();
    const tx = txEnvelope.tx();
    const fee = tx.fee().toString();
    const signatures = (txEnvelope.signatures() || []).slice();

    super(tx, signatures, fee, networkPassphrase);

    this._envelopeType = envelopeType;
    this._memo = tx.memo();
    this._sequence = tx.seqNum().toString();

    switch (this._envelopeType) {
      case xdr.EnvelopeType.envelopeTypeTxV0():
        this._source = StrKey.encodeEd25519PublicKey(
          this.tx.sourceAccountEd25519()
        );
        break;
      default:
        this._source = encodeMuxedAccountToAddress(this.tx.sourceAccount());
        break;
    }

    let cond = null;
    let timeBounds = null;
    switch (this._envelopeType) {
      case xdr.EnvelopeType.envelopeTypeTxV0():
        timeBounds = tx.timeBounds();
        break;

      case xdr.EnvelopeType.envelopeTypeTx():
        switch (tx.cond().switch()) {
          case xdr.PreconditionType.precondTime():
            timeBounds = tx.cond().timeBounds();
            break;

          case xdr.PreconditionType.precondV2():
            cond = tx.cond().v2();
            timeBounds = cond.timeBounds();
            break;

          default:
            break;
        }
        break;

      default:
        break;
    }

    if (timeBounds) {
      this._timeBounds = {
        minTime: timeBounds.minTime().toString(),
        maxTime: timeBounds.maxTime().toString()
      };
    }

    if (cond) {
      const ledgerBounds = cond.ledgerBounds();
      if (ledgerBounds) {
        this._ledgerBounds = {
          minLedger: ledgerBounds.minLedger(),
          maxLedger: ledgerBounds.maxLedger()
        };
      }

      const minSeq = cond.minSeqNum();
      if (minSeq) {
        this._minAccountSequence = minSeq.toString();
      }

      this._minAccountSequenceAge = cond.minSeqAge();
      this._minAccountSequenceLedgerGap = cond.minSeqLedgerGap();
      this._extraSigners = cond.extraSigners();
    }

    const operations = tx.operations() || [];
    this._operations = operations.map((op) => Operation.fromXDRObject(op));
  }

  /**
   * @type {object}
   * @property {string} 64 bit unix timestamp
   * @property {string} 64 bit unix timestamp
   * @readonly
   */
  get timeBounds() {
    return this._timeBounds;
  }
  set timeBounds(value) {
    throw new Error('Transaction is immutable');
  }

  /**
   * @type {object}
   * @property {number} minLedger - smallest ledger bound (uint32)
   * @property {number} maxLedger - largest ledger bound (or 0 for inf)
   * @readonly
   */
  get ledgerBounds() {
    return this._ledgerBounds;
  }
  set ledgerBounds(value) {
    throw new Error('Transaction is immutable');
  }

  /**
   * 64 bit account sequence
   * @readonly
   * @type {string}
   */
  get minAccountSequence() {
    return this._minAccountSequence;
  }
  set minAccountSequence(value) {
    throw new Error('Transaction is immutable');
  }

  /**
   * 64 bit number of seconds
   * @type {number}
   * @readonly
   */
  get minAccountSequenceAge() {
    return this._minAccountSequenceAge;
  }
  set minAccountSequenceAge(value) {
    throw new Error('Transaction is immutable');
  }

  /**
   * 32 bit number of ledgers
   * @type {number}
   * @readonly
   */
  get minAccountSequenceLedgerGap() {
    return this._minAccountSequenceLedgerGap;
  }
  set minAccountSequenceLedgerGap(value) {
    throw new Error('Transaction is immutable');
  }

  /**
   * array of extra signers ({@link StrKey}s)
   * @type {string[]}
   * @readonly
   */
  get extraSigners() {
    return this._extraSigners;
  }
  set extraSigners(value) {
    throw new Error('Transaction is immutable');
  }

  /**
   * @type {string}
   * @readonly
   */
  get sequence() {
    return this._sequence;
  }
  set sequence(value) {
    throw new Error('Transaction is immutable');
  }

  /**
   * @type {string}
   * @readonly
   */
  get source() {
    return this._source;
  }
  set source(value) {
    throw new Error('Transaction is immutable');
  }

  /**
   * @type {Array.<xdr.Operation>}
   * @readonly
   */
  get operations() {
    return this._operations;
  }
  set operations(value) {
    throw new Error('Transaction is immutable');
  }

  /**
   * @type {string}
   * @readonly
   */
  get memo() {
    return Memo.fromXDRObject(this._memo);
  }
  set memo(value) {
    throw new Error('Transaction is immutable');
  }

  /**
   * Returns the "signature base" of this transaction, which is the value
   * that, when hashed, should be signed to create a signature that
   * validators on the Stellar Network will accept.
   *
   * It is composed of a 4 prefix bytes followed by the xdr-encoded form
   * of this transaction.
   * @returns {Buffer}
   */
  signatureBase() {
    let tx = this.tx;

    // Backwards Compatibility: Use ENVELOPE_TYPE_TX to sign ENVELOPE_TYPE_TX_V0
    // we need a Transaction to generate the signature base
    if (this._envelopeType === xdr.EnvelopeType.envelopeTypeTxV0()) {
      tx = xdr.Transaction.fromXDR(
        Buffer.concat([
          // TransactionV0 is a transaction with the AccountID discriminant
          // stripped off, we need to put it back to build a valid transaction
          // which we can use to build a TransactionSignaturePayloadTaggedTransaction
          xdr.PublicKeyType.publicKeyTypeEd25519().toXDR(),
          tx.toXDR()
        ])
      );
    }

    const taggedTransaction =
      new xdr.TransactionSignaturePayloadTaggedTransaction.envelopeTypeTx(tx);

    const txSignature = new xdr.TransactionSignaturePayload({
      networkId: xdr.Hash.fromXDR(hash(this.networkPassphrase)),
      taggedTransaction
    });

    return txSignature.toXDR();
  }

  /**
   * To envelope returns a xdr.TransactionEnvelope which can be submitted to the network.
   * @returns {xdr.TransactionEnvelope}
   */
  toEnvelope() {
    const rawTx = this.tx.toXDR();
    const signatures = this.signatures.slice(); // make a copy of the signatures

    let envelope;
    switch (this._envelopeType) {
      case xdr.EnvelopeType.envelopeTypeTxV0():
        envelope = new xdr.TransactionEnvelope.envelopeTypeTxV0(
          new xdr.TransactionV0Envelope({
            tx: xdr.TransactionV0.fromXDR(rawTx), // make a copy of tx
            signatures
          })
        );
        break;
      case xdr.EnvelopeType.envelopeTypeTx():
        envelope = new xdr.TransactionEnvelope.envelopeTypeTx(
          new xdr.TransactionV1Envelope({
            tx: xdr.Transaction.fromXDR(rawTx), // make a copy of tx
            signatures
          })
        );
        break;
      default:
        throw new Error(
          `Invalid TransactionEnvelope: expected an envelopeTypeTxV0 or envelopeTypeTx but received an ${this._envelopeType.name}.`
        );
    }

    return envelope;
  }

  /**
   * Calculate the claimable balance ID for an operation within the transaction.
   *
   * @param   {integer}  opIndex   the index of the CreateClaimableBalance op
   * @returns {string}   a hex string representing the claimable balance ID
   *
   * @throws {RangeError}   for invalid `opIndex` value
   * @throws {TypeError}    if op at `opIndex` is not `CreateClaimableBalance`
   * @throws for general XDR un/marshalling failures
   *
   * @see https://github.com/stellar/go/blob/d712346e61e288d450b0c08038c158f8848cc3e4/txnbuild/transaction.go#L392-L435
   *
   */
  getClaimableBalanceId(opIndex) {
    // Validate and then extract the operation from the transaction.
    if (
      !Number.isInteger(opIndex) ||
      opIndex < 0 ||
      opIndex >= this.operations.length
    ) {
      throw new RangeError('invalid operation index');
    }

    let op = this.operations[opIndex];
    try {
      op = Operation.createClaimableBalance(op);
    } catch (err) {
      throw new TypeError(
        `expected createClaimableBalance, got ${op.type}: ${err}`
      );
    }

    // Always use the transaction's *unmuxed* source.
    const account = StrKey.decodeEd25519PublicKey(
      extractBaseAddress(this.source)
    );
    const operationId = xdr.HashIdPreimage.envelopeTypeOpId(
      new xdr.HashIdPreimageOperationId({
        sourceAccount: xdr.AccountId.publicKeyTypeEd25519(account),
        seqNum: xdr.SequenceNumber.fromString(this.sequence),
        opNum: opIndex
      })
    );

    const opIdHash = hash(operationId.toXDR('raw'));
    const balanceId = xdr.ClaimableBalanceId.claimableBalanceIdTypeV0(opIdHash);
    return balanceId.toXDR('hex');
  }
}