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

'use strict';

const assert = require('./util/assert');
const consensus = require('../lib/protocol/consensus');
const util = require('../lib/utils/util');
const encoding = require('../lib/utils/encoding');
const digest = require('../lib/crypto/digest');
const random = require('../lib/crypto/random');
const WalletDB = require('../lib/wallet/walletdb');
const WorkerPool = require('../lib/workers/workerpool');
const Address = require('../lib/primitives/address');
const MTX = require('../lib/primitives/mtx');
const Coin = require('../lib/primitives/coin');
const KeyRing = require('../lib/primitives/keyring');
const Input = require('../lib/primitives/input');
const Outpoint = require('../lib/primitives/outpoint');
const Script = require('../lib/script/script');
const HD = require('../lib/hd');

const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt'
  + 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ';

const KEY2 = 'xprv9s21ZrQH143K3mqiSThzPtWAabQ22Pjp3uSNnZ53A5bQ4udp'
  + 'faKekc2m4AChLYH1XDzANhrSdxHYWUeTWjYJwFwWFyHkTMnMeAcW4JyRCZa';

const workers = new WorkerPool({
  enabled: false
});

const wdb = new WalletDB({
  db: 'memory',
  verify: true,
  workers
});

let currentWallet = null;
let importedWallet = null;
let importedKey = null;
let doubleSpendWallet = null;
let doubleSpendCoin = null;

let globalTime = util.now();
let globalHeight = 1;

function nextBlock() {
  const height = globalHeight++;
  const time = globalTime++;

  const prevHead = encoding.U32(height - 1);
  const prevHash = digest.hash256(prevHead);

  const head = encoding.U32(height);
  const hash = digest.hash256(head);

  return {
    hash: hash.toString('hex'),
    height: height,
    prevBlock: prevHash.toString('hex'),
    time: time,
    merkleRoot: encoding.NULL_HASH,
    nonce: 0,
    bits: 0
  };
}

function dummyInput() {
  const hash = random.randomBytes(32).toString('hex');
  return Input.fromOutpoint(new Outpoint(hash, 0));
}

