Source: wallet/rpc.js

/*!
 * rpc.js - bitcoind-compatible json rpc for bcoin.
 * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

const assert = require('bsert');
const {format} = require('util');
const bweb = require('bweb');
const {Lock} = require('bmutex');
const fs = require('bfile');
const Validator = require('bval');
const {BufferMap, BufferSet} = require('buffer-map');
const util = require('../utils/util');
const messageUtil = require('../utils/message');
const Amount = require('../btc/amount');
const Script = require('../script/script');
const Address = require('../primitives/address');
const KeyRing = require('../primitives/keyring');
const MerkleBlock = require('../primitives/merkleblock');
const MTX = require('../primitives/mtx');
const Outpoint = require('../primitives/outpoint');
const Output = require('../primitives/output');
const TX = require('../primitives/tx');
const consensus = require('../protocol/consensus');
const pkg = require('../pkg');
const common = require('./common');
const {BlockMeta} = require('./records');
const RPCBase = bweb.RPC;
const RPCError = bweb.RPCError;

/*
 * Constants
 */

const errs = {
  // Standard JSON-RPC 2.0 errors
  INVALID_REQUEST: bweb.errors.INVALID_REQUEST,
  METHOD_NOT_FOUND: bweb.errors.METHOD_NOT_FOUND,
  INVALID_PARAMS: bweb.errors.INVALID_PARAMS,
  INTERNAL_ERROR: bweb.errors.INTERNAL_ERROR,
  PARSE_ERROR: bweb.errors.PARSE_ERROR,

  // General application defined errors
  MISC_ERROR: -1,
  FORBIDDEN_BY_SAFE_MODE: -2,
  TYPE_ERROR: -3,
  INVALID_ADDRESS_OR_KEY: -5,
  OUT_OF_MEMORY: -7,
  INVALID_PARAMETER: -8,
  DATABASE_ERROR: -20,
  DESERIALIZATION_ERROR: -22,
  VERIFY_ERROR: -25,
  VERIFY_REJECTED: -26,
  VERIFY_ALREADY_IN_CHAIN: -27,
  IN_WARMUP: -28,

  // Wallet errors
  WALLET_ERROR: -4,
  WALLET_INSUFFICIENT_FUNDS: -6,
  WALLET_INVALID_ACCOUNT_NAME: -11,
  WALLET_KEYPOOL_RAN_OUT: -12,
  WALLET_UNLOCK_NEEDED: -13,
  WALLET_PASSPHRASE_INCORRECT: -14,
  WALLET_WRONG_ENC_STATE: -15,
  WALLET_ENCRYPTION_FAILED: -16,
  WALLET_ALREADY_UNLOCKED: -17
};

/**
 * Wallet RPC
 * @alias module:wallet.RPC
 * @extends bweb.RPC
 */

class RPC extends RPCBase {
  /**
   * Create an RPC.
   * @param {WalletDB} wdb
   */

  constructor(node) {
    super();

    assert(node, 'RPC requires a WalletDB.');

    this.wdb = node.wdb;
    this.network = node.network;
    this.logger = node.logger.context('wallet-rpc');
    this.client = node.client;
    this.locker = new Lock();

    this.wallet = null;

    this.init();
  }

  getCode(err) {
    switch (err.type) {
      case 'RPCError':
        return err.code;
      case 'ValidationError':
        return errs.TYPE_ERROR;
      case 'EncodingError':
        return errs.DESERIALIZATION_ERROR;
      case 'FundingError':
        return errs.WALLET_INSUFFICIENT_FUNDS;
      default:
        return errs.INTERNAL_ERROR;
    }
  }

  handleCall(cmd, query) {
    this.logger.debug('Handling RPC call: %s.', cmd.method);
  }

  init() {
    this.add('help', this.help);
    this.add('stop', this.stop);
    this.add('fundrawtransaction', this.fundRawTransaction);
    this.add('resendwallettransactions', this.resendWalletTransactions);
    this.add('abandontransaction', this.abandonTransaction);
    this.add('addmultisigaddress', this.addMultisigAddress);
    this.add('addwitnessaddress', this.addWitnessAddress);
    this.add('backupwallet', this.backupWallet);
    this.add('dumpprivkey', this.dumpPrivKey);
    this.add('dumpwallet', this.dumpWallet);
    this.add('encryptwallet', this.encryptWallet);
    this.add('getaddressinfo', this.getAddressInfo);
    this.add('getaccountaddress', this.getAccountAddress);
    this.add('getaccount', this.getAccount);
    this.add('getaddressesbyaccount', this.getAddressesByAccount);
    this.add('getbalance', this.getBalance);
    this.add('getnewaddress', this.getNewAddress);
    this.add('getrawchangeaddress', this.getRawChangeAddress);
    this.add('getreceivedbyaccount', this.getReceivedByAccount);
    this.add('getreceivedbyaddress', this.getReceivedByAddress);
    this.add('gettransaction', this.getTransaction);
    this.add('getunconfirmedbalance', this.getUnconfirmedBalance);
    this.add('getwalletinfo', this.getWalletInfo);
    this.add('importprivkey', this.importPrivKey);
    this.add('importwallet', this.importWallet);
    this.add('importaddress', this.importAddress);
    this.add('importprunedfunds', this.importPrunedFunds);
    this.add('importpubkey', this.importPubkey);
    this.add('keypoolrefill', this.keyPoolRefill);
    this.add('listaccounts', this.listAccounts);
    this.add('listaddressgroupings', this.listAddressGroupings);
    this.add('listlockunspent', this.listLockUnspent);
    this.add('listreceivedbyaccount', this.listReceivedByAccount);
    this.add('listreceivedbyaddress', this.listReceivedByAddress);
    this.add('listsinceblock', this.listSinceBlock);
    this.add('listtransactions', this.listTransactions);
    this.add('listunspent', this.listUnspent);
    this.add('lockunspent', this.lockUnspent);
    this.add('move', this.move);
    this.add('sendfrom', this.sendFrom);
    this.add('sendmany', this.sendMany);
    this.add('sendtoaddress', this.sendToAddress);
    this.add('setaccount', this.setAccount);
    this.add('settxfee', this.setTXFee);
    this.add('signmessage', this.signMessage);
    this.add('walletlock', this.walletLock);
    this.add('walletpassphrasechange', this.walletPassphraseChange);
    this.add('walletpassphrase', this.walletPassphrase);
    this.add('removeprunedfunds', this.removePrunedFunds);
    this.add('selectwallet', this.selectWallet);
    this.add('getmemoryinfo', this.getMemoryInfo);
    this.add('setloglevel', this.setLogLevel);
  }

