Source: primitives/tx.js

/*!
 * tx.js - transaction object for bcoin
 * Copyright (c) 2014-2015, Fedor Indutny (MIT License)
 * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

const assert = require('bsert');
const bio = require('bufio');
const hash256 = require('bcrypto/lib/hash256');
const secp256k1 = require('bcrypto/lib/secp256k1');
const {BufferSet} = require('buffer-map');
const util = require('../utils/util');
const Amount = require('../btc/amount');
const Network = require('../protocol/network');
const Script = require('../script/script');
const Input = require('./input');
const Output = require('./output');
const Outpoint = require('./outpoint');
const InvItem = require('./invitem');
const consensus = require('../protocol/consensus');
const policy = require('../protocol/policy');
const ScriptError = require('../script/scripterror');
const {encoding} = bio;
const {hashType} = Script;
const {inspectSymbol} = require('../utils');

/**
 * TX
 * A static transaction object.
 * @alias module:primitives.TX
 * @property {Number} version
 * @property {Input[]} inputs
 * @property {Output[]} outputs
 * @property {Number} locktime
 */

class TX {
  /**
   * Create a transaction.
   * @constructor
   * @param {Object?} options
   */

  constructor(options) {
    this.version = 1;
    this.inputs = [];
    this.outputs = [];
    this.locktime = 0;

    this.mutable = false;

    this._hash = null;
    this._hhash = null;
    this._whash = null;

    this._raw = null;
    this._offset = -1;
    this._block = false;
    this._size = -1;
    this._witness = -1;
    this._sigops = -1;

    this._hashPrevouts = null;
    this._hashSequence = null;
    this._hashOutputs = null;

    if (options)
      this.fromOptions(options);
  }

  /**
   * Inject properties from options object.
   * @private
   * @param {Object} options
   */

  fromOptions(options) {
    assert(options, 'TX data is required.');

    if (options.version != null) {
      assert((options.version >>> 0) === options.version,
        'Version must be a uint32.');
      this.version = options.version;
    }

    if (options.inputs) {
      assert(Array.isArray(options.inputs), 'Inputs must be an array.');
      for (const input of options.inputs)
        this.inputs.push(new Input(input));
    }

    if (options.outputs) {
      assert(Array.isArray(options.outputs), 'Outputs must be an array.');
      for (const output of options.outputs)
        this.outputs.push(new Output(output));
    }

    if (options.locktime != null) {
      assert((options.locktime >>> 0) === options.locktime,
        'Locktime must be a uint32.');
      this.locktime = options.locktime;
    }

    return this;
  }

  /**
   * Instantiate TX from options object.
   * @param {Object} options
   * @returns {TX}
   */

  static fromOptions(options) {
    return new this().fromOptions(options);
  }

  /**
   * Clone the transaction.
   * @returns {TX}
   */

  clone() {
    return new this.constructor().inject(this);
  }

  /**
   * Inject properties from tx.
   * Used for cloning.
   * @private
   * @param {TX} tx
   * @returns {TX}
   */

  inject(tx) {
    this.version = tx.version;

    for (const input of tx.inputs)
      this.inputs.push(input.clone());

    for (const output of tx.outputs)
      this.outputs.push(output.clone());

    this.locktime = tx.locktime;

    return this;
  }

  /**
   * Clear any cached values.
   */

  refresh() {
    this._hash = null;
    this._hhash = null;
    this._whash = null;

    this._raw = null;
    this._size = -1;
    this._offset = -1;
    this._block = false;
    this._witness = -1;
    this._sigops = -1;

    this._hashPrevouts = null;
    this._hashSequence = null;
    this._hashOutputs = null;
  }

  /**
   * Hash the transaction with the non-witness serialization.
   * @param {String?} enc - Can be `'hex'` or `null`.
   * @returns {Hash|Buffer} hash
   */

  hash(enc) {
    let h = this._hash;

    if (!h) {
      h = hash256.digest(this.toNormal());
      if (!this.mutable)
        this._hash = h;
    }

    if (enc === 'hex') {
      let hex = this._hhash;
      if (!hex) {
        hex = h.toString('hex');
        if (!this.mutable)
          this._hhash = hex;
      }
      h = hex;
    }

    return h;
  }

  /**
   * Hash the transaction with the witness
   * serialization, return the wtxid (normal
   * hash if no witness is present, all zeroes
   * if coinbase).
   * @param {String?} enc - Can be `'hex'` or `null`.
   * @returns {Hash|Buffer} hash
   */

  witnessHash(enc) {
    if (!this.hasWitness())
      return this.hash(enc);

    let hash = this._whash;

    if (!hash) {
      hash = hash256.digest(this.toRaw());
      if (!this.mutable)
        this._whash = hash;
    }

    return enc === 'hex' ? hash.toString('hex') : hash;
  }

  /**
   * Serialize the transaction. Note
   * that this is cached. This will use
   * the witness serialization if a
   * witness is present.
   * @returns {Buffer} Serialized transaction.
   */

  toRaw() {
    return this.frame().data;
  }

  /**
   * Serialize the transaction without the
   * witness vector, regardless of whether it
   * is a witness transaction or not.
   * @returns {Buffer} Serialized transaction.
   */

  toNormal() {
    if (this.hasWitness())
      return this.frameNormal().data;
    return this.toRaw();
  }

  /**
   * Write the transaction to a buffer writer.
   * @param {BufferWriter} bw
   * @param {Boolean} block
   */

  toWriter(bw, block) {
    if (this.mutable) {
      if (this.hasWitness())
        return this.writeWitness(bw);
      return this.writeNormal(bw);
    }

    if (block) {
      this._offset = bw.offset;
      this._block = true;
    }

    bw.writeBytes(this.toRaw());

    return bw;
  }

  /**
   * Write the transaction to a buffer writer.
   * Uses non-witness serialization.
   * @param {BufferWriter} bw
   */

  toNormalWriter(bw) {
    if (this.hasWitness()) {
      this.writeNormal(bw);
      return bw;
    }
    return this.toWriter(bw);
  }

  /**
   * Serialize the transaction. Note
   * that this is cached. This will use
   * the witness serialization if a
   * witness is present.
   * @private
   * @returns {RawTX}
   */

  frame() {
    if (this.mutable) {
      assert(!this._raw);
      if (this.hasWitness())
        return this.frameWitness();
      return this.frameNormal();
    }

    if (this._raw) {
      assert(this._size >= 0);
      assert(this._witness >= 0);
      const raw = new RawTX(this._size, this._witness);
      raw.data = this._raw;
      return raw;
    }

    let raw;
    if (this.hasWitness())
      raw = this.frameWitness();
    else
      raw = this.frameNormal();

    this._raw = raw.data;
    this._size = raw.size;
    this._witness = raw.witness;

    return raw;
  }

  /**
   * Return the offset and size of the transaction. Useful
   * when the transaction is deserialized within a block.
   * @returns {Object} Contains `size` and `offset`.
   */

  getPosition() {
    assert(this._block && this._offset > 80, 'Position not available.');

    return {
      offset: this._offset,
      size: this._size
    };
  }

  /**
   * Calculate total size and size of the witness bytes.
   * @returns {Object} Contains `size` and `witness`.
   */

