/* eslint-env mocha */
/* eslint prefer-arrow-callback: "off" */

'use strict';

const {inspect} = require('util');
const {encoding} = require('bufio');
const assert = require('./util/assert');
const random = require('bcrypto/lib/random');
const util = require('../lib/utils/util');
const consensus = require('../lib/protocol/consensus');
const TX = require('../lib/primitives/tx');
const MTX = require('../lib/primitives/mtx');
const Output = require('../lib/primitives/output');
const Outpoint = require('../lib/primitives/outpoint');
const Script = require('../lib/script/script');
const Opcode = require('../lib/script/opcode');
const Input = require('../lib/primitives/input');
const CoinView = require('../lib/coins/coinview');
const KeyRing = require('../lib/primitives/keyring');
const Address = require('../lib/primitives/address');
const common = require('./util/common');

const validTests = require('./data/tx-valid.json');
const invalidTests = require('./data/tx-invalid.json');
const sighashTests = require('./data/sighash-tests.json');

const tx1 = common.readTX('tx1');
const tx2 = common.readTX('tx2');
const tx3 = common.readTX('tx3');
const tx4 = common.readTX('tx4');
const tx6 = common.readTX('tx6');
const tx7 = common.readTX('tx7');

const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
const MAX_SAFE_ADDITION = 0xfffffffffffff;

function clearCache(tx, noCache) {
  if (noCache) {
    tx.refresh();
    return;
  }

  const copy = tx.clone();

  assert.bufferEqual(tx.hash(), copy.hash());
}

function parseTXTest(data) {
  const coins = data[0];
  const hex = data[1];
  const names = data[2] || 'NONE';

  let flags = 0;

  for (const name of names.split(',')) {
    const flag = Script.flags[`VERIFY_${name}`];

    if (flag == null)
      throw new Error(`Unknown flag: ${name}.`);

    flags |= flag;
  }

  const view = new CoinView();

  for (const [txid, index, str, amount] of coins) {
    const hash = util.revHex(txid);
    const script = Script.fromString(str);
    const value = parseInt(amount || '0', 10);

    // Ignore the coinbase tests.
    // They should all fail.
    if ((index >>> 0) === 0xffffffff)
      continue;

    const prevout = new Outpoint(hash, index);
    const output = new Output({script, value});

    view.addOutput(prevout, output);
  }

  const raw = Buffer.from(hex, 'hex');
  const tx = TX.fromRaw(raw);

  const coin = view.getOutputFor(tx.inputs[0]);

  return {
    tx: tx,
    flags: flags,
    view: view,
    comments: coin
      ? inspect(coin.script)
      : 'coinbase',
    data: data
  };
}

function parseSighashTest(data) {
  const [
    txHex,
    scriptHex,
    index,
    type,
    regularHash,
    noforkidHash,
    replayProtectedHash
  ] = data;

  const tx = TX.fromRaw(txHex, 'hex');
  const script = Script.fromRaw(scriptHex, 'hex');

  let hex = type & 3;

  if (type & 0x80)
    hex |= 0x80;

  hex = hex.toString(16);

  if (hex.length % 2 !== 0)
    hex = '0' + hex;

  return {
    tx: tx,
    script: script,
    index: index,
    type: type,
    regularHash: regularHash,
    noforkidHash: noforkidHash,
    replayProtectedHash: replayProtectedHash,
    hex: hex
  };
}

function createInput(value, view) {
  const hash = random.randomBytes(32).toString('hex');

  const input = {
    prevout: {
      hash: hash,
      index: 0
    }
  };

  const output = new Output();
  output.value = value;

  if (!view)
    view = new CoinView();

  view.addOutput(new Outpoint(hash, 0), output);

  return [input, view];
};