  async help(args, _help) {
    if (args.length === 0)
      return `Select a command:\n${Object.keys(this.calls).join('\n')}`;

    const json = {
      method: args[0],
      params: []
    };

    return await this.execute(json, true);
  }

  async stop(args, help) {
    if (help || args.length !== 0)
      throw new RPCError(errs.MISC_ERROR, 'stop');

    this.wdb.close();

    return 'Stopping.';
  }

  async fundRawTransaction(args, help) {
    if (help || args.length < 1 || args.length > 2) {
      throw new RPCError(errs.MISC_ERROR,
        'fundrawtransaction "hexstring" ( options )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    const data = valid.buf(0);
    const options = valid.obj(1);

    if (!data)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.');

    const tx = MTX.fromRaw(data);

    if (tx.outputs.length === 0) {
      throw new RPCError(errs.INVALID_PARAMETER,
        'TX must have at least one output.');
    }

    let rate = null;
    let change = null;

    if (options) {
      const valid = new Validator(options);

      rate = valid.ufixed('feeRate', 8);
      change = valid.str('changeAddress');

      if (change)
        change = parseAddress(change, this.network);
    }

    await wallet.fund(tx, {
      rate: rate,
      changeAddress: change
    });

    return {
      hex: tx.toRaw().toString('hex'),
      changepos: tx.changeIndex,
      fee: Amount.btc(tx.getFee(), true)
    };
  }

  /*
   * Wallet
   */

  async resendWalletTransactions(args, help) {
    if (help || args.length !== 0)
      throw new RPCError(errs.MISC_ERROR, 'resendwallettransactions');

    const wallet = this.wallet;
    const txs = await wallet.resend();
    const hashes = [];

    for (const tx of txs)
      hashes.push(tx.txid());

    return hashes;
  }

  async addMultisigAddress(args, help) {
    // Impossible to implement in bcoin (no address book).
    throw new Error('Not implemented.');
  }

  async addWitnessAddress(args, help) {
    // Unlikely to be implemented.
    throw new Error('Not implemented.');
  }

  async backupWallet(args, help) {
    const valid = new Validator(args);
    const dest = valid.str(0);

    if (help || args.length !== 1 || !dest)
      throw new RPCError(errs.MISC_ERROR, 'backupwallet "destination"');

    await this.wdb.backup(dest);

    return null;
  }

  async dumpPrivKey(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'dumpprivkey "bitcoinaddress"');

    const wallet = this.wallet;
    const valid = new Validator(args);
    const addr = valid.str(0, '');

    const hash = parseHash(addr, this.network);
    const ring = await wallet.getPrivateKey(hash);

    if (!ring)
      throw new RPCError(errs.MISC_ERROR, 'Key not found.');

    return ring.toSecret(this.network);
  }

  async dumpWallet(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'dumpwallet "filename"');

    const wallet = this.wallet;
    const valid = new Validator(args);
    const file = valid.str(0);

    if (!file)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');

    const tip = await this.wdb.getTip();
    const time = util.date();

    const out = [
      format('# Wallet Dump created by Bcoin %s', pkg.version),
      format('# * Created on %s', time),
      format('# * Best block at time of backup was %d (%s).',
        tip.height, util.revHex(tip.hash)),
      format('# * File: %s', file),
      ''
    ];

    const hashes = await wallet.getAddressHashes();

    for (const hash of hashes) {
      const ring = await wallet.getPrivateKey(hash);

      if (!ring)
        continue;

      const addr = ring.getAddress('string', this.network);

      let fmt = '%s %s label= addr=%s';

      if (ring.branch === 1)
        fmt = '%s %s change=1 addr=%s';

      const str = format(fmt, ring.toSecret(this.network), time, addr);

      out.push(str);
    }

    out.push('');
    out.push('# End of dump');
    out.push('');

    const dump = out.join('\n');

    if (fs.unsupported)
      return dump;

    await fs.writeFile(file, dump, 'utf8');

    return null;
  }