  getSizes() {
    if (this.mutable) {
      if (this.hasWitness())
        return this.getWitnessSizes();
      return this.getNormalSizes();
    }
    return this.frame();
  }

  /**
   * Calculate the virtual size of the transaction.
   * Note that this is cached.
   * @returns {Number} vsize
   */

  getVirtualSize() {
    const scale = consensus.WITNESS_SCALE_FACTOR;
    return (this.getWeight() + scale - 1) / scale | 0;
  }

  /**
   * Calculate the virtual size of the transaction
   * (weighted against bytes per sigop cost).
   * @param {Number} sigops - Sigops cost.
   * @returns {Number} vsize
   */

  getSigopsSize(sigops) {
    const scale = consensus.WITNESS_SCALE_FACTOR;
    const bytes = policy.BYTES_PER_SIGOP;
    const weight = Math.max(this.getWeight(), sigops * bytes);
    return (weight + scale - 1) / scale | 0;
  }

  /**
   * Calculate the weight of the transaction.
   * Note that this is cached.
   * @returns {Number} weight
   */

  getWeight() {
    const raw = this.getSizes();
    const base = raw.size - raw.witness;
    return base * (consensus.WITNESS_SCALE_FACTOR - 1) + raw.size;
  }

  /**
   * Calculate the real size of the transaction
   * with the witness included.
   * @returns {Number} size
   */

  getSize() {
    return this.getSizes().size;
  }

  /**
   * Calculate the size of the transaction
   * without the witness.
   * with the witness included.
   * @returns {Number} size
   */

  getBaseSize() {
    const raw = this.getSizes();
    return raw.size - raw.witness;
  }

  /**
   * Test whether the transaction has a non-empty witness.
   * @returns {Boolean}
   */

  hasWitness() {
    if (this._witness !== -1)
      return this._witness !== 0;

    for (const input of this.inputs) {
      if (input.witness.items.length > 0)
        return true;
    }

    return false;
  }

  /**
   * Get the signature hash of the transaction for signing verifying.
   * @param {Number} index - Index of input being signed/verified.
   * @param {Script} prev - Previous output script or redeem script
   * (in the case of witnesspubkeyhash, this should be the generated
   * p2pkh script).
   * @param {Amount} value - Previous output value.
   * @param {SighashType} type - Sighash type.
   * @param {Number} version - Sighash version (0=legacy, 1=segwit).
   * @returns {Buffer} Signature hash.
   */

  signatureHash(index, prev, value, type, version) {
    assert(index >= 0 && index < this.inputs.length);
    assert(prev instanceof Script);
    assert(typeof value === 'number');
    assert(typeof type === 'number');

    // Traditional sighashing
    if (version === 0)
      return this.signatureHashV0(index, prev, type);

    // Segwit sighashing
    if (version === 1)
      return this.signatureHashV1(index, prev, value, type);

    throw new Error('Unknown sighash version.');
  }

  /**
   * Legacy sighashing -- O(n^2).
   * @private
   * @param {Number} index
   * @param {Script} prev
   * @param {SighashType} type
   * @returns {Buffer}
   */

  signatureHashV0(index, prev, type) {
    if ((type & 0x1f) === hashType.SINGLE) {
      // Bitcoind used to return 1 as an error code:
      // it ended up being treated like a hash.
      if (index >= this.outputs.length) {
        const hash = Buffer.alloc(32, 0x00);
        hash[0] = 0x01;
        return hash;
      }
    }

    // Remove all code separators.
    prev = prev.removeSeparators();

    // Calculate buffer size.
    const size = this.hashSize(index, prev, type);
    const bw = bio.pool(size);

    bw.writeU32(this.version);

    // Serialize inputs.
    if (type & hashType.ANYONECANPAY) {
      // Serialize only the current
      // input if ANYONECANPAY.
      const input = this.inputs[index];

      // Count.
      bw.writeVarint(1);

      // Outpoint.
      input.prevout.toWriter(bw);

      // Replace script with previous
      // output script if current index.
      bw.writeVarBytes(prev.toRaw());
      bw.writeU32(input.sequence);
    } else {
      bw.writeVarint(this.inputs.length);
      for (let i = 0; i < this.inputs.length; i++) {
        const input = this.inputs[i];

        // Outpoint.
        input.prevout.toWriter(bw);

        // Replace script with previous
        // output script if current index.
        if (i === index) {
          bw.writeVarBytes(prev.toRaw());
          bw.writeU32(input.sequence);
          continue;
        }

        // Script is null.
        bw.writeVarint(0);

        // Sequences are 0 if NONE or SINGLE.
        switch (type & 0x1f) {
          case hashType.NONE:
          case hashType.SINGLE:
            bw.writeU32(0);
            break;
          default:
            bw.writeU32(input.sequence);
            break;
        }
      }
    }

    // Serialize outputs.
    switch (type & 0x1f) {
      case hashType.NONE: {
        // No outputs if NONE.
        bw.writeVarint(0);
        break;
      }
      case hashType.SINGLE: {
        const output = this.outputs[index];

        // Drop all outputs after the
        // current input index if SINGLE.
        bw.writeVarint(index + 1);

        for (let i = 0; i < index; i++) {
          // Null all outputs not at
          // current input index.
          bw.writeI64(-1);
          bw.writeVarint(0);
        }

        // Regular serialization
        // at current input index.
        output.toWriter(bw);

        break;
      }
      default: {
        // Regular output serialization if ALL.
        bw.writeVarint(this.outputs.length);
        for (const output of this.outputs)
          output.toWriter(bw);
        break;
      }
    }

    bw.writeU32(this.locktime);

    // Append the hash type.
    bw.writeU32(type);

    return hash256.digest(bw.render());
  }

  /**
   * Calculate sighash size.
   * @private
   * @param {Number} index
   * @param {Script} prev
   * @param {Number} type
   * @returns {Number}
   */

  hashSize(index, prev, type) {
    let size = 0;

    size += 4;

    if (type & hashType.ANYONECANPAY) {
      size += 1;
      size += 36;
      size += prev.getVarSize();
      size += 4;
    } else {
      size += encoding.sizeVarint(this.inputs.length);
      size += 41 * (this.inputs.length - 1);
      size += 36;
      size += prev.getVarSize();
      size += 4;
    }

    switch (type & 0x1f) {
      case hashType.NONE:
        size += 1;
        break;
      case hashType.SINGLE:
        size += encoding.sizeVarint(index + 1);
        size += 9 * index;
        size += this.outputs[index].getSize();
        break;
      default:
        size += encoding.sizeVarint(this.outputs.length);
        for (const output of this.outputs)
          size += output.getSize();
        break;
    }

    size += 8;

    return size;
  }

  /**
   * Witness sighashing -- O(n).
   * @private
   * @param {Number} index
   * @param {Script} prev
   * @param {Amount} value
   * @param {SighashType} type
   * @returns {Buffer}
   */