function sigopContext(scriptSig, scriptPubkey) {
  const fund = new TX();

  {
    fund.version = 1;

    const input = new Input();
    fund.inputs.push(input);

    const output = new Output();
    output.value = 1;
    output.script = scriptPubkey;
    fund.outputs.push(output);

    fund.refresh();
  }

  const spend = new TX();

  {
    spend.version = 1;

    const input = new Input();
    input.prevout.hash = fund.hash('hex');
    input.prevout.index = 0;
    input.script = scriptSig;
    spend.inputs.push(input);

    const output = new Output();
    output.value = 1;
    spend.outputs.push(output);

    spend.refresh();
  }

  const view = new CoinView();

  view.addTX(fund, 0);

  return {
    fund: fund,
    spend: spend,
    view: view
  };
}

describe('TX', function() {
  for (const noCache of [false, true]) {
    const suffix = noCache ? 'without cache' : 'with cache';

    it(`should verify non-minimal output ${suffix}`, () => {
      const [tx, view] = tx1.getTX();
      clearCache(tx, noCache);
      assert(tx.verify(view, Script.flags.VERIFY_P2SH));
    });

    it(`should verify tx.version == 0 ${suffix}`, () => {
      const [tx, view] = tx2.getTX();
      clearCache(tx, noCache);
      assert(tx.verify(view, Script.flags.VERIFY_P2SH));
    });

    it(`should verify sighash_single bug w/ findanddelete ${suffix}`, () => {
      const [tx, view] = tx3.getTX();
      clearCache(tx, noCache);
      assert(tx.verify(view, Script.flags.VERIFY_P2SH));
    });

    it(`should verify high S value with only DERSIG enabled ${suffix}`, () => {
      const [tx, view] = tx4.getTX();
      const coin = view.getOutputFor(tx.inputs[0]);
      const flags = Script.flags.VERIFY_P2SH | Script.flags.VERIFY_DERSIG;
      clearCache(tx, noCache);
      assert(tx.verifyInput(0, coin, flags));
    });

    it(`should verify the coolest tx ever sent ${suffix}`, () => {
      const [tx, view] = tx6.getTX();
      clearCache(tx, noCache);
      assert(tx.verify(view, Script.flags.VERIFY_NONE));
    });

    it(`should verify a historical transaction ${suffix}`, () => {
      const [tx, view] = tx7.getTX();
      clearCache(tx, noCache);

      const flags = Script.flags.STANDARD_VERIFY_FLAGS
        ^ Script.flags.VERIFY_SIGHASH_FORKID;

      assert(tx.verify(view, flags));
    });

    for (const tests of [validTests, invalidTests]) {
      let comment = '';

      for (const json of tests) {
        if (json.length === 1) {
          comment += ' ' + json[0];
          continue;
        }

        const data = parseTXTest(json);
        const {tx, view, flags} = data;
        const comments = comment.trim() || data.comments;

        comment = '';

        if (tests === validTests) {
          if (comments.indexOf('Coinbase') === 0) {
            it(`should handle valid tx test ${suffix}: ${comments}`, () => {
              clearCache(tx, noCache);
              assert.ok(tx.isSane());
            });
            continue;
          }
          it(`should handle valid tx test ${suffix}: ${comments}`, () => {
            clearCache(tx, noCache);
            assert.ok(tx.verify(view, flags));
          });
        } else {
          if (comments === 'Duplicate inputs') {
            it(`should handle invalid tx test ${suffix}: ${comments}`, () => {
              clearCache(tx, noCache);
              assert.ok(tx.verify(view, flags));
              assert.ok(!tx.isSane());
            });
            continue;
          }
          if (comments === 'Negative output') {
            it(`should handle invalid tx test ${suffix}: ${comments}`, () => {
              clearCache(tx, noCache);
              assert.ok(tx.verify(view, flags));
              assert.ok(!tx.isSane());
            });
            continue;
          }
          if (comments.indexOf('Coinbase') === 0) {
            it(`should handle invalid tx test ${suffix}: ${comments}`, () => {
              clearCache(tx, noCache);
              assert.ok(!tx.isSane());
            });
            continue;
          }

          it(`should handle invalid tx test ${suffix}: ${comments}`, () => {
            clearCache(tx, noCache);
            assert.ok(!tx.verify(view, flags));
          });
        }
      }
    }

    for (const json of sighashTests) {
      if (json.length === 1)
        continue;

      const test = parseSighashTest(json);
      const {tx, script, index, type, hex} = test;
      const {regularHash, noforkidHash, replayProtectedHash} = test;

      clearCache(tx, noCache);

      it(`should get sighash of ${regularHash} (${hex}) ${suffix}`, () => {
        let subscript = script;

        if (!(type & Script.hashType.SIGHASH_FORKID))
          subscript = script.getSubscript(0).removeSeparators();

        const reghash = tx.signatureHash(index, subscript, 0, type);
        const rreg = util.revHex(regularHash);

        const oldhash = tx.signatureHash(index, subscript, 0, type, 0);
        const rold = util.revHex(noforkidHash);

        const repflags = Script.flags.VERIFY_SIGHASH_FORKID
          | Script.flags.VERIFY_REPLAY_PROTECTION;

        const rephash = tx.signatureHash(index, subscript, 0, type, repflags);
        const rrph = util.revHex(replayProtectedHash);

        assert.strictEqual(reghash.toString('hex'), rreg,
          'Regular sighash was not correct.');
        assert.strictEqual(oldhash.toString('hex'), rold,
          'No Fork ID sighash was not correct.');
        assert.strictEqual(rephash.toString('hex'), rrph,
          'Replay protected hash was not correct.'
        );
      });
    }
  }

  it('should fail on >51 bit coin values', () => {
    const [input, view] = createInput(consensus.MAX_MONEY + 1);
    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: consensus.MAX_MONEY
      }],
      locktime: 0
    });
    assert.ok(tx.isSane());
    assert.ok(!tx.verifyInputs(view, 0));
  });

  it('should handle 51 bit coin values', () => {
    const [input, view] = createInput(consensus.MAX_MONEY);
    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: consensus.MAX_MONEY
      }],
      locktime: 0
    });
    assert.ok(tx.isSane());
    assert.ok(tx.verifyInputs(view, 0));
  });

  it('should fail on >51 bit output values', () => {
    const [input, view] = createInput(consensus.MAX_MONEY);
    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: consensus.MAX_MONEY + 1
      }],
      locktime: 0
    });
    assert.ok(!tx.isSane());
    assert.ok(!tx.verifyInputs(view, 0));
  });

  it('should handle 51 bit output values', () => {
    const [input, view] = createInput(consensus.MAX_MONEY);
    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: consensus.MAX_MONEY
      }],
      locktime: 0
    });
    assert.ok(tx.isSane());
    assert.ok(tx.verifyInputs(view, 0));
  });

  it('should fail on >51 bit fees', () => {
    const [input, view] = createInput(consensus.MAX_MONEY + 1);
    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: 0
      }],
      locktime: 0
    });
    assert.ok(tx.isSane());
    assert.ok(!tx.verifyInputs(view, 0));
  });

  it('should fail on >51 bit values from multiple', () => {
    const view = new CoinView();
    const tx = new TX({
      version: 1,
      inputs: [
        createInput(Math.floor(consensus.MAX_MONEY / 2), view)[0],
        createInput(Math.floor(consensus.MAX_MONEY / 2), view)[0],
        createInput(Math.floor(consensus.MAX_MONEY / 2), view)[0]
      ],
      outputs: [{
        script: [],
        value: consensus.MAX_MONEY
      }],
      locktime: 0
    });
    assert.ok(tx.isSane());
    assert.ok(!tx.verifyInputs(view, 0));
  });

  it('should fail on >51 bit output values from multiple', () => {
    const [input, view] = createInput(consensus.MAX_MONEY);
    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [
        {
          script: [],
          value: Math.floor(consensus.MAX_MONEY / 2)
        },
        {
          script: [],
          value: Math.floor(consensus.MAX_MONEY / 2)
        },
        {
          script: [],
          value: Math.floor(consensus.MAX_MONEY / 2)
        }
      ],
      locktime: 0
    });
    assert.ok(!tx.isSane());
    assert.ok(!tx.verifyInputs(view, 0));
  });

  it('should fail on >51 bit fees from multiple', () => {
    const view = new CoinView();
    const tx = new TX({
      version: 1,
      inputs: [
        createInput(Math.floor(consensus.MAX_MONEY / 2), view)[0],
        createInput(Math.floor(consensus.MAX_MONEY / 2), view)[0],
        createInput(Math.floor(consensus.MAX_MONEY / 2), view)[0]
      ],
      outputs: [{
        script: [],
        value: 0
      }],
      locktime: 0
    });
    assert.ok(tx.isSane());
    assert.ok(!tx.verifyInputs(view, 0));
  });

  it('should fail to parse >53 bit values', () => {
    const [input] = createInput(Math.floor(consensus.MAX_MONEY / 2));

    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: 0xdeadbeef
      }],
      locktime: 0
    });

    let raw = tx.toRaw();
    assert.strictEqual(encoding.readU64(raw, 47), 0xdeadbeef);
    raw[54] = 0x7f;

    assert.throws(() => TX.fromRaw(raw));

    tx.outputs[0].value = 0;
    tx.refresh();

    raw = tx.toRaw();
    assert.strictEqual(encoding.readU64(raw, 47), 0x00);
    raw[54] = 0x80;
    assert.throws(() => TX.fromRaw(raw));
  });

  it('should fail on 53 bit coin values', () => {
    const [input, view] = createInput(MAX_SAFE_INTEGER);
    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: consensus.MAX_MONEY
      }],
      locktime: 0
    });
    assert.ok(tx.isSane());
    assert.ok(!tx.verifyInputs(view, 0));
  });

  it('should fail on 53 bit output values', () => {
    const [input, view] = createInput(consensus.MAX_MONEY);
    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: MAX_SAFE_INTEGER
      }],
      locktime: 0
    });
    assert.ok(!tx.isSane());
    assert.ok(!tx.verifyInputs(view, 0));
  });

  it('should fail on 53 bit fees', () => {
    const [input, view] = createInput(MAX_SAFE_INTEGER);
    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: 0
      }],
      locktime: 0
    });
    assert.ok(tx.isSane());
    assert.ok(!tx.verifyInputs(view, 0));
  });

  for (const value of [MAX_SAFE_ADDITION, MAX_SAFE_INTEGER]) {
    it('should fail on >53 bit values from multiple', () => {
      const view = new CoinView();
      const tx = new TX({
        version: 1,
        inputs: [
          createInput(value, view)[0],
          createInput(value, view)[0],
          createInput(value, view)[0]
        ],
        outputs: [{
          script: [],
          value: consensus.MAX_MONEY
        }],
        locktime: 0
      });
      assert.ok(tx.isSane());
      assert.ok(!tx.verifyInputs(view, 0));
    });

    it('should fail on >53 bit output values from multiple', () => {
      const [input, view] = createInput(consensus.MAX_MONEY);
      const tx = new TX({
        version: 1,
        inputs: [input],
        outputs: [
          {
            script: [],
            value: value
          },
          {
            script: [],
            value: value
          },
          {
            script: [],
            value: value
          }
        ],
        locktime: 0
      });
      assert.ok(!tx.isSane());
      assert.ok(!tx.verifyInputs(view, 0));
    });

    it('should fail on >53 bit fees from multiple', () => {
      const view = new CoinView();
      const tx = new TX({
        version: 1,
        inputs: [
          createInput(value, view)[0],
          createInput(value, view)[0],
          createInput(value, view)[0]
        ],
        outputs: [{
          script: [],
          value: 0
        }],
        locktime: 0
      });
      assert.ok(tx.isSane());
      assert.ok(!tx.verifyInputs(view, 0));
    });
  }

  it('should count sigops for multisig', () => {
    const flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH;
    const key = KeyRing.generate();
    const pub = key.publicKey;

    const output = Script.fromMultisig(1, 2, [pub, pub]);

    const input = new Script([
      Opcode.fromInt(0),
      Opcode.fromInt(0)
    ]);

    const ctx = sigopContext(input, output);

    assert.strictEqual(ctx.spend.getSigopsCount(ctx.view, flags), 0);
    assert.strictEqual(ctx.fund.getSigopsCount(ctx.view, flags),
      consensus.MAX_MULTISIG_PUBKEYS);
  });

  it('should count sigops for p2sh multisig', () => {
    const flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH;
    const key = KeyRing.generate();
    const pub = key.publicKey;

    const redeem = Script.fromMultisig(1, 2, [pub, pub]);
    const output = Script.fromScripthash(redeem.hash160());

    const input = new Script([
      Opcode.fromInt(0),
      Opcode.fromInt(0),
      Opcode.fromData(redeem.toRaw())
    ]);

    const ctx = sigopContext(input, output);

    assert.strictEqual(ctx.spend.getSigopsCount(ctx.view, flags), 2);
  });

  it('should return addresses for standard inputs', () => {
    // txid: 7ef7cde4e4a7829ea6feaf377c924b36d0958e2231a31ff268bd33a59ac9e178
    const [tx, view] = tx2.getTX();

    const inputAddresses = [
      Address.fromBase58('1Wjrrc2DrtB2CXRiPa3c8528fDdNHnQ2K')
    ];

    const inputAddressesView = tx.getInputAddresses(view);
    const inputAddressesNoView = tx.getInputAddresses();

    assert.strictEqual(inputAddresses.length, inputAddressesView.length);
    assert.strictEqual(inputAddresses.length, inputAddressesNoView.length);

    inputAddresses.forEach((inputAddr, i) => {
      assert(inputAddr.equals(inputAddressesView[i]));
      assert(inputAddr.equals(inputAddressesNoView[i]));
    });
  });

  it('should return addresses for standard outputs', () => {
    // txid: 7f2dc9bcc0b1b0d19a4cb62d0f6474990c12a5b996d2fa2c4be54ca1beb5d339
    const [tx] = tx7.getTX();

    // If you fetch only outputs they should be sorted
    // by vouts, not merged.
    const outputAddresses = [
      Address.fromBase58('1fLeMazoEy8FfgeFcppRxNYZs54jyLccw'),
      Address.fromBase58('1EeREnzQujX7CLgmzDSaebS48jxjeyBHQM')
    ];

    const getOutputAddresses = tx.getOutputAddresses();

    assert.strictEqual(outputAddresses.length, getOutputAddresses.length);

    outputAddresses.forEach((outputAddr, i) => {
      assert(outputAddr.equals(getOutputAddresses[i]));
    });
  });

  it('should return addresses for standard inputs and outputs', () => {
    // txid: 7ef7cde4e4a7829ea6feaf377c924b36d0958e2231a31ff268bd33a59ac9e178
    const [tx, view] = tx2.getTX();

    const addresses = [
      // inputs
      Address.fromBase58('1Wjrrc2DrtB2CXRiPa3c8528fDdNHnQ2K'),
      // outputs
      Address.fromBase58('1GcKLBv6iFSCkbhht2m44qnZTYK8xV8nNA'),
      Address.fromBase58('1EpKnnMo1rSkktYw8vPLtXGBRNLraXWd73')
    ];

    const addressesView = tx.getAddresses(view);
    const addressesNoView = tx.getAddresses();

    assert.strictEqual(addresses.length, addressesView.length);
    assert.strictEqual(addresses.length, addressesNoView.length);

    addresses.forEach((addr, i) => {
      assert(addr.equals(addressesView[i]));
      assert(addr.equals(addressesNoView[i]));
    });
  });

  it('should return merged addresses for same input/output address', () => {
    // txid: 7f2dc9bcc0b1b0d19a4cb62d0f6474990c12a5b996d2fa2c4be54ca1beb5d339
    const [tx, view] = tx7.getTX();

    const addresses = [
      // this is input and output
      Address.fromBase58('1EeREnzQujX7CLgmzDSaebS48jxjeyBHQM'),
      Address.fromBase58('1fLeMazoEy8FfgeFcppRxNYZs54jyLccw')
    ];

    const addressesView = tx.getAddresses(view);
    const addressesNoView = tx.getAddresses();

    assert.strictEqual(addresses.length, addressesView.length);
    assert.strictEqual(addresses.length, addressesNoView.length);

    addresses.forEach((addr, i) => {
      assert(addr.equals(addressesView[i]));
      assert(addr.equals(addressesNoView[i]));
    });
  });

  it('should return address hashes for standard inputs and outputs', () => {
    // txid: 7ef7cde4e4a7829ea6feaf377c924b36d0958e2231a31ff268bd33a59ac9e178
    const [tx, view] = tx2.getTX();

    const hashes = [
      // inputs
      Address.fromBase58('1Wjrrc2DrtB2CXRiPa3c8528fDdNHnQ2K').getHash(),
      // outputs
      Address.fromBase58('1GcKLBv6iFSCkbhht2m44qnZTYK8xV8nNA').getHash(),
      Address.fromBase58('1EpKnnMo1rSkktYw8vPLtXGBRNLraXWd73').getHash()
    ];

    const hashesBuf = tx.getHashes(view);
    const hashesHex = tx.getHashes(view, 'hex');

    assert.strictEqual(hashes.length, hashesBuf.length);
    assert.strictEqual(hashes.length, hashesHex.length);

    hashes.forEach((hash, i) => {
      assert.bufferEqual(hash, hashesBuf[i]);
      assert.strictEqual(hash.toString('hex'), hashesHex[i]);
    });
  });

  it('should return address hashes for standard inputs', () => {
    // txid: 7ef7cde4e4a7829ea6feaf377c924b36d0958e2231a31ff268bd33a59ac9e178
    const [tx, view] = tx2.getTX();

    const inputHashes = [
      Address.fromBase58('1Wjrrc2DrtB2CXRiPa3c8528fDdNHnQ2K').getHash()
    ];

    const hashesBuf = tx.getInputHashes(view);
    const hashesHex = tx.getInputHashes(view, 'hex');

    assert.strictEqual(inputHashes.length, hashesBuf.length);
    assert.strictEqual(inputHashes.length, hashesHex.length);

    inputHashes.forEach((hash, i) => {
      assert.bufferEqual(hash, hashesBuf[i]);
      assert.strictEqual(hash.toString('hex'), hashesHex[i]);
    });
  });

  it('should return address hashes for standard outputs', () => {
    // txid: 7ef7cde4e4a7829ea6feaf377c924b36d0958e2231a31ff268bd33a59ac9e178
    const [tx] = tx2.getTX();

    const outputHashes = [
      Address.fromBase58('1GcKLBv6iFSCkbhht2m44qnZTYK8xV8nNA').getHash(),
      Address.fromBase58('1EpKnnMo1rSkktYw8vPLtXGBRNLraXWd73').getHash()
    ];

    const hashesBuf = tx.getOutputHashes();
    const hashesHex = tx.getOutputHashes('hex');

    assert.strictEqual(outputHashes.length, hashesBuf.length);
    assert.strictEqual(outputHashes.length, hashesHex.length);

    outputHashes.forEach((hash, i) => {
      assert.bufferEqual(hash, hashesBuf[i]);
      assert.strictEqual(hash.toString('hex'), hashesHex[i]);
    });
  });

  it('should return all prevouts', () => {
    const [tx] = tx3.getTX();

    const expectedPrevouts = [
      '2f196cf1e5bd426a04f07b882c893b5b5edebad67da6eb50f066c372ed736d5f',
      'ff8755f073f1170c0d519457ffc4acaa7cb2988148163b5dc457fae0fe42aa19'
    ];

    const prevouts = tx.getPrevout();

    assert(expectedPrevouts.length, prevouts.length);
    expectedPrevouts.forEach((prevout, i) => {
      assert.strictEqual(prevout, prevouts[i]);
    });
  });

  it('should check if tx is free', () => {
    const value = 100000000; // 1 btc
    const height = 100;
    const [input, view] = createInput(value);

    // hack height into coinEntry
    const entry = view.getEntry(input.prevout);
    entry.height = height;

    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: value
      }],
      locktime: 0
    });

    // Priority should be more than FREE_THRESHOLD
    // txsize: 60, value: 1 btc
    // freeAfter: 144/250*txsize = 34.56
    const size = tx.getSize();
    const freeHeight = height + 35;
    const freeAt34 = tx.isFree(view, freeHeight - 1);
    const freeAt34size = tx.isFree(view, freeHeight - 1, tx, size);
    const freeAt35 = tx.isFree(view, freeHeight);
    const freeAt35size = tx.isFree(view, freeHeight, size);

    assert.strictEqual(freeAt34, false);
    assert.strictEqual(freeAt34size, false);
    assert.strictEqual(freeAt35, true);
    assert.strictEqual(freeAt35size, true);
  });

  it('should return correct minFee and roundedFee', () => {
    const value = 100000000; // 1 btc

    const [input] = createInput(value);
    const tx = new TX({
      version: 1,
      inputs: [input],
      outputs: [{
        script: [],
        value: value
      }],
      locktime: 0
    });

    // 1000 satoshis per kb
    const rate = 1000;
    const size = tx.getSize(); // 60 bytes

    // doesn't round to KB
    assert.strictEqual(tx.getMinFee(size, rate), 60);
    assert.strictEqual(tx.getMinFee(size, rate * 10), 600);
    assert.strictEqual(tx.getMinFee(size * 10, rate), 600);

    // rounds to KB
    assert.strictEqual(tx.getRoundFee(size, rate), 1000);
    // still under kb
    assert.strictEqual(tx.getRoundFee(size * 10, rate), 1000);
    assert.strictEqual(tx.getRoundFee(size, rate * 10), 10000);

    assert.strictEqual(tx.getRoundFee(1000, rate), 1000);
    assert.strictEqual(tx.getRoundFee(1001, rate), 2000);
  });

  it('should return JSON for tx', () => {
    const [tx, view] = tx2.getTX();
    const hash = '7ef7cde4e4a7829ea6feaf377c924b36d0958e22'
      + '31a31ff268bd33a59ac9e178';
    const version = 0;
    const locktime = 0;
    const hex = tx2.getRaw().toString('hex');

    // hack for ChainEntry
    const entry = {
      height: 1000,
      hash: 'c82d447db6150d2308d9571c19bc3dc6efde97a8227d9e57bc77ec0900000000',
      time: 1365870306
    };
    const network = 'testnet';
    const index = 0;

    const jsonDefault = tx.getJSON(network);
    const jsonView = tx.getJSON(network, view);
    const jsonEntry = tx.getJSON(network, null, entry);
    const jsonIndex = tx.getJSON(network, null, null, index);
    const jsonAll = tx.getJSON(network, view, entry, index);

    for (const json of [jsonDefault, jsonView, jsonEntry, jsonIndex, jsonAll]) {
      assert.strictEqual(json.hash, hash);
      assert.strictEqual(json.witnessHash, hash);
      assert.strictEqual(json.version, version);
      assert.strictEqual(json.locktime, locktime);
      assert.strictEqual(json.hex, hex);
    }

    const fee = 10000;
    const rate = 44247;

    for (const json of [jsonView, jsonAll]) {
      assert.strictEqual(json.fee, fee);
      assert.strictEqual(json.rate, rate);
    }

    const date = '2013-04-13T16:25:06Z';
    for (const json of [jsonEntry, jsonAll]) {
      assert.strictEqual(json.height, entry.height);
      assert.strictEqual(json.block, util.revHex(entry.hash));
      assert.strictEqual(json.time, entry.time);
      assert.strictEqual(json.date, date);
    }

    for (const json of [jsonIndex, jsonAll]) {
      assert.strictEqual(json.index, index);
    }
  });

  it('should recover coins from JSON', () => {
    const [tx, view] = tx2.getTX();

    const mtx = MTX.fromTX(tx);
    mtx.view = view;

    // get input value as example
    const value1 = mtx.getInputValue();

    const mtx2 = MTX.fromJSON(mtx.toJSON());
    const value2 = mtx2.getInputValue();

    assert.strictEqual(value1, value2);
  });
});