  async encryptWallet(args, help) {
    const wallet = this.wallet;

    if (!wallet.master.encrypted && (help || args.length !== 1))
      throw new RPCError(errs.MISC_ERROR, 'encryptwallet "passphrase"');

    const valid = new Validator(args);
    const passphrase = valid.str(0, '');

    if (wallet.master.encrypted) {
      throw new RPCError(errs.WALLET_WRONG_ENC_STATE,
        'Already running with an encrypted wallet.');
    }

    if (passphrase.length < 1)
      throw new RPCError(errs.MISC_ERROR, 'encryptwallet "passphrase"');

    try {
      await wallet.encrypt(passphrase);
    } catch (e) {
      throw new RPCError(errs.WALLET_ENCRYPTION_FAILED, 'Encryption failed.');
    }

    return 'wallet encrypted; we do not need to stop!';
  }

  async getAccountAddress(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'getaccountaddress "account"');

    const wallet = this.wallet;
    const valid = new Validator(args);
    let name = valid.str(0, '');

    if (!name)
      name = 'default';

    const addr = await wallet.receiveAddress(name);

    if (!addr)
      return '';

    return addr.toString(this.network);
  }

  async getAccount(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'getaccount "bitcoinaddress"');

    const wallet = this.wallet;
    const valid = new Validator(args);
    const addr = valid.str(0, '');

    const hash = parseHash(addr, this.network);
    const path = await wallet.getPath(hash);

    if (!path)
      return '';

    return path.name;
  }

  async getAddressesByAccount(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'getaddressesbyaccount "account"');

    const wallet = this.wallet;
    const valid = new Validator(args);
    let name = valid.str(0, '');
    const addrs = [];

    if (name === '')
      name = 'default';

    let paths;
    try {
      paths = await wallet.getPaths(name);
    } catch (e) {
      if (e.message === 'Account not found.')
        return [];
      throw e;
    }

    for (const path of paths) {
      const addr = path.toAddress();
      addrs.push(addr.toString(this.network));
    }

    return addrs;
  }

  async getAddressInfo(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'getaddressinfo "address"');

    const valid = new Validator(args);
    const addr = valid.str(0, '');

    const address = parseAddress(addr, this.network);
    const script = Script.fromAddress(address);
    const wallet = this.wallet.toJSON();

    const path = await this.wallet.getPath(address);

    const isScript = script.isScripthash() || script.isWitnessScripthash();
    const isWitness = address.isProgram();

    const result = {
      address: address.toString(this.network),
      scriptPubKey: script ? script.toJSON() : undefined,
      ismine: path != null,
      ischange: path ? path.branch === 1 : false,
      iswatchonly: wallet.watchOnly,
      isscript: isScript,
      iswitness: isWitness
    };

    if (isWitness) {
      result.witness_version = address.version;
      result.witness_program = address.hash.toString('hex');
    }

    return result;
  }

  async getBalance(args, help) {
    if (help || args.length > 3) {
      throw new RPCError(errs.MISC_ERROR,
        'getbalance ( "account" minconf includeWatchonly )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    let name = valid.str(0);
    const minconf = valid.u32(1, 1);
    const watchOnly = valid.bool(2, false);

    if (name === '')
      name = 'default';

    if (name === '*')
      name = null;

    if (wallet.watchOnly !== watchOnly)
      return 0;

    const balance = await wallet.getBalance(name);

    let value;
    if (minconf > 0)
      value = balance.confirmed;
    else
      value = balance.unconfirmed;

    return Amount.btc(value, true);
  }

  async getNewAddress(args, help) {
    if (help || args.length > 1)
      throw new RPCError(errs.MISC_ERROR, 'getnewaddress ( "account" )');

    const wallet = this.wallet;
    const valid = new Validator(args);
    let name = valid.str(0);

    if (name === '' || args.length === 0)
      name = 'default';

    const addr = await wallet.createReceive(name);

    return addr.getAddress('string', this.network);
  }

  async getRawChangeAddress(args, help) {
    if (help || args.length !== 0)
      throw new RPCError(errs.MISC_ERROR, 'getrawchangeaddress');

    const wallet = this.wallet;
    const addr = await wallet.createChange();

    return addr.getAddress('string', this.network);
  }

  async getReceivedByAccount(args, help) {
    if (help || args.length < 1 || args.length > 2) {
      throw new RPCError(errs.MISC_ERROR,
        'getreceivedbyaccount "account" ( minconf )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    let name = valid.str(0);
    const minconf = valid.u32(1, 0);
    const height = this.wdb.state.height;

    if (name === '')
      name = 'default';

    const paths = await wallet.getPaths(name);
    const filter = new BufferSet();

    for (const path of paths)
      filter.add(path.hash);

    const txs = await wallet.getHistory(name);

    let total = 0;
    let lastConf = -1;

    for (const wtx of txs) {
      const conf = wtx.getDepth(height);

      if (conf < minconf)
        continue;

      if (lastConf === -1 || conf < lastConf)
        lastConf = conf;

      for (const output of wtx.tx.outputs) {
        const hash = output.getHash();
        if (hash && filter.has(hash))
          total += output.value;
      }
    }

    return Amount.btc(total, true);
  }

  async getReceivedByAddress(args, help) {
    if (help || args.length < 1 || args.length > 2) {
      throw new RPCError(errs.MISC_ERROR,
        'getreceivedbyaddress "bitcoinaddress" ( minconf )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    const addr = valid.str(0, '');
    const minconf = valid.u32(1, 0);
    const height = this.wdb.state.height;

    const hash = parseHash(addr, this.network);
    const txs = await wallet.getHistory();

    let total = 0;

    for (const wtx of txs) {
      if (wtx.getDepth(height) < minconf)
        continue;

      for (const output of wtx.tx.outputs) {
        if (output.getHash().equals(hash))
          total += output.value;
      }
    }

    return Amount.btc(total, true);
  }

  async _toWalletTX(wtx) {
    const wallet = this.wallet;
    const details = await wallet.toDetails(wtx);

    if (!details)
      throw new RPCError(errs.WALLET_ERROR, 'TX not found.');

    let receive = true;
    for (const member of details.inputs) {
      if (member.path) {
        receive = false;
        break;
      }
    }

    const det = [];
    let sent = 0;
    let received = 0;

    for (let i = 0; i < details.outputs.length; i++) {
      const member = details.outputs[i];

      if (member.path) {
        if (member.path.branch === 1)
          continue;

        det.push({
          account: member.path.name,
          address: member.address.toString(this.network),
          category: 'receive',
          amount: Amount.btc(member.value, true),
          label: member.path.name,
          vout: i
        });

        received += member.value;

        continue;
      }

      if (receive)
        continue;

      det.push({
        account: '',
        address: member.address
          ? member.address.toString(this.network)
          : null,
        category: 'send',
        amount: -(Amount.btc(member.value, true)),
        fee: -(Amount.btc(details.fee, true)),
        vout: i
      });

      sent += member.value;
    }

    return {
      amount: Amount.btc(receive ? received : -sent, true),
      confirmations: details.confirmations,
      blockhash: details.block ? util.revHex(details.block) : null,
      blockindex: details.index,
      blocktime: details.time,
      txid: util.revHex(details.hash),
      walletconflicts: [],
      time: details.mtime,
      timereceived: details.mtime,
      'bip125-replaceable': 'no',
      details: det,
      hex: details.tx.toRaw().toString('hex')
    };
  }

  async getTransaction(args, help) {
    if (help || args.length < 1 || args.length > 2) {
      throw new RPCError(errs.MISC_ERROR,
        'gettransaction "txid" ( includeWatchonly )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    const hash = valid.brhash(0);
    const watchOnly = valid.bool(1, false);

    if (!hash)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter');

    const wtx = await wallet.getTX(hash);

    if (!wtx)
      throw new RPCError(errs.WALLET_ERROR, 'TX not found.');

    return await this._toWalletTX(wtx, watchOnly);
  }

  async abandonTransaction(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'abandontransaction "txid"');

    const wallet = this.wallet;
    const valid = new Validator(args);
    const hash = valid.brhash(0);

    if (!hash)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');

    const result = await wallet.abandon(hash);

    if (!result)
      throw new RPCError(errs.WALLET_ERROR, 'Transaction not in wallet.');

    return null;
  }

  async getUnconfirmedBalance(args, help) {
    if (help || args.length > 0)
      throw new RPCError(errs.MISC_ERROR, 'getunconfirmedbalance');

    const wallet = this.wallet;
    const balance = await wallet.getBalance();

    return Amount.btc(balance.unconfirmed, true);
  }

  async getWalletInfo(args, help) {
    if (help || args.length !== 0)
      throw new RPCError(errs.MISC_ERROR, 'getwalletinfo');

    const wallet = this.wallet;
    const balance = await wallet.getBalance();

    return {
      walletid: wallet.id,
      walletversion: 6,
      balance: Amount.btc(balance.unconfirmed, true),
      unconfirmed_balance: Amount.btc(balance.unconfirmed, true),
      txcount: balance.tx,
      keypoololdest: 0,
      keypoolsize: 0,
      unlocked_until: wallet.master.until,
      paytxfee: Amount.btc(this.wdb.feeRate, true)
    };
  }

  async importPrivKey(args, help) {
    if (help || args.length < 1 || args.length > 3) {
      throw new RPCError(errs.MISC_ERROR,
        'importprivkey "bitcoinprivkey" ( "label" rescan )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    const secret = valid.str(0);
    const rescan = valid.bool(2, false);

    const key = parseSecret(secret, this.network);

    await wallet.importKey(0, key);

    if (rescan)
      await this.wdb.rescan(0);

    return null;
  }

  async importWallet(args, help) {
    if (help || args.length < 1 || args.length > 2)
      throw new RPCError(errs.MISC_ERROR, 'importwallet "filename" ( rescan )');

    const wallet = this.wallet;
    const valid = new Validator(args);
    const file = valid.str(0);
    const rescan = valid.bool(1, false);

    if (fs.unsupported)
      throw new RPCError(errs.INTERNAL_ERROR, 'FS not available.');

    let data;
    try {
      data = await fs.readFile(file, 'utf8');
    } catch (e) {
      throw new RPCError(errs.INTERNAL_ERROR, e.code || '');
    }

    const lines = data.split(/\n+/);
    const keys = [];

    for (let line of lines) {
      line = line.trim();

      if (line.length === 0)
        continue;

      if (/^\s*#/.test(line))
        continue;

      const parts = line.split(/\s+/);

      if (parts.length < 4)
        throw new RPCError(errs.DESERIALIZATION_ERROR, 'Malformed wallet.');

      const secret = parseSecret(parts[0], this.network);

      keys.push(secret);
    }

    for (const key of keys)
      await wallet.importKey(0, key);

    if (rescan)
      await this.wdb.rescan(0);

    return null;
  }

  async importAddress(args, help) {
    if (help || args.length < 1 || args.length > 4) {
      throw new RPCError(errs.MISC_ERROR,
        'importaddress "address" ( "label" rescan p2sh )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    let addr = valid.str(0, '');
    const rescan = valid.bool(2, false);
    const p2sh = valid.bool(3, false);

    if (p2sh) {
      let script = valid.buf(0);

      if (!script)
        throw new RPCError(errs.TYPE_ERROR, 'Invalid parameters.');

      script = Script.fromRaw(script);
      script = Script.fromScripthash(script.hash160());

      addr = script.getAddress();
    } else {
      addr = parseAddress(addr, this.network);
    }

    try {
      await wallet.importAddress(0, addr);
    } catch (e) {
      if (e.message !== 'Address already exists.')
        throw e;
    }

    if (rescan)
      await this.wdb.rescan(0);

    return null;
  }

  async importPubkey(args, help) {
    if (help || args.length < 1 || args.length > 3) {
      throw new RPCError(errs.MISC_ERROR,
        'importpubkey "pubkey" ( "label" rescan )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    const data = valid.buf(0);
    const rescan = valid.bool(2, false);

    if (!data)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');

    const key = KeyRing.fromPublic(data, this.network);

    await wallet.importKey(0, key);

    if (rescan)
      await this.wdb.rescan(0);

    return null;
  }

  async keyPoolRefill(args, help) {
    if (help || args.length > 1)
      throw new RPCError(errs.MISC_ERROR, 'keypoolrefill ( newsize )');
    return null;
  }

  async listAccounts(args, help) {
    if (help || args.length > 2) {
      throw new RPCError(errs.MISC_ERROR,
        'listaccounts ( minconf includeWatchonly)');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    const minconf = valid.u32(0, 0);
    const watchOnly = valid.bool(1, false);

    const accounts = await wallet.getAccounts();
    const map = {};

    for (const account of accounts) {
      const balance = await wallet.getBalance(account);
      let value = balance.unconfirmed;

      if (minconf > 0)
        value = balance.confirmed;

      if (wallet.watchOnly !== watchOnly)
        value = 0;

      map[account] = Amount.btc(value, true);
    }

    return map;
  }

  async listAddressGroupings(args, help) {
    throw new Error('Not implemented.');
  }

  async listLockUnspent(args, help) {
    if (help || args.length > 0)
      throw new RPCError(errs.MISC_ERROR, 'listlockunspent');

    const wallet = this.wallet;
    const outpoints = wallet.getLocked();
    const out = [];

    for (const outpoint of outpoints) {
      out.push({
        txid: outpoint.txid(),
        vout: outpoint.index
      });
    }

    return out;
  }

  async listReceivedByAccount(args, help) {
    if (help || args.length > 3) {
      throw new RPCError(errs.MISC_ERROR,
        'listreceivedbyaccount ( minconf includeempty includeWatchonly )');
    }

    const valid = new Validator(args);
    const minconf = valid.u32(0, 0);
    const includeEmpty = valid.bool(1, false);
    const watchOnly = valid.bool(2, false);

    return await this._listReceived(minconf, includeEmpty, watchOnly, true);
  }

  async listReceivedByAddress(args, help) {
    if (help || args.length > 3) {
      throw new RPCError(errs.MISC_ERROR,
        'listreceivedbyaddress ( minconf includeempty includeWatchonly )');
    }

    const valid = new Validator(args);
    const minconf = valid.u32(0, 0);
    const includeEmpty = valid.bool(1, false);
    const watchOnly = valid.bool(2, false);

    return await this._listReceived(minconf, includeEmpty, watchOnly, false);
  }

  async _listReceived(minconf, empty, watchOnly, account) {
    const wallet = this.wallet;
    const paths = await wallet.getPaths();
    const height = this.wdb.state.height;

    const map = new BufferMap();
    for (const path of paths) {
      const addr = path.toAddress();
      map.set(path.hash, {
        involvesWatchonly: wallet.watchOnly,
        address: addr.toString(this.network),
        account: path.name,
        amount: 0,
        confirmations: -1,
        label: ''
      });
    }

    const txs = await wallet.getHistory();

    for (const wtx of txs) {
      const conf = wtx.getDepth(height);

      if (conf < minconf)
        continue;

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

        if (!addr)
          continue;

        const hash = addr.getHash();
        const entry = map.get(hash);

        if (entry) {
          if (entry.confirmations === -1 || conf < entry.confirmations)
            entry.confirmations = conf;
          entry.address = addr.toString(this.network);
          entry.amount += output.value;
        }
      }
    }

    let out = [];
    for (const entry of map.values())
      out.push(entry);

    if (account) {
      const map = new Map();

      for (const entry of out) {
        const item = map.get(entry.account);
        if (!item) {
          map.set(entry.account, entry);
          entry.address = undefined;
          continue;
        }
        item.amount += entry.amount;
      }

      out = [];

      for (const entry of map.values())
        out.push(entry);
    }

    const result = [];
    for (const entry of out) {
      if (!empty && entry.amount === 0)
        continue;

      if (entry.confirmations === -1)
        entry.confirmations = 0;

      entry.amount = Amount.btc(entry.amount, true);
      result.push(entry);
    }

    return result;
  }

  async listSinceBlock(args, help) {
    if (help || args.length > 3) {
      throw new RPCError(errs.MISC_ERROR,
        'listsinceblock ( "blockhash" target-confirmations includeWatchonly)');
    }

    const wallet = this.wallet;
    const chainHeight = this.wdb.state.height;
    const valid = new Validator(args);
    const block = valid.brhash(0);
    const minconf = valid.u32(1, 0);
    const watchOnly = valid.bool(2, false);

    if (wallet.watchOnly !== watchOnly)
      return [];

    let height = -1;

    if (block) {
      const entry = await this.client.getEntry(block);
      if (entry)
        height = entry.height;
      else
        throw new RPCError(errs.MISC_ERROR, 'Block not found.');
    }

    if (height === -1)
      height = chainHeight;

    const txs = await wallet.getHistory();
    const out = [];

    let highest = null;

    for (const wtx of txs) {
      if (wtx.height < height)
        continue;

      if (wtx.getDepth(chainHeight) < minconf)
        continue;

      if (!highest || wtx.height > highest)
        highest = wtx;

      const json = await this._toListTX(wtx);

      out.push(json);
    }

    return {
      transactions: out,
      lastblock: highest && highest.block
        ? util.revHex(highest.block)
        : util.revHex(consensus.ZERO_HASH)
    };
  }

  async _toListTX(wtx) {
    const wallet = this.wallet;
    const details = await wallet.toDetails(wtx);

    if (!details)
      throw new RPCError(errs.WALLET_ERROR, 'TX not found.');

    let receive = true;
    for (const member of details.inputs) {
      if (member.path) {
        receive = false;
        break;
      }
    }

    let sent = 0;
    let received = 0;
    let sendMember = null;
    let recMember = null;
    let sendIndex = -1;
    let recIndex = -1;

    for (let i = 0; i < details.outputs.length; i++) {
      const member = details.outputs[i];

      if (member.path) {
        if (member.path.branch === 1)
          continue;
        received += member.value;
        recMember = member;
        recIndex = i;
        continue;
      }

      sent += member.value;
      sendMember = member;
      sendIndex = i;
    }

    let member = null;
    let index = -1;

    if (receive) {
      assert(recMember);
      member = recMember;
      index = recIndex;
    } else {
      if (sendMember) {
        member = sendMember;
        index = sendIndex;
      } else {
        // In the odd case where we send to ourselves.
        receive = true;
        received = 0;
        member = recMember;
        index = recIndex;
      }
    }

    let rbf = false;

    if (wtx.height === -1 && wtx.tx.isRBF())
      rbf = true;

    return {
      account: member.path ? member.path.name : '',
      address: member.address
        ? member.address.toString(this.network)
        : null,
      category: receive ? 'receive' : 'send',
      amount: Amount.btc(receive ? received : -sent, true),
      label: member.path ? member.path.name : undefined,
      vout: index,
      confirmations: details.getDepth(this.wdb.height),
      blockhash: details.block ? util.revHex(details.block) : null,
      blockindex: -1,
      blocktime: details.time,
      blockheight: details.height,
      txid: util.revHex(details.hash),
      walletconflicts: [],
      time: details.mtime,
      timereceived: details.mtime,
      'bip125-replaceable': rbf ? 'yes' : 'no'
    };
  }

  async listTransactions(args, help) {
    if (help || args.length > 4) {
      throw new RPCError(errs.MISC_ERROR,
        'listtransactions ( "account" count from includeWatchonly)');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    let name = valid.str(0);
    const count = valid.u32(1, 10);
    const from = valid.u32(2, 0);
    const watchOnly = valid.bool(3, false);

    if (wallet.watchOnly !== watchOnly)
      return [];

    if (name === '')
      name = 'default';

    const txs = await wallet.getHistory(name);

    common.sortTX(txs);

    const end = from + count;
    const to = Math.min(end, txs.length);
    const out = [];

    for (let i = from; i < to; i++) {
      const wtx = txs[i];
      const json = await this._toListTX(wtx);
      out.push(json);
    }

    return out;
  }

  async listUnspent(args, help) {
    if (help || args.length > 3) {
      throw new RPCError(errs.MISC_ERROR,
        'listunspent ( minconf maxconf  ["address",...] )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    const minDepth = valid.u32(0, 1);
    const maxDepth = valid.u32(1, 9999999);
    const addrs = valid.array(2);
    const height = this.wdb.state.height;

    const map = new BufferSet();

    if (addrs) {
      const valid = new Validator(addrs);
      for (let i = 0; i < addrs.length; i++) {
        const addr = valid.str(i, '');
        const hash = parseHash(addr, this.network);

        if (map.has(hash))
          throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate address.');

        map.add(hash);
      }
    }

    const coins = await wallet.getCoins();

    common.sortCoins(coins);

    const out = [];

    for (const coin of coins) {
      const depth = coin.getDepth(height);

      if (depth < minDepth || depth > maxDepth)
        continue;

      const addr = coin.getAddress();

      if (!addr)
        continue;

      const hash = coin.getHash();

      if (addrs) {
        if (!hash || !map.has(hash))
          continue;
      }

      const ring = await wallet.getKey(hash);

      out.push({
        txid: coin.txid(),
        vout: coin.index,
        address: addr ? addr.toString(this.network) : null,
        account: ring ? ring.name : undefined,
        redeemScript: ring && ring.script
          ? ring.script.toJSON()
          : undefined,
        scriptPubKey: coin.script.toJSON(),
        amount: Amount.btc(coin.value, true),
        confirmations: depth,
        spendable: !wallet.isLocked(coin),
        solvable: true
      });
    }

    return out;
  }

  async lockUnspent(args, help) {
    if (help || args.length < 1 || args.length > 2) {
      throw new RPCError(errs.MISC_ERROR,
        'lockunspent unlock ([{"txid":"txid","vout":n},...])');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    const unlock = valid.bool(0, false);
    const outputs = valid.array(1);

    if (args.length === 1) {
      if (unlock)
        wallet.unlockCoins();
      return true;
    }

    if (!outputs)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');

    for (const output of outputs) {
      const valid = new Validator(output);
      const hash = valid.brhash('txid');
      const index = valid.u32('vout');

      if (hash == null || index == null)
        throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.');

      const outpoint = new Outpoint(hash, index);

      if (unlock) {
        wallet.unlockCoin(outpoint);
        continue;
      }

      wallet.lockCoin(outpoint);
    }

    return true;
  }

  async move(args, help) {
    // Not implementing: stupid and deprecated.
    throw new Error('Not implemented.');
  }

  async sendFrom(args, help) {
    if (help || args.length < 3 || args.length > 6) {
      throw new RPCError(errs.MISC_ERROR,
        'sendfrom "fromaccount" "tobitcoinaddress"'
        + ' amount ( minconf "comment" "comment-to" )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    let name = valid.str(0);
    const str = valid.str(1);
    const value = valid.ufixed(2, 8);
    const minconf = valid.u32(3, 0);

    const addr = parseAddress(str, this.network);

    if (!addr || value == null)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');

    if (name === '')
      name = 'default';

    const options = {
      account: name,
      depth: minconf,
      outputs: [{
        address: addr,
        value: value
      }]
    };

    const tx = await wallet.send(options);

    return tx.txid();
  }

  async sendMany(args, help) {
    if (help || args.length < 2 || args.length > 5) {
      throw new RPCError(errs.MISC_ERROR,
        'sendmany "fromaccount" {"address":amount,...}'
        + ' ( minconf "comment" subtractfee )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    let name = valid.str(0);
    const sendTo = valid.obj(1);
    const minconf = valid.u32(2, 1);
    const subtract = valid.bool(4, false);

    if (name === '')
      name = 'default';

    if (!sendTo)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid send-to address.');

    const to = new Validator(sendTo);
    const uniq = new BufferSet();
    const outputs = [];

    for (const key of Object.keys(sendTo)) {
      const value = to.ufixed(key, 8);
      const addr = parseAddress(key, this.network);
      const hash = addr.getHash();

      if (value == null)
        throw new RPCError(errs.INVALID_PARAMETER, 'Invalid amount.');

      if (uniq.has(hash))
        throw new RPCError(errs.INVALID_PARAMETER,
          'Each send-to address must be unique.');

      uniq.add(hash);

      const output = new Output();
      output.value = value;
      output.script.fromAddress(addr);
      outputs.push(output);
    }

    const options = {
      outputs: outputs,
      subtractFee: subtract,
      account: name,
      depth: minconf
    };

    const tx = await wallet.send(options);

    return tx.txid();
  }

  async sendToAddress(args, help) {
    if (help || args.length < 2 || args.length > 5) {
      throw new RPCError(errs.MISC_ERROR,
        'sendtoaddress "bitcoinaddress" amount'
        + ' ( "comment" "comment-to" subtractfeefromamount )');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    const str = valid.str(0);
    const value = valid.ufixed(1, 8);
    const subtract = valid.bool(4, false);

    const addr = parseAddress(str, this.network);

    if (!addr || value == null)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');

    const options = {
      subtractFee: subtract,
      outputs: [{
        address: addr,
        value: value
      }]
    };

    const tx = await wallet.send(options);

    return tx.txid();
  }

  async setAccount(args, help) {
    // Impossible to implement in bcoin:
    throw new Error('Not implemented.');
  }

  async setTXFee(args, help) {
    const valid = new Validator(args);
    const rate = valid.ufixed(0, 8);

    if (help || args.length < 1 || args.length > 1)
      throw new RPCError(errs.MISC_ERROR, 'settxfee amount');

    if (rate == null)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');

    this.wdb.feeRate = rate;

    return true;
  }

  async signMessage(args, help) {
    if (help || args.length !== 2) {
      throw new RPCError(errs.MISC_ERROR,
        'signmessage "bitcoinaddress" "message"');
    }

    const wallet = this.wallet;
    const valid = new Validator(args);
    const b58 = valid.str(0, '');
    const str = valid.str(1, '');

    const addr = parseHash(b58, this.network);

    const ring = await wallet.getKey(addr);

    if (!ring)
      throw new RPCError(errs.WALLET_ERROR, 'Address not found.');

    if (!wallet.master.key)
      throw new RPCError(errs.WALLET_UNLOCK_NEEDED, 'Wallet is locked.');

    const sig = messageUtil.sign(str, ring);

    return sig.toString('base64');
  }

  async walletLock(args, help) {
    const wallet = this.wallet;

    if (help || (wallet.master.encrypted && args.length !== 0))
      throw new RPCError(errs.MISC_ERROR, 'walletlock');

    if (!wallet.master.encrypted) {
      throw new RPCError(
        errs.WALLET_WRONG_ENC_STATE,
        'Wallet is not encrypted.');
    }

    await wallet.lock();

    return null;
  }

  async walletPassphraseChange(args, help) {
    const wallet = this.wallet;

    if (help || (wallet.master.encrypted && args.length !== 2)) {
      throw new RPCError(errs.MISC_ERROR, 'walletpassphrasechange'
        + ' "oldpassphrase" "newpassphrase"');
    }

    const valid = new Validator(args);
    const old = valid.str(0, '');
    const passphrase = valid.str(1, '');

    if (!wallet.master.encrypted) {
      throw new RPCError(
        errs.WALLET_WRONG_ENC_STATE,
        'Wallet is not encrypted.');
    }

    if (old.length < 1 || passphrase.length < 1)
      throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter');

    await wallet.setPassphrase(passphrase, old);

    return null;
  }

  async walletPassphrase(args, help) {
    const wallet = this.wallet;
    const valid = new Validator(args);
    const passphrase = valid.str(0, '');
    const timeout = valid.u32(1);

    if (help || (wallet.master.encrypted && args.length !== 2)) {
      throw new RPCError(errs.MISC_ERROR,
        'walletpassphrase "passphrase" timeout');
    }

    if (!wallet.master.encrypted) {
      throw new RPCError(
        errs.WALLET_WRONG_ENC_STATE,
        'Wallet is not encrypted.');
    }

    if (passphrase.length < 1)
      throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter');

    if (timeout == null)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter');

    await wallet.unlock(passphrase, timeout);

    return null;
  }

  async importPrunedFunds(args, help) {
    if (help || args.length !== 2) {
      throw new RPCError(errs.MISC_ERROR,
        'importprunedfunds "rawtransaction" "txoutproof"');
    }

    const valid = new Validator(args);
    const txRaw = valid.buf(0);
    const blockRaw = valid.buf(1);

    if (!txRaw || !blockRaw)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');

    const tx = TX.fromRaw(txRaw);
    const block = MerkleBlock.fromRaw(blockRaw);
    const hash = block.hash();

    if (!block.verify())
      throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.');

    if (!block.hasTX(tx.hash()))
      throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.');

    const entry = await this.client.getEntry(hash);

    if (!entry)
      throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.');

    const meta = BlockMeta.fromEntry(entry);

    if (!await this.wdb.addTX(tx, meta))
      throw new RPCError(errs.WALLET_ERROR,
        'Address for TX not in wallet, or TX already in wallet');

    return null;
  }

  async removePrunedFunds(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'removeprunedfunds "txid"');

    const wallet = this.wallet;
    const valid = new Validator(args);
    const hash = valid.brhash(0);

    if (!hash)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');

    if (!await wallet.remove(hash))
      throw new RPCError(errs.WALLET_ERROR, 'Transaction not in wallet.');

    return null;
  }

  async selectWallet(args, help) {
    const valid = new Validator(args);
    const id = valid.str(0);

    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'selectwallet "id"');

    const wallet = await this.wdb.get(id);

    if (!wallet)
      throw new RPCError(errs.WALLET_ERROR, 'Wallet not found.');

    this.wallet = wallet;

    return null;
  }

  async getMemoryInfo(args, help) {
    if (help || args.length !== 0)
      throw new RPCError(errs.MISC_ERROR, 'getmemoryinfo');

    return this.logger.memoryUsage();
  }

  async setLogLevel(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'setloglevel "level"');

    const valid = new Validator(args);
    const level = valid.str(0, '');

    this.logger.setLevel(level);

    return null;
  }
}

/*
 * Helpers
 */

function parseHash(raw, network) {
  const addr = parseAddress(raw, network);
  return addr.getHash();
}

function parseAddress(raw, network) {
  try {
    return Address.fromString(raw, network);
  } catch (e) {
    throw new RPCError(errs.INVALID_ADDRESS_OR_KEY, 'Invalid address.');
  }
}

function parseSecret(raw, network) {
  try {
    return KeyRing.fromSecret(raw, network);
  } catch (e) {
    throw new RPCError(errs.INVALID_ADDRESS_OR_KEY, 'Invalid key.');
  }
}

/*
 * Expose
 */

module.exports = RPC;