  signatureHashV1(index, prev, value, type) {
    const input = this.inputs[index];
    let prevouts = consensus.ZERO_HASH;
    let sequences = consensus.ZERO_HASH;
    let outputs = consensus.ZERO_HASH;

    if (!(type & hashType.ANYONECANPAY)) {
      if (this._hashPrevouts) {
        prevouts = this._hashPrevouts;
      } else {
        const bw = bio.pool(this.inputs.length * 36);

        for (const input of this.inputs)
          input.prevout.toWriter(bw);

        prevouts = hash256.digest(bw.render());

        if (!this.mutable)
          this._hashPrevouts = prevouts;
      }
    }

    if (!(type & hashType.ANYONECANPAY)
        && (type & 0x1f) !== hashType.SINGLE
        && (type & 0x1f) !== hashType.NONE) {
      if (this._hashSequence) {
        sequences = this._hashSequence;
      } else {
        const bw = bio.pool(this.inputs.length * 4);

        for (const input of this.inputs)
          bw.writeU32(input.sequence);

        sequences = hash256.digest(bw.render());

        if (!this.mutable)
          this._hashSequence = sequences;
      }
    }

    if ((type & 0x1f) !== hashType.SINGLE
        && (type & 0x1f) !== hashType.NONE) {
      if (this._hashOutputs) {
        outputs = this._hashOutputs;
      } else {
        let size = 0;

        for (const output of this.outputs)
          size += output.getSize();

        const bw = bio.pool(size);

        for (const output of this.outputs)
          output.toWriter(bw);

        outputs = hash256.digest(bw.render());

        if (!this.mutable)
          this._hashOutputs = outputs;
      }
    } else if ((type & 0x1f) === hashType.SINGLE) {
      if (index < this.outputs.length) {
        const output = this.outputs[index];
        outputs = hash256.digest(output.toRaw());
      }
    }

    const size = 156 + prev.getVarSize();
    const bw = bio.pool(size);

    bw.writeU32(this.version);
    bw.writeBytes(prevouts);
    bw.writeBytes(sequences);
    bw.writeHash(input.prevout.hash);
    bw.writeU32(input.prevout.index);
    bw.writeVarBytes(prev.toRaw());
    bw.writeI64(value);
    bw.writeU32(input.sequence);
    bw.writeBytes(outputs);
    bw.writeU32(this.locktime);
    bw.writeU32(type);

    return hash256.digest(bw.render());
  }

  /**
   * Verify signature.
   * @param {Number} index
   * @param {Script} prev
   * @param {Amount} value
   * @param {Buffer} sig
   * @param {Buffer} key
   * @param {Number} version
   * @returns {Boolean}
   */

  checksig(index, prev, value, sig, key, version) {
    if (sig.length === 0)
      return false;

    const type = sig[sig.length - 1];
    const hash = this.signatureHash(index, prev, value, type, version);

    return secp256k1.verifyDER(hash, sig.slice(0, -1), key);
  }

  /**
   * Create a signature suitable for inserting into scriptSigs/witnesses.
   * @param {Number} index - Index of input being signed.
   * @param {Script} prev - Previous output script or redeem script
   * (in the case of witnesspubkeyhash, this should be the generated
   * p2pkh script).
   * @param {Amount} value - Previous output value.
   * @param {Buffer} key
   * @param {SighashType} type
   * @param {Number} version - Sighash version (0=legacy, 1=segwit).
   * @returns {Buffer} Signature in DER format.
   */

  signature(index, prev, value, key, type, version) {
    if (type == null)
      type = hashType.ALL;

    if (version == null)
      version = 0;

    const hash = this.signatureHash(index, prev, value, type, version);
    const sig = secp256k1.signDER(hash, key);
    const bw = bio.write(sig.length + 1);

    bw.writeBytes(sig);
    bw.writeU8(type);

    return bw.render();
  }

  /**
   * Verify all transaction inputs.
   * @param {CoinView} view
   * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS]
   * @throws {ScriptError} on invalid inputs
   */

  check(view, flags) {
    if (this.inputs.length === 0)
      throw new ScriptError('UNKNOWN_ERROR', 'No inputs.');

    if (this.isCoinbase())
      return;

    for (let i = 0; i < this.inputs.length; i++) {
      const {prevout} = this.inputs[i];
      const coin = view.getOutput(prevout);

      if (!coin)
        throw new ScriptError('UNKNOWN_ERROR', 'No coin available.');

      this.checkInput(i, coin, flags);
    }
  }

  /**
   * Verify a transaction input.
   * @param {Number} index - Index of output being
   * verified.
   * @param {Coin|Output} coin - Previous output.
   * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS]
   * @throws {ScriptError} on invalid input
   */

  checkInput(index, coin, flags) {
    const input = this.inputs[index];

    assert(input, 'Input does not exist.');
    assert(coin, 'No coin passed.');

    Script.verify(
      input.script,
      input.witness,
      coin.script,
      this,
      index,
      coin.value,
      flags
    );
  }

  /**
   * Verify the transaction inputs on the worker pool
   * (if workers are enabled).
   * @param {CoinView} view
   * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS]
   * @param {WorkerPool?} pool
   * @returns {Promise}
   */

  async checkAsync(view, flags, pool) {
    if (this.inputs.length === 0)
      throw new ScriptError('UNKNOWN_ERROR', 'No inputs.');

    if (this.isCoinbase())
      return;

    if (!pool) {
      this.check(view, flags);
      return;
    }

    await pool.check(this, view, flags);
  }

  /**
   * Verify a transaction input asynchronously.
   * @param {Number} index - Index of output being
   * verified.
   * @param {Coin|Output} coin - Previous output.
   * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS]
   * @param {WorkerPool?} pool
   * @returns {Promise}
   */

  async checkInputAsync(index, coin, flags, pool) {
    const input = this.inputs[index];

    assert(input, 'Input does not exist.');
    assert(coin, 'No coin passed.');

    if (!pool) {
      this.checkInput(index, coin, flags);
      return;
    }

    await pool.checkInput(this, index, coin, flags);
  }

  /**
   * Verify all transaction inputs.
   * @param {CoinView} view
   * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS]
   * @returns {Boolean} Whether the inputs are valid.
   */

  verify(view, flags) {
    try {
      this.check(view, flags);
    } catch (e) {
      if (e.type === 'ScriptError')
        return false;
      throw e;
    }
    return true;
  }

  /**
   * Verify a transaction input.
   * @param {Number} index - Index of output being
   * verified.
   * @param {Coin|Output} coin - Previous output.
   * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS]
   * @returns {Boolean} Whether the input is valid.
   */

  verifyInput(index, coin, flags) {
    try {
      this.checkInput(index, coin, flags);
    } catch (e) {
      if (e.type === 'ScriptError')
        return false;
      throw e;
    }
    return true;
  }

  /**
   * Verify the transaction inputs on the worker pool
   * (if workers are enabled).
   * @param {CoinView} view
   * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS]
   * @param {WorkerPool?} pool
   * @returns {Promise}
   */

  async verifyAsync(view, flags, pool) {
    try {
      await this.checkAsync(view, flags, pool);
    } catch (e) {
      if (e.type === 'ScriptError')
        return false;
      throw e;
    }
    return true;
  }

  /**
   * Verify a transaction input asynchronously.
   * @param {Number} index - Index of output being
   * verified.
   * @param {Coin|Output} coin - Previous output.
   * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS]
   * @param {WorkerPool?} pool
   * @returns {Promise}
   */