async function testP2PKH(witness, nesting) {
  const flags = Script.flags.STANDARD_VERIFY_FLAGS;

  const wallet = await wdb.create({
    witness
  });

  const addr = Address.fromString(wallet.getAddress('string'));

  const type = witness ? Address.types.WITNESS : Address.types.PUBKEYHASH;
  assert.strictEqual(addr.type, type);

  const src = new MTX();
  src.addInput(dummyInput());
  src.addOutput(nesting ? wallet.getNested() : wallet.getAddress(), 5460 * 2);
  src.addOutput(new Address(), 2 * 5460);

  const mtx = new MTX();
  mtx.addTX(src, 0);
  mtx.addOutput(wallet.getAddress(), 5460);

  await wallet.sign(mtx);

  const [tx, view] = mtx.commit();

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

async function testP2SH(witness, nesting) {
  const flags = Script.flags.STANDARD_VERIFY_FLAGS;
  const receive = nesting ? 'nested' : 'receive';
  const receiveDepth = nesting ? 'nestedDepth' : 'receiveDepth';
  const vector = witness ? 'witness' : 'script';

  // Create 3 2-of-3 wallets with our pubkeys as "shared keys"
  const options = {
    witness,
    type: 'multisig',
    m: 2,
    n: 3
  };

  const alice = await wdb.create(options);
  const bob = await wdb.create(options);
  const carol = await wdb.create(options);
  const recipient = await wdb.create();

  await alice.addSharedKey(bob.account.accountKey);
  await alice.addSharedKey(carol.account.accountKey);

  await bob.addSharedKey(alice.account.accountKey);
  await bob.addSharedKey(carol.account.accountKey);

  await carol.addSharedKey(alice.account.accountKey);
  await carol.addSharedKey(bob.account.accountKey);

  // Our p2sh address
  const addr1 = alice.account[receive].getAddress();

  if (witness) {
    const type = nesting ? Address.types.SCRIPTHASH : Address.types.WITNESS;
    assert.strictEqual(addr1.type, type);
  } else {
    assert.strictEqual(addr1.type, Address.types.SCRIPTHASH);
  }

  assert(alice.account[receive].getAddress().equals(addr1));
  assert(bob.account[receive].getAddress().equals(addr1));
  assert(carol.account[receive].getAddress().equals(addr1));

  const nestedAddr1 = alice.getNested();

  if (witness) {
    assert(nestedAddr1);
    assert(alice.getNested().equals(nestedAddr1));
    assert(bob.getNested().equals(nestedAddr1));
    assert(carol.getNested().equals(nestedAddr1));
  }

  {
    // Add a shared unspent transaction to our wallets
    const fund = new MTX();
    fund.addInput(dummyInput());
    fund.addOutput(nesting ? nestedAddr1 : addr1, 5460 * 10);

    // Simulate a confirmation
    const block = nextBlock();

    assert.strictEqual(alice.account[receiveDepth], 1);

    await wdb.addBlock(block, [fund.toTX()]);

    assert.strictEqual(alice.account[receiveDepth], 2);
    assert.strictEqual(alice.account.changeDepth, 1);
  }

  const addr2 = alice.account[receive].getAddress();
  assert(!addr2.equals(addr1));

  assert(alice.account[receive].getAddress().equals(addr2));
  assert(bob.account[receive].getAddress().equals(addr2));
  assert(carol.account[receive].getAddress().equals(addr2));

  // Create a tx requiring 2 signatures
  const send = new MTX();

  send.addOutput(recipient.getAddress(), 5460);

  assert(!send.verify(flags));

  await alice.fund(send, {
    rate: 10000,
    round: true
  });

  await alice.sign(send);

  assert(!send.verify(flags));

  await bob.sign(send);

  const [tx, view] = send.commit();
  assert(tx.verify(view, flags));

  assert.strictEqual(alice.account.changeDepth, 1);

  const change = alice.account.change.getAddress();

  assert(alice.account.change.getAddress().equals(change));
  assert(bob.account.change.getAddress().equals(change));
  assert(carol.account.change.getAddress().equals(change));

  // Simulate a confirmation
  {
    const block = nextBlock();

    await wdb.addBlock(block, [tx]);

    assert.strictEqual(alice.account[receiveDepth], 2);
    assert.strictEqual(alice.account.changeDepth, 2);

    assert(alice.account[receive].getAddress().equals(addr2));
    assert(!alice.account.change.getAddress().equals(change));
  }

  const change2 = alice.account.change.getAddress();

  assert(alice.account.change.getAddress().equals(change2));
  assert(bob.account.change.getAddress().equals(change2));
  assert(carol.account.change.getAddress().equals(change2));

  const input = tx.inputs[0];
  input[vector].setData(2, encoding.ZERO_SIG);
  input[vector].compile();

  assert(!tx.verify(view, flags));
  assert.strictEqual(tx.getFee(view), 10000);
}

describe('Wallet', function() {
  this.timeout(5000);

  it('should open walletdb', async () => {
    consensus.COINBASE_MATURITY = 0;
    await wdb.open();
  });

  it('should generate new key and address', async () => {
    const wallet = await wdb.create();

    const addr1 = wallet.getAddress();
    assert(addr1);

    const str = addr1.toString();
    const addr2 = Address.fromString(str);

    assert(addr2.equals(addr1));
  });

  it('should validate existing address', () => {
    assert(Address.fromString('1KQ1wMNwXHUYj1nV2xzsRcKUH8gVFpTFUc'));
  });

  it('should fail to validate invalid address', () => {
    assert.throws(() => {
      Address.fromString('1KQ1wMNwXHUYj1nv2xzsRcKUH8gVFpTFUc');
    });
  });

  it('should create and get wallet', async () => {
    const wallet1 = await wdb.create();

    await wallet1.destroy();

    const wallet2 = await wdb.get(wallet1.id);

    assert(wallet1 !== wallet2);
    assert(wallet1.master !== wallet2.master);
    assert(wallet1.master.key.equals(wallet2.master.key));
    assert(wallet1.account.accountKey.equals(wallet2.account.accountKey));
  });

  it('should sign/verify p2pkh tx', async () => {
    await testP2PKH(false, false);
  });

  it('should sign/verify p2wpkh tx', async () => {
    await testP2PKH(true, false);
  });

  it('should sign/verify p2wpkh tx w/ nested bullshit', async () => {
    await testP2PKH(true, true);
  });

  it('should multisign/verify TX', async () => {
    const wallet = await wdb.create({
      type: 'multisig',
      m: 1,
      n: 2
    });

    const xpriv = HD.PrivateKey.generate();
    const key = xpriv.deriveBIP44(0).toPublic();

    await wallet.addSharedKey(key);

    const script = Script.fromMultisig(1, 2, [
      wallet.account.receive.getPublicKey(),
      key.derivePath('m/0/0').publicKey
    ]);

    // Input transaction (bare 1-of-2 multisig)
    const src = new MTX();
    src.addInput(dummyInput());
    src.addOutput(script, 5460 * 2);
    src.addOutput(new Address(), 5460 * 2);

    const tx = new MTX();
    tx.addTX(src, 0);
    tx.addOutput(wallet.getAddress(), 5460);

    const maxSize = await tx.estimateSize();

    await wallet.sign(tx);

    assert(tx.toRaw().length <= maxSize);
    assert(tx.verify());
  });

  it('should handle missed and invalid txs', async () => {
    const alice = await wdb.create();
    const bob = await wdb.create();

    // Coinbase
    // balance: 51000
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(alice.getAddress(), 50000);
    t1.addOutput(alice.getAddress(), 1000);

    const t2 = new MTX();
    t2.addTX(t1, 0); // 50000
    t2.addOutput(alice.getAddress(), 24000);
    t2.addOutput(alice.getAddress(), 24000);

    // Save for later.
    doubleSpendWallet = alice;
    doubleSpendCoin = Coin.fromTX(t1, 0, -1);

    // balance: 49000
    await alice.sign(t2);

    const t3 = new MTX();
    t3.addTX(t1, 1); // 1000
    t3.addTX(t2, 0); // 24000
    t3.addOutput(alice.getAddress(), 23000);

    // balance: 47000
    await alice.sign(t3);

    const t4 = new MTX();
    t4.addTX(t2, 1); // 24000
    t4.addTX(t3, 0); // 23000
    t4.addOutput(alice.getAddress(), 11000);
    t4.addOutput(alice.getAddress(), 11000);

    // balance: 22000
    await alice.sign(t4);

    const f1 = new MTX();
    f1.addTX(t4, 1); // 11000
    f1.addOutput(bob.getAddress(), 10000);

    // balance: 11000
    await alice.sign(f1);

    const fake = new MTX();
    fake.addTX(t1, 1); // 1000 (already redeemed)
    fake.addOutput(alice.getAddress(), 500);

    // Script inputs but do not sign
    await alice.template(fake);
    // Fake signature
    const input = fake.inputs[0];
    input.script.setData(0, encoding.ZERO_SIG);
    input.script.compile();
    // balance: 11000

    // Fake TX should temporarily change output.
    {
      await wdb.addTX(fake.toTX());
      await wdb.addTX(t4.toTX());

      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 22500);
    }

    {
      await wdb.addTX(t1.toTX());

      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 72500);
    }

    {
      await wdb.addTX(t2.toTX());

      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 46500);
    }

    {
      await wdb.addTX(t3.toTX());

      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 22000);
    }

    {
      await wdb.addTX(f1.toTX());

      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 11000);

      const txs = await alice.getHistory();
      assert(txs.some((wtx) => {
        return wtx.hash === f1.hash('hex');
      }));
    }

    {
      const balance = await bob.getBalance();
      assert.strictEqual(balance.unconfirmed, 10000);

      const txs = await bob.getHistory();
      assert(txs.some((wtx) => {
        return wtx.tx.hash('hex') === f1.hash('hex');
      }));
    }
  });

  it('should cleanup spenders after double-spend', async () => {
    const wallet = doubleSpendWallet;

    {
      const txs = await wallet.getHistory();
      assert.strictEqual(txs.length, 5);

      const total = txs.reduce((t, wtx) => {
        return t + wtx.tx.getOutputValue();
      }, 0);
      assert.strictEqual(total, 154000);
    }

    {
      const balance = await wallet.getBalance();
      assert.strictEqual(balance.unconfirmed, 11000);
    }

    {
      const tx = new MTX();
      tx.addCoin(doubleSpendCoin);
      tx.addOutput(wallet.getAddress(), 5000);

      await wallet.sign(tx);

      await wdb.addTX(tx.toTX());

      const balance = await wallet.getBalance();
      assert.strictEqual(balance.unconfirmed, 6000);
    }

    {
      const txs = await wallet.getHistory();
      assert.strictEqual(txs.length, 2);

      const total = txs.reduce((t, wtx) => {
        return t + wtx.tx.getOutputValue();
      }, 0);
      assert.strictEqual(total, 56000);
    }
  });

  it('should handle missed txs without resolution', async () => {
    const wdb = new WalletDB({
      name: 'wallet-test',
      db: 'memory',
      verify: false
    });

    await wdb.open();

    const alice = await wdb.create();
    const bob = await wdb.create();

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(alice.getAddress(), 50000);
    t1.addOutput(alice.getAddress(), 1000);

    // balance: 51000

    const t2 = new MTX();
    t2.addTX(t1, 0); // 50000
    t2.addOutput(alice.getAddress(), 24000);
    t2.addOutput(alice.getAddress(), 24000);

    // balance: 49000
    await alice.sign(t2);

    const t3 = new MTX();
    t3.addTX(t1, 1); // 1000
    t3.addTX(t2, 0); // 24000
    t3.addOutput(alice.getAddress(), 23000);

    // balance: 47000
    await alice.sign(t3);

    const t4 = new MTX();
    t4.addTX(t2, 1); // 24000
    t4.addTX(t3, 0); // 23000
    t4.addOutput(alice.getAddress(), 11000);
    t4.addOutput(alice.getAddress(), 11000);

    // balance: 22000
    await alice.sign(t4);

    const f1 = new MTX();
    f1.addTX(t4, 1); // 11000
    f1.addOutput(bob.getAddress(), 10000);

    // balance: 11000
    await alice.sign(f1);

    {
      await wdb.addTX(t4.toTX());
      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 22000);
    }

    {
      await wdb.addTX(t1.toTX());
      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 73000);
    }

    {
      await wdb.addTX(t2.toTX());
      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 47000);
    }

    {
      await wdb.addTX(t3.toTX());
      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 22000);
    }

    {
      await wdb.addTX(f1.toTX());

      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 11000);

      const txs = await alice.getHistory();
      assert(txs.some((wtx) => {
        return wtx.tx.hash('hex') === f1.hash('hex');
      }));
    }

    {
      const balance = await bob.getBalance();
      assert.strictEqual(balance.unconfirmed, 10000);

      const txs = await bob.getHistory();
      assert(txs.some((wtx) => {
        return wtx.tx.hash('hex') === f1.hash('hex');
      }));
    }

    await wdb.addTX(t2.toTX());
    await wdb.addTX(t3.toTX());
    await wdb.addTX(t4.toTX());
    await wdb.addTX(f1.toTX());

    {
      const balance = await alice.getBalance();
      assert.strictEqual(balance.unconfirmed, 11000);
    }

    {
      const balance = await bob.getBalance();
      assert.strictEqual(balance.unconfirmed, 10000);
    }
  });

  it('should fill tx with inputs', async () => {
    const alice = await wdb.create();
    const bob = await wdb.create();

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);

    await wdb.addTX(t1.toTX());

    // Create new transaction
    const m2 = new MTX();
    m2.addOutput(bob.getAddress(), 5460);

    await alice.fund(m2, {
      rate: 10000,
      round: true
    });

    await alice.sign(m2);

    const [t2, v2] = m2.commit();

    assert(t2.verify(v2));

    assert.strictEqual(t2.getInputValue(v2), 16380);
    assert.strictEqual(t2.getOutputValue(), 6380);
    assert.strictEqual(t2.getFee(v2), 10000);

    // Create new transaction
    const t3 = new MTX();
    t3.addOutput(bob.getAddress(), 15000);

    let err;
    try {
      await alice.fund(t3, {
        rate: 10000,
        round: true
      });
    } catch (e) {
      err = e;
    }

    assert(err);
    assert.strictEqual(err.requiredFunds, 25000);
  });

  it('should fill tx with inputs with accurate fee', async () => {
    const alice = await wdb.create({
      master: KEY1
    });

    const bob = await wdb.create({
      master: KEY2
    });

    // Coinbase
    const t1 = new MTX();
    t1.addOutpoint(new Outpoint(encoding.NULL_HASH, 0));
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);

    await wdb.addTX(t1.toTX());

    // Create new transaction
    const m2 = new MTX();
    m2.addOutput(bob.getAddress(), 5460);

    await alice.fund(m2, {
      rate: 10000
    });

    await alice.sign(m2);

    const [t2, v2] = m2.commit();

    assert(t2.verify(v2));

    assert.strictEqual(t2.getInputValue(v2), 16380);

    // Should now have a change output:
    assert.strictEqual(t2.getOutputValue(), 11130);

    assert.strictEqual(t2.getFee(v2), 5250);

    assert.strictEqual(t2.getWeight(), 2084);
    assert.strictEqual(t2.getBaseSize(), 521);
    assert.strictEqual(t2.getSize(), 521);
    assert.strictEqual(t2.getVirtualSize(), 521);

    let balance;
    bob.once('balance', (b) => {
      balance = b;
    });

    await wdb.addTX(t2);

    // Create new transaction
    const t3 = new MTX();
    t3.addOutput(bob.getAddress(), 15000);

    let err;
    try {
      await alice.fund(t3, {
        rate: 10000
      });
    } catch (e) {
      err = e;
    }

    assert(err);
    assert(balance);
    assert.strictEqual(balance.unconfirmed, 5460);
  });

  it('should sign multiple inputs using different keys', async () => {
    const alice = await wdb.create();
    const bob = await wdb.create();
    const carol = await wdb.create();

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);

    // Coinbase
    const t2 = new MTX();
    t2.addInput(dummyInput());
    t2.addOutput(bob.getAddress(), 5460);
    t2.addOutput(bob.getAddress(), 5460);
    t2.addOutput(bob.getAddress(), 5460);
    t2.addOutput(bob.getAddress(), 5460);

    await wdb.addTX(t1.toTX());
    await wdb.addTX(t2.toTX());

    // Create our tx with an output
    const tx = new MTX();
    tx.addOutput(carol.getAddress(), 5460);

    const coins1 = await alice.getCoins();
    const coins2 = await bob.getCoins();

    // Add our unspent inputs to sign
    tx.addCoin(coins1[0]);
    tx.addCoin(coins1[1]);
    tx.addCoin(coins2[0]);

    // Sign transaction
    assert.strictEqual(await alice.sign(tx), 2);
    assert.strictEqual(await bob.sign(tx), 1);

    // Verify
    assert.strictEqual(tx.verify(), true);

    tx.inputs.length = 0;
    tx.addCoin(coins1[1]);
    tx.addCoin(coins1[2]);
    tx.addCoin(coins2[1]);

    assert.strictEqual(await alice.sign(tx), 2);
    assert.strictEqual(await bob.sign(tx), 1);

    // Verify
    assert.strictEqual(tx.verify(), true);
  });

  it('should verify 2-of-3 p2sh tx', async () => {
    await testP2SH(false, false);
  });

  it('should verify 2-of-3 p2wsh tx', async () => {
    await testP2SH(true, false);
  });

  it('should verify 2-of-3 p2wsh tx w/ nested bullshit', async () => {
    await testP2SH(true, true);
  });

  it('should fill tx with account 1', async () => {
    const alice = await wdb.create();
    const bob = await wdb.create();

    {
      const account = await alice.createAccount({
        name: 'foo'
      });
      assert.strictEqual(account.name, 'foo');
      assert.strictEqual(account.accountIndex, 1);
    }

    const account = await alice.getAccount('foo');
    assert.strictEqual(account.name, 'foo');
    assert.strictEqual(account.accountIndex, 1);

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(account.receive.getAddress(), 5460);
    t1.addOutput(account.receive.getAddress(), 5460);
    t1.addOutput(account.receive.getAddress(), 5460);
    t1.addOutput(account.receive.getAddress(), 5460);

    await wdb.addTX(t1.toTX());

    // Create new transaction
    const t2 = new MTX();
    t2.addOutput(bob.getAddress(), 5460);

    await alice.fund(t2, {
      rate: 10000,
      round: true
    });

    await alice.sign(t2);

    assert(t2.verify());

    assert.strictEqual(t2.getInputValue(), 16380);
    assert.strictEqual(t2.getOutputValue(), 6380);
    assert.strictEqual(t2.getFee(), 10000);

    // Create new transaction
    const t3 = new MTX();
    t3.addOutput(bob.getAddress(), 15000);

    let err;
    try {
      await alice.fund(t3, {
        rate: 10000,
        round: true
      });
    } catch (e) {
      err = e;
    }

    assert(err);
    assert.strictEqual(err.requiredFunds, 25000);

    const accounts = await alice.getAccounts();
    assert.deepStrictEqual(accounts, ['default', 'foo']);
  });

  it('should fail to fill tx with account 1', async () => {
    const wallet = await wdb.create();

    {
      const account = await wallet.createAccount({
        name: 'foo'
      });
      assert.strictEqual(account.name, 'foo');
      assert.strictEqual(account.accountIndex, 1);
    }

    const account = await wallet.getAccount('foo');
    assert.strictEqual(account.name, 'foo');
    assert.strictEqual(account.accountIndex, 1);
    assert.strictEqual(wallet.account.accountIndex, 0);

    assert(!account.receive.getAddress().equals(
      wallet.account.receive.getAddress()));

    assert(wallet.getAddress().equals(wallet.account.receive.getAddress()));

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(wallet.getAddress(), 5460);
    t1.addOutput(wallet.getAddress(), 5460);
    t1.addOutput(wallet.getAddress(), 5460);
    t1.addOutput(account.receive.getAddress(), 5460);

    await wdb.addTX(t1.toTX());

    // Should fill from `foo` and fail
    const t2 = new MTX();

    t2.addOutput(wallet.getAddress(), 5460);

    let err;
    try {
      await wallet.fund(t2, {
        rate: 10000,
        round: true,
        account: 'foo'
      });
    } catch (e) {
      err = e;
    }

    assert(err);

    // Should fill from whole wallet and succeed
    const t3 = new MTX();
    t3.addOutput(wallet.getAddress(), 5460);

    await wallet.fund(t3, {
      rate: 10000,
      round: true
    });

    // Coinbase
    const t4 = new MTX();
    t4.addInput(dummyInput());
    t4.addOutput(account.receive.getAddress(), 5460);
    t4.addOutput(account.receive.getAddress(), 5460);
    t4.addOutput(account.receive.getAddress(), 5460);

    await wdb.addTX(t4.toTX());

    // Should fill from `foo` and succeed
    const t5 = new MTX();
    t5.addOutput(wallet.getAddress(), 5460);

    await wallet.fund(t5, {
      rate: 10000,
      round: true,
      account: 'foo'
    });

    currentWallet = wallet;
  });

  it('should create two accounts (multiple encryption)', async () => {
    {
      const wallet = await wdb.create({
        id: 'foobar',
        passphrase: 'foo'
      });
      await wallet.destroy();
    }

    const wallet = await wdb.get('foobar');
    assert(wallet);

    const options = {
      name: 'foo1'
    };

    const account = await wallet.createAccount(options, 'foo');

    assert(account);

    await wallet.lock();
  });

  it('should fill tx with inputs when encrypted', async () => {
    const wallet = await wdb.create({
      passphrase: 'foo'
    });

    wallet.master.stop();
    wallet.master.key = null;

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(wallet.getAddress(), 5460);
    t1.addOutput(wallet.getAddress(), 5460);
    t1.addOutput(wallet.getAddress(), 5460);
    t1.addOutput(wallet.getAddress(), 5460);

    await wdb.addTX(t1.toTX());

    // Create new transaction
    const t2 = new MTX();
    t2.addOutput(wallet.getAddress(), 5460);

    await wallet.fund(t2, {
      rate: 10000,
      round: true
    });

    // Should fail
    let err;
    try {
      await wallet.sign(t2, 'bar');
    } catch (e) {
      err = e;
    }

    assert(err);
    assert(!t2.verify());

    // Should succeed
    await wallet.sign(t2, 'foo');
    assert(t2.verify());
  });

  it('should fill tx with inputs with subtract fee (1)', async () => {
    const alice = await wdb.create();
    const bob = await wdb.create();

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);

    await wdb.addTX(t1.toTX());

    // Create new transaction
    const t2 = new MTX();
    t2.addOutput(bob.getAddress(), 21840);

    await alice.fund(t2, {
      rate: 10000,
      round: true,
      subtractFee: true
    });

    await alice.sign(t2);

    assert(t2.verify());

    assert.strictEqual(t2.getInputValue(), 5460 * 4);
    assert.strictEqual(t2.getOutputValue(), 21840 - 10000);
    assert.strictEqual(t2.getFee(), 10000);
  });

  it('should fill tx with inputs with subtract fee (2)', async () => {
    const alice = await wdb.create();
    const bob = await wdb.create();

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);

    await wdb.addTX(t1.toTX());

    const options = {
      subtractFee: true,
      rate: 10000,
      round: true,
      outputs: [{ address: bob.getAddress(), value: 21840 }]
    };

    // Create new transaction
    const t2 = await alice.createTX(options);
    await alice.sign(t2);

    assert(t2.verify());

    assert.strictEqual(t2.getInputValue(), 5460 * 4);
    assert.strictEqual(t2.getOutputValue(), 21840 - 10000);
    assert.strictEqual(t2.getFee(), 10000);
  });

  it('should fill tx with smart coin selection', async () => {
    const alice = await wdb.create();
    const bob = await wdb.create();

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);
    t1.addOutput(alice.getAddress(), 5460);

    await wdb.addTX(t1.toTX());

    // Coinbase
    const t2 = new MTX();
    t2.addInput(dummyInput());
    t2.addOutput(alice.getAddress(), 5460);
    t2.addOutput(alice.getAddress(), 5460);
    t2.addOutput(alice.getAddress(), 5460);
    t2.addOutput(alice.getAddress(), 5460);

    const block = nextBlock();

    await wdb.addBlock(block, [t2.toTX()]);

    {
      const coins = await alice.getSmartCoins();
      assert.strictEqual(coins.length, 4);

      for (let i = 0; i < coins.length; i++) {
        const coin = coins[i];
        assert.strictEqual(coin.height, block.height);
      }
    }

    // Create a change output for ourselves.
    await alice.send({
      subtractFee: true,
      rate: 1000,
      depth: 1,
      outputs: [{ address: bob.getAddress(), value: 1461 }]
    });

    const coins = await alice.getSmartCoins();
    assert.strictEqual(coins.length, 4);

    let total = 0;

    {
      let found = false;

      for (let i = 0; i < coins.length; i++) {
        const coin = coins[i];
        if (coin.height === -1) {
          assert(!found);
          assert(coin.value < 5460);
          found = true;
        } else {
          assert.strictEqual(coin.height, block.height);
        }
        total += coin.value;
      }

      assert(found);
    }

    // Use smart selection
    const options = {
      subtractFee: true,
      smart: true,
      rate: 10000,
      outputs: [{
        address: bob.getAddress(),
        value: total
      }]
    };

    const t3 = await alice.createTX(options);
    assert.strictEqual(t3.inputs.length, 4);

    {
      let found = false;

      for (let i = 0; i < t3.inputs.length; i++) {
        const coin = t3.view.getCoinFor(t3.inputs[i]);
        if (coin.height === -1) {
          assert(!found);
          assert(coin.value < 5460);
          found = true;
        } else {
          assert.strictEqual(coin.height, block.height);
        }
      }

      assert(found);
    }

    await alice.sign(t3);

    assert(t3.verify());
  });

  it('should get range of txs', async () => {
    const wallet = currentWallet;
    const txs = await wallet.getRange({
      start: util.now() - 1000
    });
    assert.strictEqual(txs.length, 2);
  });

  it('should get range of txs from account', async () => {
    const wallet = currentWallet;
    const txs = await wallet.getRange('foo', {
      start: util.now() - 1000
    });
    assert.strictEqual(txs.length, 2);
  });

  it('should not get range of txs from non-existent account', async () => {
    const wallet = currentWallet;

    let txs, err;
    try {
      txs = await wallet.getRange('bad', {
        start: 0xdeadbeef - 1000
      });
    } catch (e) {
      err = e;
    }

    assert(!txs);
    assert(err);
    assert.strictEqual(err.message, 'Account not found.');
  });

  it('should get account balance', async () => {
    const wallet = currentWallet;
    const balance = await wallet.getBalance('foo');
    assert.strictEqual(balance.unconfirmed, 21840);
  });

  it('should import privkey', async () => {
    const key = KeyRing.generate();

    const wallet = await wdb.create({
      passphrase: 'test'
    });

    await wallet.importKey('default', key, 'test');

    const wkey = await wallet.getKey(key.getHash('hex'));

    assert.strictEqual(wkey.getHash('hex'), key.getHash('hex'));

    // Coinbase
    const t1 = new MTX();
    t1.addOutput(key.getAddress(), 5460);
    t1.addOutput(key.getAddress(), 5460);
    t1.addOutput(key.getAddress(), 5460);
    t1.addOutput(key.getAddress(), 5460);

    t1.addInput(dummyInput());

    await wdb.addTX(t1.toTX());

    const wtx = await wallet.getTX(t1.hash('hex'));
    assert(wtx);
    assert.strictEqual(t1.hash('hex'), wtx.hash);

    const options = {
      rate: 10000,
      round: true,
      outputs: [{
        address: wallet.getAddress(),
        value: 7000
      }]
    };

    // Create new transaction
    const t2 = await wallet.createTX(options);
    await wallet.sign(t2);
    assert(t2.verify());
    assert.strictEqual(t2.inputs[0].prevout.hash, wtx.hash);

    importedWallet = wallet;
    importedKey = key;
  });

  it('should import pubkey', async () => {
    const key = KeyRing.generate();
    const pub = new KeyRing(key.publicKey);

    const wallet = await wdb.create({
      watchOnly: true
    });

    await wallet.importKey('default', pub);

    const path = await wallet.getPath(pub.getHash('hex'));
    assert.strictEqual(path.hash, pub.getHash('hex'));

    const wkey = await wallet.getKey(pub.getHash('hex'));
    assert(wkey);
  });

  it('should import address', async () => {
    const key = KeyRing.generate();

    const wallet = await wdb.create({
      watchOnly: true
    });

    await wallet.importAddress('default', key.getAddress());

    const path = await wallet.getPath(key.getHash('hex'));
    assert(path);
    assert.strictEqual(path.hash, key.getHash('hex'));

    const wkey = await wallet.getKey(key.getHash('hex'));
    assert(!wkey);
  });

  it('should get details', async () => {
    const wallet = currentWallet;

    const txs = await wallet.getRange('foo', {
      start: util.now() - 1000
    });

    const details = await wallet.toDetails(txs);

    assert(details.some((tx) => {
      return tx.toJSON().outputs[0].path.name === 'foo';
    }));
  });

  it('should rename wallet', async () => {
    const wallet = currentWallet;

    await wallet.rename('test');

    const txs = await wallet.getRange('foo', {
      start: util.now() - 1000
    });

    const details = await wallet.toDetails(txs);

    assert.strictEqual(details[0].toJSON().id, 'test');
  });

  it('should change passphrase with encrypted imports', async () => {
    const wallet = importedWallet;
    const addr = importedKey.getAddress();

    assert(wallet.master.encrypted);

    let data;
    {
      const path = await wallet.getPath(addr);
      assert(path);
      assert(path.data && path.encrypted);
      data = path.data;
    }

    await wallet.decrypt('test');

    {
      const path = await wallet.getPath(addr);
      assert(path);
      assert(path.data && !path.encrypted);
      assert(await wallet.getKey(addr));
    }

    await wallet.encrypt('foo');

    {
      const path = await wallet.getPath(addr);
      assert(path);
      assert(path.data && path.encrypted);
      assert(!data.equals(path.data));
      assert(!await wallet.getKey(addr));
    }

    await wallet.unlock('foo');

    const key = await wallet.getKey(addr);
    assert(key);
    assert.strictEqual(key.getHash('hex'), addr.getHash('hex'));
  });

  it('should recover from a missed tx', async () => {
    const wdb = new WalletDB({
      name: 'wallet-test',
      db: 'memory',
      verify: false
    });

    await wdb.open();

    const alice = await wdb.create({
      master: KEY1
    });

    const bob = await wdb.create({
      master: KEY1
    });

    const addr = alice.getAddress();

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(addr, 50000);

    await alice.add(t1.toTX());
    await bob.add(t1.toTX());

    // Bob misses this tx!
    const t2 = new MTX();
    t2.addTX(t1, 0);
    t2.addOutput(addr, 24000);
    t2.addOutput(addr, 24000);

    await alice.sign(t2);

    await alice.add(t2.toTX());

    assert.notStrictEqual(
      (await alice.getBalance()).unconfirmed,
      (await bob.getBalance()).unconfirmed);

    // Bob sees this one.
    const t3 = new MTX();
    t3.addTX(t2, 0);
    t3.addTX(t2, 1);
    t3.addOutput(addr, 30000);

    await alice.sign(t3);

    assert.strictEqual((await bob.getBalance()).unconfirmed, 50000);

    await alice.add(t3.toTX());
    await bob.add(t3.toTX());

    assert.strictEqual((await alice.getBalance()).unconfirmed, 30000);

    // Bob sees t2 on the chain.
    await bob.add(t2.toTX());

    // Bob sees t3 on the chain.
    await bob.add(t3.toTX());

    assert.strictEqual((await bob.getBalance()).unconfirmed, 30000);
  });

  it('should recover from a missed tx and double spend', async () => {
    const wdb = new WalletDB({
      name: 'wallet-test',
      db: 'memory',
      verify: false
    });

    await wdb.open();

    const alice = await wdb.create({
      master: KEY1
    });

    const bob = await wdb.create({
      master: KEY1
    });

    const addr = alice.getAddress();

    // Coinbase
    const t1 = new MTX();
    t1.addInput(dummyInput());
    t1.addOutput(addr, 50000);

    await alice.add(t1.toTX());
    await bob.add(t1.toTX());

    // Bob misses this tx!
    const t2a = new MTX();
    t2a.addTX(t1, 0);
    t2a.addOutput(addr, 24000);
    t2a.addOutput(addr, 24000);

    await alice.sign(t2a);

    await alice.add(t2a.toTX());

    assert.notStrictEqual(
      (await alice.getBalance()).unconfirmed,
      (await bob.getBalance()).unconfirmed);

    // Bob doublespends.
    const t2b = new MTX();
    t2b.addTX(t1, 0);
    t2b.addOutput(addr, 10000);
    t2b.addOutput(addr, 10000);

    await bob.sign(t2b);

    await bob.add(t2b.toTX());

    // Bob sees this one.
    const t3 = new MTX();
    t3.addTX(t2a, 0);
    t3.addTX(t2a, 1);
    t3.addOutput(addr, 30000);

    await alice.sign(t3);

    assert.strictEqual((await bob.getBalance()).unconfirmed, 20000);

    await alice.add(t3.toTX());
    await bob.add(t3.toTX());

    assert.strictEqual((await alice.getBalance()).unconfirmed, 30000);

    // Bob sees t2a on the chain.
    await bob.add(t2a.toTX());

    // Bob sees t3 on the chain.
    await bob.add(t3.toTX());

    assert.strictEqual((await bob.getBalance()).unconfirmed, 30000);
  });

  it('should cleanup', () => {
    consensus.COINBASE_MATURITY = 100;
  });
});