  async verifyInputAsync(index, coin, flags, pool) {
    try {
      await this.checkInput(index, coin, flags, pool);
    } catch (e) {
      if (e.type === 'ScriptError')
        return false;
      throw e;
    }
    return true;
  }

  /**
   * Test whether the transaction is a coinbase
   * by examining the inputs.
   * @returns {Boolean}
   */

  isCoinbase() {
    return this.inputs.length === 1 && this.inputs[0].prevout.isNull();
  }

  /**
   * Test whether the transaction is replaceable.
   * @returns {Boolean}
   */

  isRBF() {
    // Core doesn't do this, but it should:
    if (this.version === 2)
      return false;

    for (const input of this.inputs) {
      if (input.isRBF())
        return true;
    }

    return false;
  }

  /**
   * Calculate the fee for the transaction.
   * @param {CoinView} view
   * @returns {Amount} fee (zero if not all coins are available).
   */

  getFee(view) {
    if (!this.hasCoins(view))
      return 0;

    return this.getInputValue(view) - this.getOutputValue();
  }

  /**
   * Calculate the total input value.
   * @param {CoinView} view
   * @returns {Amount} value
   */

  getInputValue(view) {
    let total = 0;

    for (const {prevout} of this.inputs) {
      const coin = view.getOutput(prevout);

      if (!coin)
        return 0;

      total += coin.value;
    }

    return total;
  }

  /**
   * Calculate the total output value.
   * @returns {Amount} value
   */

  getOutputValue() {
    let total = 0;

    for (const output of this.outputs)
      total += output.value;

    return total;
  }

  /**
   * Get all input addresses.
   * @private
   * @param {CoinView} view
   * @returns {Array} [addrs, table]
   */

  _getInputAddresses(view) {
    const table = new BufferSet();
    const addrs = [];

    if (this.isCoinbase())
      return [addrs, table];

    for (const input of this.inputs) {
      const coin = view ? view.getOutputFor(input) : null;
      const addr = input.getAddress(coin);

      if (!addr)
        continue;

      const hash = addr.getHash();

      if (!table.has(hash)) {
        table.add(hash);
        addrs.push(addr);
      }
    }

    return [addrs, table];
  }

  /**
   * Get all output addresses.
   * @private
   * @returns {Array} [addrs, table]
   */

  _getOutputAddresses() {
    const table = new BufferSet();
    const addrs = [];

    for (const output of this.outputs) {
      const addr = output.getAddress();

      if (!addr)
        continue;

      const hash = addr.getHash();

      if (!table.has(hash)) {
        table.add(hash);
        addrs.push(addr);
      }
    }

    return [addrs, table];
  }

  /**
   * Get all addresses.
   * @private
   * @param {CoinView} view
   * @returns {Array} [addrs, table]
   */

  _getAddresses(view) {
    const [addrs, table] = this._getInputAddresses(view);
    const output = this.getOutputAddresses();

    for (const addr of output) {
      const hash = addr.getHash();

      if (!table.has(hash)) {
        table.add(hash);
        addrs.push(addr);
      }
    }

    return [addrs, table];
  }

  /**
   * Get all input addresses.
   * @param {CoinView|null} view
   * @returns {Address[]} addresses
   */

  getInputAddresses(view) {
    const [addrs] = this._getInputAddresses(view);
    return addrs;
  }

  /**
   * Get all output addresses.
   * @returns {Address[]} addresses
   */

  getOutputAddresses() {
    const [addrs] = this._getOutputAddresses();
    return addrs;
  }

  /**
   * Get all addresses.
   * @param {CoinView|null} view
   * @returns {Address[]} addresses
   */

  getAddresses(view) {
    const [addrs] = this._getAddresses(view);
    return addrs;
  }

  /**
   * Get all input address hashes.
   * @param {CoinView|null} view
   * @returns {Hash[]} hashes
   */

  getInputHashes(view, enc) {
    const [, table] = this._getInputAddresses(view);

    if (enc !== 'hex')
      return table.toArray();

    return table.toArray().map(h => h.toString('hex'));
  }

  /**
   * Get all output address hashes.
   * @returns {Hash[]} hashes
   */

  getOutputHashes(enc) {
    const [, table] = this._getOutputAddresses();

    if (enc !== 'hex')
      return table.toArray();

    return table.toArray().map(h => h.toString('hex'));
  }

  /**
   * Get all address hashes.
   * @param {CoinView|null} view
   * @returns {Hash[]} hashes
   */

  getHashes(view, enc) {
    const [, table] = this._getAddresses(view);

    if (enc !== 'hex')
      return table.toArray();

    return table.toArray().map(h => h.toString('hex'));
  }

  /**
   * Test whether the transaction has
   * all coins available.
   * @param {CoinView} view
   * @returns {Boolean}
   */

  hasCoins(view) {
    if (this.inputs.length === 0)
      return false;

    for (const {prevout} of this.inputs) {
      if (!view.hasEntry(prevout))
        return false;
    }

    return true;
  }

  /**
   * Check finality of transaction by examining
   * nLocktime and nSequence values.
   * @example
   * tx.isFinal(chain.height + 1, network.now());
   * @param {Number} height - Height at which to test. This
   * is usually the chain height, or the chain height + 1
   * when the transaction entered the mempool.
   * @param {Number} time - Time at which to test. This is
   * usually the chain tip's parent's median time, or the
   * time at which the transaction entered the mempool. If
   * MEDIAN_TIME_PAST is enabled this will be the median
   * time of the chain tip's previous entry's median time.
   * @returns {Boolean}
   */

  isFinal(height, time) {
    const THRESHOLD = consensus.LOCKTIME_THRESHOLD;

    if (this.locktime === 0)
      return true;

    if (this.locktime < (this.locktime < THRESHOLD ? height : time))
      return true;

    for (const input of this.inputs) {
      if (input.sequence !== 0xffffffff)
        return false;
    }

    return true;
  }

  /**
   * Verify the absolute locktime of a transaction.
   * Called by OP_CHECKLOCKTIMEVERIFY.
   * @param {Number} index - Index of input being verified.
   * @param {Number} predicate - Locktime to verify against.
   * @returns {Boolean}
   */

  verifyLocktime(index, predicate) {
    const THRESHOLD = consensus.LOCKTIME_THRESHOLD;
    const input = this.inputs[index];

    assert(input, 'Input does not exist.');
    assert(predicate >= 0, 'Locktime must be non-negative.');

    // Locktimes must be of the same type (blocks or seconds).
    if ((this.locktime < THRESHOLD) !== (predicate < THRESHOLD))
      return false;

    if (predicate > this.locktime)
      return false;

    if (input.sequence === 0xffffffff)
      return false;

    return true;
  }

  /**
   * Verify the relative locktime of an input.
   * Called by OP_CHECKSEQUENCEVERIFY.
   * @param {Number} index - Index of input being verified.
   * @param {Number} predicate - Relative locktime to verify against.
   * @returns {Boolean}
   */

  verifySequence(index, predicate) {
    const DISABLE_FLAG = consensus.SEQUENCE_DISABLE_FLAG;
    const TYPE_FLAG = consensus.SEQUENCE_TYPE_FLAG;
    const MASK = consensus.SEQUENCE_MASK;
    const input = this.inputs[index];

    assert(input, 'Input does not exist.');
    assert(predicate >= 0, 'Locktime must be non-negative.');

    // For future softfork capability.
    if (predicate & DISABLE_FLAG)
      return true;

    // Version must be >=2.
    if (this.version < 2)
      return false;

    // Cannot use the disable flag without
    // the predicate also having the disable
    // flag (for future softfork capability).
    if (input.sequence & DISABLE_FLAG)
      return false;

    // Locktimes must be of the same type (blocks or seconds).
    if ((input.sequence & TYPE_FLAG) !== (predicate & TYPE_FLAG))
      return false;

    if ((predicate & MASK) > (input.sequence & MASK))
      return false;

    return true;
  }

  /**
   * Calculate legacy (inaccurate) sigop count.
   * @returns {Number} sigop count
   */

  getLegacySigops() {
    if (this._sigops !== -1)
      return this._sigops;

    let total = 0;

    for (const input of this.inputs)
      total += input.script.getSigops(false);

    for (const output of this.outputs)
      total += output.script.getSigops(false);

    if (!this.mutable)
      this._sigops = total;

    return total;
  }

  /**
   * Calculate accurate sigop count, taking into account redeem scripts.
   * @param {CoinView} view
   * @returns {Number} sigop count
   */

  getScripthashSigops(view) {
    if (this.isCoinbase())
      return 0;

    let total = 0;

    for (const input of this.inputs) {
      const coin = view.getOutputFor(input);

      if (!coin)
        continue;

      if (!coin.script.isScripthash())
        continue;

      total += coin.script.getScripthashSigops(input.script);
    }

    return total;
  }

  /**
   * Calculate accurate sigop count, taking into account redeem scripts.
   * @param {CoinView} view
   * @returns {Number} sigop count
   */

  getWitnessSigops(view) {
    if (this.isCoinbase())
      return 0;

    let total = 0;

    for (const input of this.inputs) {
      const coin = view.getOutputFor(input);

      if (!coin)
        continue;

      total += coin.script.getWitnessSigops(input.script, input.witness);
    }

    return total;
  }

  /**
   * Calculate sigops cost, taking into account witness programs.
   * @param {CoinView} view
   * @param {VerifyFlags?} flags
   * @returns {Number} sigop weight
   */

  getSigopsCost(view, flags) {
    if (flags == null)
      flags = Script.flags.STANDARD_VERIFY_FLAGS;

    const scale = consensus.WITNESS_SCALE_FACTOR;

    let cost = this.getLegacySigops() * scale;

    if (flags & Script.flags.VERIFY_P2SH)
      cost += this.getScripthashSigops(view) * scale;

    if (flags & Script.flags.VERIFY_WITNESS)
      cost += this.getWitnessSigops(view);

    return cost;
  }

  /**
   * Calculate virtual sigop count.
   * @param {CoinView} view
   * @param {VerifyFlags?} flags
   * @returns {Number} sigop count
   */

  getSigops(view, flags) {
    const scale = consensus.WITNESS_SCALE_FACTOR;
    return (this.getSigopsCost(view, flags) + scale - 1) / scale | 0;
  }

  /**
   * Non-contextual sanity checks for the transaction.
   * Will mostly verify coin and output values.
   * @see CheckTransaction()
   * @returns {Array} [result, reason, score]
   */

  isSane() {
    const [valid] = this.checkSanity();
    return valid;
  }

  /**
   * Non-contextual sanity checks for the transaction.
   * Will mostly verify coin and output values.
   * @see CheckTransaction()
   * @returns {Array} [valid, reason, score]
   */

  checkSanity() {
    if (this.inputs.length === 0)
      return [false, 'bad-txns-vin-empty', 100];

    if (this.outputs.length === 0)
      return [false, 'bad-txns-vout-empty', 100];

    if (this.getBaseSize() > consensus.MAX_BLOCK_SIZE)
      return [false, 'bad-txns-oversize', 100];

    let total = 0;

    for (const output of this.outputs) {
      if (output.value < 0)
        return [false, 'bad-txns-vout-negative', 100];

      if (output.value > consensus.MAX_MONEY)
        return [false, 'bad-txns-vout-toolarge', 100];

      total += output.value;

      if (total < 0 || total > consensus.MAX_MONEY)
        return [false, 'bad-txns-txouttotal-toolarge', 100];
    }

    const prevout = new BufferSet();

    for (const input of this.inputs) {
      const key = input.prevout.toKey();

      if (prevout.has(key))
        return [false, 'bad-txns-inputs-duplicate', 100];

      prevout.add(key);
    }

    if (this.isCoinbase()) {
      const size = this.inputs[0].script.getSize();
      if (size < 2 || size > 100)
        return [false, 'bad-cb-length', 100];
    } else {
      for (const input of this.inputs) {
        if (input.prevout.isNull())
          return [false, 'bad-txns-prevout-null', 10];
      }
    }

    return [true, 'valid', 0];
  }

  /**
   * Non-contextual checks to determine whether the
   * transaction has all standard output script
   * types and standard input script size with only
   * pushdatas in the code.
   * Will mostly verify coin and output values.
   * @see IsStandardTx()
   * @returns {Array} [valid, reason, score]
   */

  isStandard() {
    const [valid] = this.checkStandard();
    return valid;
  }

  /**
   * Non-contextual checks to determine whether the
   * transaction has all standard output script
   * types and standard input script size with only
   * pushdatas in the code.
   * Will mostly verify coin and output values.
   * @see IsStandardTx()
   * @returns {Array} [valid, reason, score]
   */

  checkStandard() {
    if (this.version < 1 || this.version > policy.MAX_TX_VERSION)
      return [false, 'version', 0];

    if (this.getWeight() >= policy.MAX_TX_WEIGHT)
      return [false, 'tx-size', 0];

    for (const input of this.inputs) {
      if (input.script.getSize() > 1650)
        return [false, 'scriptsig-size', 0];

      if (!input.script.isPushOnly())
        return [false, 'scriptsig-not-pushonly', 0];
    }

    let nulldata = 0;

    for (const output of this.outputs) {
      if (!output.script.isStandard())
        return [false, 'scriptpubkey', 0];

      if (output.script.isNulldata()) {
        nulldata++;
        continue;
      }

      if (output.script.isMultisig() && !policy.BARE_MULTISIG)
        return [false, 'bare-multisig', 0];

      if (output.isDust(policy.MIN_RELAY))
        return [false, 'dust', 0];
    }

    if (nulldata > 1)
      return [false, 'multi-op-return', 0];

    return [true, 'valid', 0];
  }

  /**
   * Perform contextual checks to verify coin and input
   * script standardness (including the redeem script).
   * @see AreInputsStandard()
   * @param {CoinView} view
   * @param {VerifyFlags?} flags
   * @returns {Boolean}
   */

  hasStandardInputs(view) {
    if (this.isCoinbase())
      return true;

    for (const input of this.inputs) {
      const coin = view.getOutputFor(input);

      if (!coin)
        return false;

      if (coin.script.isPubkeyhash())
        continue;

      if (coin.script.isScripthash()) {
        const redeem = input.script.getRedeem();

        if (!redeem)
          return false;

        if (redeem.getSigops(true) > policy.MAX_P2SH_SIGOPS)
          return false;

        continue;
      }

      if (coin.script.isUnknown())
        return false;
    }

    return true;
  }

  /**
   * Perform contextual checks to verify coin and witness standardness.
   * @see IsBadWitness()
   * @param {CoinView} view
   * @returns {Boolean}
   */

  hasStandardWitness(view) {
    if (this.isCoinbase())
      return true;

    for (const input of this.inputs) {
      const witness = input.witness;
      const coin = view.getOutputFor(input);

      if (!coin)
        continue;

      if (witness.items.length === 0)
        continue;

      let prev = coin.script;

      if (prev.isScripthash()) {
        prev = input.script.getRedeem();
        if (!prev)
          return false;
      }

      if (!prev.isProgram())
        return false;

      if (prev.isWitnessPubkeyhash()) {
        if (witness.items.length !== 2)
          return false;

        if (witness.items[0].length > 73)
          return false;

        if (witness.items[1].length > 65)
          return false;

        continue;
      }

      if (prev.isWitnessScripthash()) {
        if (witness.items.length - 1 > policy.MAX_P2WSH_STACK)
          return false;

        for (let i = 0; i < witness.items.length - 1; i++) {
          const item = witness.items[i];
          if (item.length > policy.MAX_P2WSH_PUSH)
            return false;
        }

        const raw = witness.items[witness.items.length - 1];

        if (raw.length > policy.MAX_P2WSH_SIZE)
          return false;

        const redeem = Script.fromRaw(raw);

        if (redeem.isPubkey()) {
          if (witness.items.length - 1 !== 1)
            return false;

          if (witness.items[0].length > 73)
            return false;

          continue;
        }

        if (redeem.isPubkeyhash()) {
          if (input.witness.items.length - 1 !== 2)
            return false;

          if (witness.items[0].length > 73)
            return false;

          if (witness.items[1].length > 65)
            return false;

          continue;
        }

        const [m] = redeem.getMultisig();

        if (m !== -1) {
          if (witness.items.length - 1 !== m + 1)
            return false;

          if (witness.items[0].length !== 0)
            return false;

          for (let i = 1; i < witness.items.length - 1; i++) {
            const item = witness.items[i];
            if (item.length > 73)
              return false;
          }
        }

        continue;
      }

      if (witness.items.length > policy.MAX_P2WSH_STACK)
        return false;

      for (const item of witness.items) {
        if (item.length > policy.MAX_P2WSH_PUSH)
          return false;
      }
    }

    return true;
  }

  /**
   * Perform contextual checks to verify input, output,
   * and fee values, as well as coinbase spend maturity
   * (coinbases can only be spent 100 blocks or more
   * after they're created). Note that this function is
   * consensus critical.
   * @param {CoinView} view
   * @param {Number} height - Height at which the
   * transaction is being spent. In the mempool this is
   * the chain height plus one at the time it entered the pool.
   * @returns {Boolean}
   */

  verifyInputs(view, height) {
    const [fee] = this.checkInputs(view, height);
    return fee !== -1;
  }

  /**
   * Perform contextual checks to verify input, output,
   * and fee values, as well as coinbase spend maturity
   * (coinbases can only be spent 100 blocks or more
   * after they're created). Note that this function is
   * consensus critical.
   * @param {CoinView} view
   * @param {Number} height - Height at which the
   * transaction is being spent. In the mempool this is
   * the chain height plus one at the time it entered the pool.
   * @returns {Array} [fee, reason, score]
   */

  checkInputs(view, height) {
    assert(typeof height === 'number');

    let total = 0;

    for (const {prevout} of this.inputs) {
      const entry = view.getEntry(prevout);

      if (!entry)
        return [-1, 'bad-txns-inputs-missingorspent', 0];

      if (entry.coinbase) {
        if (height - entry.height < consensus.COINBASE_MATURITY)
          return [-1, 'bad-txns-premature-spend-of-coinbase', 0];
      }

      const coin = view.getOutput(prevout);
      assert(coin);

      if (coin.value < 0 || coin.value > consensus.MAX_MONEY)
        return [-1, 'bad-txns-inputvalues-outofrange', 100];

      total += coin.value;

      if (total < 0 || total > consensus.MAX_MONEY)
        return [-1, 'bad-txns-inputvalues-outofrange', 100];
    }

    // Overflows already checked in `isSane()`.
    const value = this.getOutputValue();

    if (total < value)
      return [-1, 'bad-txns-in-belowout', 100];

    const fee = total - value;

    if (fee < 0)
      return [-1, 'bad-txns-fee-negative', 100];

    if (fee > consensus.MAX_MONEY)
      return [-1, 'bad-txns-fee-outofrange', 100];

    return [fee, 'valid', 0];
  }

  /**
   * Calculate the modified size of the transaction. This
   * is used in the mempool for calculating priority.
   * @param {Number?} size - The size to modify. If not present,
   * virtual size will be used.
   * @returns {Number} Modified size.
   */

  getModifiedSize(size) {
    if (size == null)
      size = this.getVirtualSize();

    for (const input of this.inputs) {
      const offset = 41 + Math.min(110, input.script.getSize());
      if (size > offset)
        size -= offset;
    }

    return size;
  }

  /**
   * Calculate the transaction priority.
   * @param {CoinView} view
   * @param {Number} height
   * @param {Number?} size - Size to calculate priority
   * based on. If not present, virtual size will be used.
   * @returns {Number}
   */

  getPriority(view, height, size) {
    assert(typeof height === 'number', 'Must pass in height.');

    if (this.isCoinbase())
      return 0;

    if (size == null)
      size = this.getVirtualSize();

    let sum = 0;

    for (const {prevout} of this.inputs) {
      const coin = view.getOutput(prevout);

      if (!coin)
        continue;

      const coinHeight = view.getHeight(prevout);

      if (coinHeight === -1)
        continue;

      if (coinHeight <= height) {
        const age = height - coinHeight;
        sum += coin.value * age;
      }
    }

    return Math.floor(sum / size);
  }

  /**
   * Calculate the transaction's on-chain value.
   * @param {CoinView} view
   * @returns {Number}
   */

  getChainValue(view) {
    if (this.isCoinbase())
      return 0;

    let value = 0;

    for (const {prevout} of this.inputs) {
      const coin = view.getOutput(prevout);

      if (!coin)
        continue;

      const height = view.getHeight(prevout);

      if (height === -1)
        continue;

      value += coin.value;
    }

    return value;
  }

  /**
   * Determine whether the transaction is above the
   * free threshold in priority. A transaction which
   * passed this test is most likely relayable
   * without a fee.
   * @param {CoinView} view
   * @param {Number?} height - If not present, tx
   * height or network height will be used.
   * @param {Number?} size - If not present, modified
   * size will be calculated and used.
   * @returns {Boolean}
   */

  isFree(view, height, size) {
    const priority = this.getPriority(view, height, size);
    return priority > policy.FREE_THRESHOLD;
  }

  /**
   * Calculate minimum fee in order for the transaction
   * to be relayable (not the constant min relay fee).
   * @param {Number?} size - If not present, max size
   * estimation will be calculated and used.
   * @param {Rate?} rate - Rate of satoshi per kB.
   * @returns {Amount} fee
   */

  getMinFee(size, rate) {
    if (size == null)
      size = this.getVirtualSize();

    return policy.getMinFee(size, rate);
  }

  /**
   * Calculate the minimum fee in order for the transaction
   * to be relayable, but _round to the nearest kilobyte
   * when taking into account size.
   * @param {Number?} size - If not present, max size
   * estimation will be calculated and used.
   * @param {Rate?} rate - Rate of satoshi per kB.
   * @returns {Amount} fee
   */

  getRoundFee(size, rate) {
    if (size == null)
      size = this.getVirtualSize();

    return policy.getRoundFee(size, rate);
  }

  /**
   * Calculate the transaction's rate based on size
   * and fees. Size will be calculated if not present.
   * @param {CoinView} view
   * @param {Number?} size
   * @returns {Rate}
   */

  getRate(view, size) {
    const fee = this.getFee(view);

    if (fee < 0)
      return 0;

    if (size == null)
      size = this.getVirtualSize();

    return policy.getRate(size, fee);
  }

  /**
   * Get all unique outpoint hashes.
   * @returns {Hash[]} Outpoint hashes.
   */

  getPrevout() {
    if (this.isCoinbase())
      return [];

    const prevout = new BufferSet();

    for (const input of this.inputs)
      prevout.add(input.prevout.hash);

    return prevout.toArray();
  }

  /**
   * Test a transaction against a bloom filter using
   * the BIP37 matching algorithm. Note that this may
   * update the filter depending on what the `update`
   * value is.
   * @see "Filter matching algorithm":
   * @see https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
   * @param {BloomFilter} filter
   * @returns {Boolean} True if the transaction matched.
   */

  isWatched(filter) {
    let found = false;

    // 1. Test the tx hash
    if (filter.test(this.hash()))
      found = true;

    // 2. Test data elements in output scripts
    //    (may need to update filter on match)
    for (let i = 0; i < this.outputs.length; i++) {
      const output = this.outputs[i];
      // Test the output script
      if (output.script.test(filter)) {
        if (filter.update === 1 /* ALL */) {
          const prevout = Outpoint.fromTX(this, i);
          filter.add(prevout.toRaw());
        } else if (filter.update === 2 /* PUBKEY_ONLY */) {
          if (output.script.isPubkey() || output.script.isMultisig()) {
            const prevout = Outpoint.fromTX(this, i);
            filter.add(prevout.toRaw());
          }
        }
        found = true;
      }
    }

    if (found)
      return found;

    // 3. Test prev_out structure
    // 4. Test data elements in input scripts
    for (const input of this.inputs) {
      const prevout = input.prevout;

      // Test the COutPoint structure
      if (filter.test(prevout.toRaw()))
        return true;

      // Test the input script
      if (input.script.test(filter))
        return true;
    }

    // 5. No match
    return false;
  }

  /**
   * Get little-endian tx hash.
   * @returns {Hash}
   */

  rhash() {
    return util.revHex(this.hash());
  }

  /**
   * Get little-endian wtx hash.
   * @returns {Hash}
   */

  rwhash() {
    return util.revHex(this.witnessHash());
  }

  /**
   * Get little-endian tx hash.
   * @returns {Hash}
   */

  txid() {
    return this.rhash();
  }

  /**
   * Get little-endian wtx hash.
   * @returns {Hash}
   */

  wtxid() {
    return this.rwhash();
  }

  /**
   * Convert the tx to an inv item.
   * @returns {InvItem}
   */

  toInv() {
    return new InvItem(InvItem.types.TX, this.hash());
  }

  /**
   * Inspect the transaction and return a more
   * user-friendly representation of the data.
   * @returns {Object}
   */

  [inspectSymbol]() {
    return this.format();
  }

  /**
   * Inspect the transaction and return a more
   * user-friendly representation of the data.
   * @param {CoinView} view
   * @param {ChainEntry} entry
   * @param {Number} index
   * @returns {Object}
   */

  format(view, entry, index) {
    let rate = 0;
    let fee = 0;
    let height = -1;
    let block = null;
    let time = 0;
    let date = null;

    if (view) {
      fee = this.getFee(view);
      rate = this.getRate(view);

      // Rate can exceed 53 bits in testing.
      if (!Number.isSafeInteger(rate))
        rate = 0;
    }

    if (entry) {
      height = entry.height;
      block = util.revHex(entry.hash);
      time = entry.time;
      date = util.date(time);
    }

    if (index == null)
      index = -1;

    return {
      hash: this.txid(),
      witnessHash: this.wtxid(),
      size: this.getSize(),
      virtualSize: this.getVirtualSize(),
      value: Amount.btc(this.getOutputValue()),
      fee: Amount.btc(fee),
      rate: Amount.btc(rate),
      minFee: Amount.btc(this.getMinFee()),
      height: height,
      block: block,
      time: time,
      date: date,
      index: index,
      version: this.version,
      inputs: this.inputs.map((input) => {
        const coin = view ? view.getOutputFor(input) : null;
        return input.format(coin);
      }),
      outputs: this.outputs,
      locktime: this.locktime
    };
  }

  /**
   * Convert the transaction to an object suitable
   * for JSON serialization.
   * @returns {Object}
   */

  toJSON() {
    return this.getJSON();
  }

  /**
   * Convert the transaction to an object suitable
   * for JSON serialization. Note that the hashes
   * will be reversed to abide by bitcoind's legacy
   * of little-endian uint256s.
   * @param {Network} network
   * @param {CoinView} view
   * @param {ChainEntry} entry
   * @param {Number} index
   * @returns {Object}
   */

  getJSON(network, view, entry, index) {
    let rate, fee, height, block, time, date;

    if (view) {
      fee = this.getFee(view);
      rate = this.getRate(view);

      // Rate can exceed 53 bits in testing.
      if (!Number.isSafeInteger(rate))
        rate = 0;
    }

    if (entry) {
      height = entry.height;
      block = util.revHex(entry.hash);
      time = entry.time;
      date = util.date(time);
    }

    network = Network.get(network);

    return {
      hash: this.txid(),
      witnessHash: this.wtxid(),
      fee: fee,
      rate: rate,
      mtime: util.now(),
      height: height,
      block: block,
      time: time,
      date: date,
      index: index,
      version: this.version,
      inputs: this.inputs.map((input) => {
        const coin = view ? view.getCoinFor(input) : null;
        return input.getJSON(network, coin);
      }),
      outputs: this.outputs.map((output) => {
        return output.getJSON(network);
      }),
      locktime: this.locktime,
      hex: this.toRaw().toString('hex')
    };
  }

  /**
   * Inject properties from a json object.
   * @private
   * @param {Object} json
   */

  fromJSON(json) {
    assert(json, 'TX data is required.');
    assert((json.version >>> 0) === json.version, 'Version must be a uint32.');
    assert(Array.isArray(json.inputs), 'Inputs must be an array.');
    assert(Array.isArray(json.outputs), 'Outputs must be an array.');
    assert((json.locktime >>> 0) === json.locktime,
      'Locktime must be a uint32.');

    this.version = json.version;

    for (const input of json.inputs)
      this.inputs.push(Input.fromJSON(input));

    for (const output of json.outputs)
      this.outputs.push(Output.fromJSON(output));

    this.locktime = json.locktime;

    return this;
  }

  /**
   * Instantiate a transaction from a
   * jsonified transaction object.
   * @param {Object} json - The jsonified transaction object.
   * @returns {TX}
   */

  static fromJSON(json) {
    return new this().fromJSON(json);
  }

  /**
   * Instantiate a transaction from a serialized Buffer.
   * @param {Buffer} data
   * @param {String?} enc - Encoding, can be `'hex'` or null.
   * @returns {TX}
   */

  static fromRaw(data, enc) {
    if (typeof data === 'string')
      data = Buffer.from(data, enc);
    return new this().fromRaw(data);
  }

  /**
   * Instantiate a transaction from a buffer reader.
   * @param {BufferReader} br
   * @param {Boolean} block
   * @returns {TX}
   */

  static fromReader(br, block) {
    return new this().fromReader(br, block);
  }

  /**
   * Inject properties from serialized data.
   * @private
   * @param {Buffer} data
   */

  fromRaw(data) {
    return this.fromReader(bio.read(data));
  }

  /**
   * Inject properties from buffer reader.
   * @private
   * @param {BufferReader} br
   * @param {Boolean} block
   */

  fromReader(br, block) {
    if (hasWitnessBytes(br))
      return this.fromWitnessReader(br, block);

    const start = br.start();

    this.version = br.readU32();

    const inCount = br.readVarint();

    for (let i = 0; i < inCount; i++)
      this.inputs.push(Input.fromReader(br));

    const outCount = br.readVarint();

    for (let i = 0; i < outCount; i++)
      this.outputs.push(Output.fromReader(br));

    this.locktime = br.readU32();

    if (block) {
      this._offset = start;
      this._block = true;
    }

    if (!this.mutable) {
      this._raw = br.endData();
      this._size = this._raw.length;
      this._witness = 0;
    } else {
      br.end();
    }

    return this;
  }

  /**
   * Inject properties from serialized
   * buffer reader (witness serialization).
   * @private
   * @param {BufferReader} br
   * @param {Boolean} block
   */

  fromWitnessReader(br, block) {
    const start = br.start();

    this.version = br.readU32();

    assert(br.readU8() === 0, 'Non-zero marker.');

    let flags = br.readU8();

    assert(flags !== 0, 'Flags byte is zero.');

    const inCount = br.readVarint();

    for (let i = 0; i < inCount; i++)
      this.inputs.push(Input.fromReader(br));

    const outCount = br.readVarint();

    for (let i = 0; i < outCount; i++)
      this.outputs.push(Output.fromReader(br));

    let witness = 0;
    let hasWitness = false;

    if (flags & 1) {
      flags ^= 1;

      witness = br.offset;

      for (const input of this.inputs) {
        input.witness.fromReader(br);
        if (input.witness.items.length > 0)
          hasWitness = true;
      }

      witness = (br.offset - witness) + 2;
    }

    if (flags !== 0)
      throw new Error('Unknown witness flag.');

    // We'll never be able to reserialize
    // this to get the regular txid, and
    // there's no way it's valid anyway.
    if (this.inputs.length === 0 && this.outputs.length !== 0)
      throw new Error('Zero input witness tx.');

    this.locktime = br.readU32();

    if (block) {
      this._offset = start;
      this._block = true;
    }

    if (!this.mutable && hasWitness) {
      this._raw = br.endData();
      this._size = this._raw.length;
      this._witness = witness;
    } else {
      br.end();
    }

    return this;
  }

  /**
   * Serialize transaction without witness.
   * @private
   * @returns {RawTX}
   */

  frameNormal() {
    const raw = this.getNormalSizes();
    const bw = bio.write(raw.size);
    this.writeNormal(bw);
    raw.data = bw.render();
    return raw;
  }

  /**
   * Serialize transaction with witness. Calculates the witness
   * size as it is framing (exposed on return value as `witness`).
   * @private
   * @returns {RawTX}
   */

  frameWitness() {
    const raw = this.getWitnessSizes();
    const bw = bio.write(raw.size);
    this.writeWitness(bw);
    raw.data = bw.render();
    return raw;
  }

  /**
   * Serialize transaction without witness.
   * @private
   * @param {BufferWriter} bw
   * @returns {RawTX}
   */

  writeNormal(bw) {
    if (this.inputs.length === 0 && this.outputs.length !== 0)
      throw new Error('Cannot serialize zero-input tx.');

    bw.writeU32(this.version);

    bw.writeVarint(this.inputs.length);

    for (const input of this.inputs)
      input.toWriter(bw);

    bw.writeVarint(this.outputs.length);

    for (const output of this.outputs)
      output.toWriter(bw);

    bw.writeU32(this.locktime);

    return bw;
  }

  /**
   * Serialize transaction with witness. Calculates the witness
   * size as it is framing (exposed on return value as `witness`).
   * @private
   * @param {BufferWriter} bw
   * @returns {RawTX}
   */

  writeWitness(bw) {
    if (this.inputs.length === 0 && this.outputs.length !== 0)
      throw new Error('Cannot serialize zero-input tx.');

    bw.writeU32(this.version);
    bw.writeU8(0);
    bw.writeU8(1);

    bw.writeVarint(this.inputs.length);

    for (const input of this.inputs)
      input.toWriter(bw);

    bw.writeVarint(this.outputs.length);

    for (const output of this.outputs)
      output.toWriter(bw);

    const start = bw.offset;

    for (const input of this.inputs)
      input.witness.toWriter(bw);

    const witness = bw.offset - start;

    bw.writeU32(this.locktime);

    if (witness === this.inputs.length)
      throw new Error('Cannot serialize empty-witness tx.');

    return bw;
  }

  /**
   * Calculate the real size of the transaction
   * without the witness vector.
   * @returns {RawTX}
   */

  getNormalSizes() {
    let base = 0;

    base += 4;

    base += encoding.sizeVarint(this.inputs.length);

    for (const input of this.inputs)
      base += input.getSize();

    base += encoding.sizeVarint(this.outputs.length);

    for (const output of this.outputs)
      base += output.getSize();

    base += 4;

    return new RawTX(base, 0);
  }

  /**
   * Calculate the real size of the transaction
   * with the witness included.
   * @returns {RawTX}
   */

  getWitnessSizes() {
    let base = 0;
    let witness = 0;

    base += 4;
    witness += 2;

    base += encoding.sizeVarint(this.inputs.length);

    for (const input of this.inputs) {
      base += input.getSize();
      witness += input.witness.getVarSize();
    }

    base += encoding.sizeVarint(this.outputs.length);

    for (const output of this.outputs)
      base += output.getSize();

    base += 4;

    return new RawTX(base + witness, witness);
  }

  /**
   * Test whether an object is a TX.
   * @param {Object} obj
   * @returns {Boolean}
   */

  static isTX(obj) {
    return obj instanceof TX;
  }
}

/*
 * Helpers
 */

function hasWitnessBytes(br) {
  if (br.left() < 6)
    return false;

  return br.data[br.offset + 4] === 0
    && br.data[br.offset + 5] !== 0;
}

class RawTX {
  constructor(size, witness) {
    this.data = null;
    this.size = size;
    this.witness = witness;
  }
}

/*
 * Expose
 */

module.exports = TX;