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

'use strict';

var fs = require('fs');
var EventEmitter = require('events').EventEmitter;
var util = require('../utils/util');
var co = require('../utils/co');
var crypto = require('../crypto/crypto');
var assert = require('assert');
var common = require('../blockchain/common');
var ec = require('../crypto/ec');
var Amount = require('../btc/amount');
var NetAddress = require('../primitives/netaddress');
var Script = require('../script/script');
var Address = require('../primitives/address');
var Block = require('../primitives/block');
var Headers = require('../primitives/headers');
var Input = require('../primitives/input');
var KeyRing = require('../primitives/keyring');
var Lock = require('../utils/lock');
var MerkleBlock = require('../primitives/merkleblock');
var MTX = require('../primitives/mtx');
var Network = require('../protocol/network');
var Outpoint = require('../primitives/outpoint');
var Output = require('../primitives/output');
var TX = require('../primitives/tx');
var Logger = require('../node/logger');
var IP = require('../utils/ip');
var encoding = require('../utils/encoding');
var consensus = require('../protocol/consensus');
var pkg = require('../pkg');

/**
 * Bitcoin Core RPC
 * @alias module:http.RPC
 * @constructor
 * @param {Node} node
 */

function RPC(node) {
  if (!(this instanceof RPC))
    return new RPC(node);

  EventEmitter.call(this);

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

  this.node = node;
  this.network = node.network;
  this.chain = node.chain;
  this.mempool = node.mempool;
  this.pool = node.pool;
  this.fees = node.fees;
  this.miner = node.miner;
  this.wallet = node.wallet;
  this.walletdb = node.walletdb;
  this.logger = node.logger;

  this.locker = new Lock();

  this.feeRate = null;
  this.mining = false;
  this.proclimit = 0;

  this.attempt = null;
  this.start = 0;
  this._boundChain = false;
  this.coinbase = {};
}

util.inherits(RPC, EventEmitter);

RPC.magic = 'Bitcoin Signed Message:\n';

RPC.prototype.execute = function execute(json, help) {
  switch (json.method) {
    case 'stop':
      return this.stop(json.params, help);
    case 'help':
      return this.help(json.params, help);

    case 'getblockchaininfo':
      return this.getblockchaininfo(json.params, help);
    case 'getbestblockhash':
      return this.getbestblockhash(json.params, help);
    case 'getblockcount':
      return this.getblockcount(json.params, help);
    case 'getblock':
      return this.getblock(json.params, help);
    case 'getblockhash':
      return this.getblockhash(json.params, help);
    case 'getblockheader':
      return this.getblockheader(json.params, help);
    case 'getchaintips':
      return this.getchaintips(json.params, help);
    case 'getdifficulty':
      return this.getdifficulty(json.params, help);
    case 'getmempoolancestors':
      return this.getmempoolancestors(json.params, help);
    case 'getmempooldescendants':
      return this.getmempooldescendants(json.params, help);
    case 'getmempoolentry':
      return this.getmempoolentry(json.params, help);
    case 'getmempoolinfo':
      return this.getmempoolinfo(json.params, help);
    case 'getrawmempool':
      return this.getrawmempool(json.params, help);
    case 'gettxout':
      return this.gettxout(json.params, help);
    case 'gettxoutsetinfo':
      return this.gettxoutsetinfo(json.params, help);
    case 'verifychain':
      return this.verifychain(json.params, help);

    case 'invalidateblock':
      return this.invalidateblock(json.params, help);
    case 'reconsiderblock':
      return this.reconsiderblock(json.params, help);

    case 'getnetworkhashps':
      return this.getnetworkhashps(json.params, help);
    case 'getmininginfo':
      return this.getmininginfo(json.params, help);
    case 'prioritisetransaction':
      return this.prioritisetransaction(json.params, help);
    case 'getwork':
      return this.getwork(json.params, help);
    case 'getworklp':
      return this.getworklp(json.params, help);
    case 'getblocktemplate':
      return this.getblocktemplate(json.params, help);
    case 'submitblock':
      return this.submitblock(json.params, help);
    case 'verifyblock':
      return this.verifyblock(json.params, help);

    case 'setgenerate':
      return this.setgenerate(json.params, help);
    case 'getgenerate':
      return this.getgenerate(json.params, help);
    case 'generate':
      return this.generate(json.params, help);
    case 'generatetoaddress':
      return this.generatetoaddress(json.params, help);

    case 'estimatefee':
      return this.estimatefee(json.params, help);
    case 'estimatepriority':
      return this.estimatepriority(json.params, help);
    case 'estimatesmartfee':
      return this.estimatesmartfee(json.params, help);
    case 'estimatesmartpriority':
      return this.estimatesmartpriority(json.params, help);

    case 'getinfo':
      return this.getinfo(json.params, help);
    case 'validateaddress':
      return this.validateaddress(json.params, help);
    case 'createmultisig':
      return this.createmultisig(json.params, help);
    case 'createwitnessaddress':
      return this.createwitnessaddress(json.params, help);
    case 'verifymessage':
      return this.verifymessage(json.params, help);
    case 'signmessagewithprivkey':
      return this.signmessagewithprivkey(json.params, help);

    case 'setmocktime':
      return this.setmocktime(json.params, help);

    case 'getconnectioncount':
      return this.getconnectioncount(json.params, help);
    case 'ping':
      return this.ping(json.params, help);
    case 'getpeerinfo':
      return this.getpeerinfo(json.params, help);
    case 'addnode':
      return this.addnode(json.params, help);
    case 'disconnectnode':
      return this.disconnectnode(json.params, help);
    case 'getaddednodeinfo':
      return this.getaddednodeinfo(json.params, help);
    case 'getnettotals':
      return this.getnettotals(json.params, help);
    case 'getnetworkinfo':
      return this.getnetworkinfo(json.params, help);
    case 'setban':
      return this.setban(json.params, help);
    case 'listbanned':
      return this.listbanned(json.params, help);
    case 'clearbanned':
      return this.clearbanned(json.params, help);

    case 'getrawtransaction':
      return this.getrawtransaction(json.params, help);
    case 'createrawtransaction':
      return this.createrawtransaction(json.params, help);
    case 'decoderawtransaction':
      return this.decoderawtransaction(json.params, help);
    case 'decodescript':
      return this.decodescript(json.params, help);
    case 'sendrawtransaction':
      return this.sendrawtransaction(json.params, help);
    case 'signrawtransaction':
      return this.signrawtransaction(json.params, help);

    case 'gettxoutproof':
      return this.gettxoutproof(json.params, help);
    case 'verifytxoutproof':
      return this.verifytxoutproof(json.params, help);

    case 'fundrawtransaction':
      return this.fundrawtransaction(json.params, help);
    case 'resendwallettransactions':
      return this.resendwallettransactions(json.params, help);
    case 'abandontransaction':
      return this.abandontransaction(json.params, help);
    case 'addmultisigaddress':
      return this.addmultisigaddress(json.params, help);
    case 'addwitnessaddress':
      return this.addwitnessaddress(json.params, help);
    case 'backupwallet':
      return this.backupwallet(json.params, help);
    case 'dumpprivkey':
      return this.dumpprivkey(json.params, help);
    case 'dumpwallet':
      return this.dumpwallet(json.params, help);
    case 'encryptwallet':
      return this.encryptwallet(json.params, help);
    case 'getaccountaddress':
      return this.getaccountaddress(json.params, help);
    case 'getaccount':
      return this.getaccount(json.params, help);
    case 'getaddressesbyaccount':
      return this.getaddressesbyaccount(json.params, help);
    case 'getbalance':
      return this.getbalance(json.params, help);
    case 'getnewaddress':
      return this.getnewaddress(json.params, help);
    case 'getrawchangeaddress':
      return this.getrawchangeaddress(json.params, help);
    case 'getreceivedbyaccount':
      return this.getreceivedbyaccount(json.params, help);
    case 'getreceivedbyaddress':
      return this.getreceivedbyaddress(json.params, help);
    case 'gettransaction':
      return this.gettransaction(json.params, help);
    case 'getunconfirmedbalance':
      return this.getunconfirmedbalance(json.params, help);
    case 'getwalletinfo':
      return this.getwalletinfo(json.params, help);
    case 'importprivkey':
      return this.importprivkey(json.params, help);
    case 'importwallet':
      return this.importwallet(json.params, help);
    case 'importaddress':
      return this.importaddress(json.params, help);
    case 'importprunedfunds':
      return this.importprunedfunds(json.params, help);
    case 'importpubkey':
      return this.importpubkey(json.params, help);
    case 'keypoolrefill':
      return this.keypoolrefill(json.params, help);
    case 'listaccounts':
      return this.listaccounts(json.params, help);
    case 'listaddressgroupings':
      return this.listaddressgroupings(json.params, help);
    case 'listlockunspent':
      return this.listlockunspent(json.params, help);
    case 'listreceivedbyaccount':
      return this.listreceivedbyaccount(json.params, help);
    case 'listreceivedbyaddress':
      return this.listreceivedbyaddress(json.params, help);
    case 'listsinceblock':
      return this.listsinceblock(json.params, help);
    case 'listtransactions':
      return this.listtransactions(json.params, help);
    case 'listunspent':
      return this.listunspent(json.params, help);
    case 'lockunspent':
      return this.lockunspent(json.params, help);
    case 'move':
      return this.move(json.params, help);
    case 'sendfrom':
      return this.sendfrom(json.params, help);
    case 'sendmany':
      return this.sendmany(json.params, help);
    case 'sendtoaddress':
      return this.sendtoaddress(json.params, help);
    case 'setaccount':
      return this.setaccount(json.params, help);
    case 'settxfee':
      return this.settxfee(json.params, help);
    case 'signmessage':
      return this.signmessage(json.params, help);
    case 'walletlock':
      return this.walletlock(json.params, help);
    case 'walletpassphrasechange':
      return this.walletpassphrasechange(json.params, help);
    case 'walletpassphrase':
      return this.walletpassphrase(json.params, help);
    case 'removeprunedfunds':
      return this.removeprunedfunds(json.params, help);

    case 'getmemory':
      return this.getmemory(json.params, help);
    case 'selectwallet':
      return this.selectwallet(json.params, help);
    case 'setloglevel':
      return this.setloglevel(json.params, help);

    default:
      return Promise.reject(new Error('Not found: ' + json.method + '.'));
  }
};

/*
 * Overall control/query calls
 */

RPC.prototype.getinfo = co(function* getinfo(args, help) {
  var balance;

  if (help || args.length !== 0)
    throw new RPCError('getinfo');

  balance = yield this.wallet.getBalance();

  return {
    version: pkg.version,
    protocolversion: this.pool.options.version,
    walletversion: 0,
    balance: Amount.btc(balance.unconfirmed, true),
    blocks: this.chain.height,
    timeoffset: this.network.time.offset,
    connections: this.pool.peers.size(),
    proxy: '',
    difficulty: this._getDifficulty(),
    testnet: this.network.type !== Network.main,
    keypoololdest: 0,
    keypoolsize: 0,
    unlocked_until: this.wallet.master.until,
    paytxfee: Amount.btc(this.network.feeRate, true),
    relayfee: Amount.btc(this.network.minRelay, true),
    errors: ''
  };
});

RPC.prototype.help = co(function* _help(args, help) {
  var json;

  if (args.length === 0)
    return 'Select a command.';

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

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

RPC.prototype.stop = co(function* stop(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('stop');

  this.node.close();

  return 'Stopping.';
});

/*
 * P2P networking
 */

RPC.prototype.getnetworkinfo = co(function* getnetworkinfo(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('getnetworkinfo');

  return {
    version: pkg.version,
    subversion: this.pool.options.agent,
    protocolversion: this.pool.options.version,
    localservices: this.pool.address.services,
    timeoffset: this.network.time.offset,
    connections: this.pool.peers.size(),
    networks: [],
    relayfee: Amount.btc(this.network.minRelay, true),
    localaddresses: [],
    warnings: ''
  };
});

RPC.prototype.addnode = co(function* addnode(args, help) {
  var node, cmd, addr, peer;

  if (help || args.length !== 2)
    throw new RPCError('addnode "node" "add|remove|onetry"');

  node = toString(args[0]);
  cmd = toString(args[1]);
  addr = NetAddress.fromHostname(node, this.network);

  switch (cmd) {
    case 'add':
      this.pool.hosts.add(addr);
      break;
    case 'remove':
      this.pool.hosts.remove(addr.hostname);
      break;
    case 'onetry':
      if (!this.pool.peers.get(addr.hostname)) {
        peer = this.pool.createPeer(addr);
        this.pool.peers.add(peer);
      }
      break;
  }

  return null;
});

RPC.prototype.disconnectnode = co(function* disconnectnode(args, help) {
  var addr, peer;

  if (help || args.length !== 1)
    throw new RPCError('disconnectnode "node"');

  addr = toString(args[0]);
  addr = IP.fromHostname(addr, this.network.port);

  peer = this.pool.peers.get(addr.hostname);

  if (peer)
    peer.destroy();

  return null;
});

RPC.prototype.getaddednodeinfo = co(function* getaddednodeinfo(args, help) {
  var out = [];
  var addr, peer;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('getaddednodeinfo dummy ( "node" )');

  if (args.length === 2) {
    addr = toString(args[1]);
    addr = IP.fromHostname(addr, this.network.port);
    peer = this.pool.peers.get(addr.hostname);
    if (!peer)
      throw new RPCError('Node has not been added.');
    return [this._toAddedNode(peer)];
  }

  for (peer = this.pool.peers.head(); peer; peer = peer.next)
    out.push(this._toAddedNode(peer));

  return out;
});

RPC.prototype._toAddedNode = function _toAddedNode(peer) {
  return {
    addednode: peer.hostname(),
    connected: peer.connected,
    addresses: [
      {
        address: peer.hostname(),
        connected: peer.outbound
          ? 'outbound'
          : 'inbound'
      }
    ]
  };
};

RPC.prototype.getconnectioncount = co(function* getconnectioncount(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('getconnectioncount');

  return this.pool.peers.size();
});

RPC.prototype.getnettotals = co(function* getnettotals(args, help) {
  var sent = 0;
  var recv = 0;
  var peer;

  if (help || args.length > 0)
    throw new RPCError('getnettotals');

  for (peer = this.pool.peers.head(); peer; peer = peer.next) {
    sent += peer.socket.bytesWritten;
    recv += peer.socket.bytesRead;
  }

  return {
    totalbytesrecv: recv,
    totalbytessent: sent,
    timemillis: util.ms()
  };
});

RPC.prototype.getpeerinfo = co(function* getpeerinfo(args, help) {
  var peers = [];
  var id = 0;
  var peer, offset;

  if (help || args.length !== 0)
    throw new RPCError('getpeerinfo');

  for (peer = this.pool.peers.head(); peer; peer = peer.next) {
    offset = this.network.time.known[peer.hostname()];

    if (offset == null)
      offset = 0;

    peers.push({
      id: id++,
      addr: peer.hostname(),
      addrlocal: peer.hostname(),
      relaytxes: !peer.noRelay,
      lastsend: peer.lastSend / 1000 | 0,
      lastrecv: peer.lastRecv / 1000 | 0,
      bytessent: peer.socket.bytesWritten,
      bytesrecv: peer.socket.bytesRead,
      conntime: peer.ts !== 0 ? (util.ms() - peer.ts) / 1000 | 0 : 0,
      timeoffset: offset,
      pingtime: peer.lastPong !== -1
        ? (peer.lastPong - peer.lastPing) / 1000
        : -1,
      minping: peer.minPing !== -1 ? peer.minPing / 1000 : -1,
      version: peer.version,
      subver: peer.agent,
      inbound: !peer.outbound,
      startingheight: peer.height,
      besthash: peer.bestHash ? util.revHex(peer.bestHash) : null,
      bestheight: peer.bestHeight,
      banscore: peer.banScore,
      inflight: peer.requestMap.keys().map(util.revHex),
      whitelisted: false
    });
  }

  return peers;
});

RPC.prototype.ping = co(function* ping(args, help) {
  var peer;

  if (help || args.length !== 0)
    throw new RPCError('ping');

  for (peer = this.pool.peers.head(); peer; peer = peer.next)
    peer.sendPing();

  return null;
});

RPC.prototype.setban = co(function* setban(args, help) {
  var addr;

  if (help
      || args.length < 2
      || (args[1] !== 'add' && args[1] !== 'remove')) {
    throw new RPCError('setban "ip(/netmask)"'
      + ' "add|remove" (bantime) (absolute)');
  }

  addr = toString(args[0]);
  addr = NetAddress.fromHostname(addr, this.network);

  switch (args[1]) {
    case 'add':
      this.pool.ban(addr);
      break;
    case 'remove':
      this.pool.unban(addr);
      break;
  }

  return null;
});

RPC.prototype.listbanned = co(function* listbanned(args, help) {
  var i, banned, keys, host, time;

  if (help || args.length !== 0)
    throw new RPCError('listbanned');

  banned = [];
  keys = Object.keys(this.pool.hosts.banned);

  for (i = 0; i < keys.length; i++) {
    host = keys[i];
    time = this.pool.hosts.banned[host];
    banned.push({
      address: host,
      banned_until: time + this.pool.hosts.banTime,
      ban_created: time,
      ban_reason: ''
    });
  }

  return banned;
});

RPC.prototype.clearbanned = co(function* clearbanned(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('clearbanned');

  this.pool.hosts.clearBanned();

  return null;
});

RPC.prototype._deployment = function _deployment(id, version, status) {
  return {
    id: id,
    version: version,
    reject: {
      status: status
    }
  };
};

RPC.prototype._getSoftforks = function _getSoftforks() {
  return [
    this._deployment('bip34', 2, this.chain.state.hasBIP34()),
    this._deployment('bip66', 3, this.chain.state.hasBIP66()),
    this._deployment('bip65', 4, this.chain.state.hasCLTV())
  ];
};

RPC.prototype._getBIP9Softforks = co(function* _getBIP9Softforks() {
  var tip = this.chain.tip;
  var forks = {};
  var i, deployment, state, status;

  for (i = 0; i < this.network.deploys.length; i++) {
    deployment = this.network.deploys[i];
    state = yield this.chain.getState(tip, deployment);

    switch (state) {
      case common.thresholdStates.DEFINED:
        status = 'defined';
        break;
      case common.thresholdStates.STARTED:
        status = 'started';
        break;
      case common.thresholdStates.LOCKED_IN:
        status = 'locked_in';
        break;
      case common.thresholdStates.ACTIVE:
        status = 'active';
        break;
      case common.thresholdStates.FAILED:
        status = 'failed';
        break;
      default:
        assert(false, 'Bad state.');
        break;
    }

    forks[deployment.name] = {
      status: status,
      bit: deployment.bit,
      startTime: deployment.startTime,
      timeout: deployment.timeout
    };
  }

  return forks;
});

/* Block chain and UTXO */
RPC.prototype.getblockchaininfo = co(function* getblockchaininfo(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('getblockchaininfo');

  return {
    chain: 'main',
    blocks: this.chain.height,
    headers: this.chain.height,
    bestblockhash: this.chain.tip.rhash(),
    difficulty: this._getDifficulty(),
    mediantime: yield this.chain.tip.getMedianTime(),
    verificationprogress: this.chain.getProgress(),
    chainwork: this.chain.tip.chainwork.toString('hex', 64),
    pruned: this.chain.options.prune,
    softforks: this._getSoftforks(),
    bip9_softforks: yield this._getBIP9Softforks(),
    pruneheight: this.chain.options.prune
      ? Math.max(0, this.chain.height - this.network.block.keepBlocks)
      : null
  };
});

RPC.prototype._getDifficulty = function getDifficulty(entry) {
  var shift, diff;

  if (!entry) {
    if (!this.chain.tip)
      return 1.0;
    entry = this.chain.tip;
  }

  shift = (entry.bits >>> 24) & 0xff;
  diff = 0x0000ffff / (entry.bits & 0x00ffffff);

  while (shift < 29) {
    diff *= 256.0;
    shift++;
  }

  while (shift > 29) {
    diff /= 256.0;
    shift--;
  }

  return diff;
};

RPC.prototype.getbestblockhash = co(function* getbestblockhash(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('getbestblockhash');

  return this.chain.tip.rhash();
});

RPC.prototype.getblockcount = co(function* getblockcount(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('getblockcount');

  return this.chain.tip.height;
});

RPC.prototype.getblock = co(function* getblock(args, help) {
  var hash, verbose, entry, block;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('getblock "hash" ( verbose )');

  hash = toHash(args[0]);

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

  verbose = true;

  if (args.length > 1)
    verbose = toBool(args[1]);

  entry = yield this.chain.db.getEntry(hash);

  if (!entry)
    throw new RPCError('Block not found');

  block = yield this.chain.db.getBlock(entry.hash);

  if (!block) {
    if (this.chain.options.spv)
      throw new RPCError('Block not available (spv mode)');

    if (this.chain.options.prune)
      throw new RPCError('Block not available (pruned data)');

    throw new RPCError('Can\'t read block from disk');
  }

  if (!verbose)
    return block.toRaw().toString('hex');

  return yield this._blockToJSON(entry, block, false);
});

RPC.prototype._txToJSON = function _txToJSON(tx, entry) {
  var self = this;
  var height = entry ? entry.height : -1;
  var conf = 0;

  if (height >= this.chain.height)
    conf = height + 1 - this.chain.height;

  return {
    txid: tx.txid(),
    hash: tx.wtxid(),
    size: tx.getSize(),
    vsize: tx.getVirtualSize(),
    version: tx.version,
    locktime: tx.locktime,
    vin: tx.inputs.map(function(input) {
      var out = {};
      if (tx.isCoinbase()) {
        out.coinbase = input.script.toJSON();
      } else {
        out.txid = input.prevout.txid();
        out.vout = input.prevout.index;
        out.scriptSig = {
          asm: input.script.toASM(),
          hex: input.script.toJSON()
        };
      }
      if (input.witness.items.length > 0) {
        out.txinwitness = input.witness.items.map(function(item) {
          return item.toString('hex');
        });
      }
      out.sequence = input.sequence;
      return out;
    }),
    vout: tx.outputs.map(function(output, i) {
      return {
        value: Amount.btc(output.value, true),
        n: i,
        scriptPubKey: self._scriptToJSON(output.script, true)
      };
    }),
    blockhash: entry ? entry.rhash() : null,
    confirmations: conf,
    time: entry ? entry.ts : 0,
    blocktime: entry ? entry.ts : 0,
    hex: undefined
  };
};

RPC.prototype._scriptToJSON = function scriptToJSON(script, hex) {
  var type = script.getType();
  var address = script.getAddress();
  var out;

  out = {
    asm: script.toASM(),
    hex: undefined,
    type: Script.typesByVal[type],
    reqSigs: 1,
    addresses: []
  };

  if (hex)
    out.hex = script.toJSON();

  if (script.isMultisig())
    out.reqSigs = script.getSmall(0);

  if (address) {
    address = address.toBase58(this.network);
    out.addresses.push(address);
  }

  return out;
};

RPC.prototype.getblockhash = co(function* getblockhash(args, help) {
  var height, hash;

  if (help || args.length !== 1)
    throw new RPCError('getblockhash index');

  height = toNumber(args[0]);

  if (height < 0 || height > this.chain.height)
    throw new RPCError('Block height out of range.');

  hash = yield this.chain.db.getHash(height);

  if (!hash)
    throw new RPCError('Not found.');

  return util.revHex(hash);
});

RPC.prototype.getblockheader = co(function* getblockheader(args, help) {
  var hash, verbose, entry;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('getblockheader "hash" ( verbose )');

  hash = toHash(args[0]);

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

  verbose = true;

  if (args.length > 1)
    verbose = toBool(args[1], true);

  entry = yield this.chain.db.getEntry(hash);

  if (!entry)
    throw new RPCError('Block not found');

  if (!verbose)
    return entry.toRaw().toString('hex', 0, 80);

  return yield this._headerToJSON(entry);
});

RPC.prototype._headerToJSON = co(function* _headerToJSON(entry) {
  var medianTime = yield entry.getMedianTime();
  var nextHash = yield this.chain.db.getNextHash(entry.hash);

  return {
    hash: entry.rhash(),
    confirmations: this.chain.height - entry.height + 1,
    height: entry.height,
    version: entry.version,
    merkleroot: util.revHex(entry.merkleRoot),
    time: entry.ts,
    mediantime: medianTime,
    bits: entry.bits,
    difficulty: this._getDifficulty(entry),
    chainwork: entry.chainwork.toString('hex', 64),
    previousblockhash: entry.prevBlock !== encoding.NULL_HASH
      ? util.revHex(entry.prevBlock)
      : null,
    nextblockhash: nextHash ? util.revHex(nextHash) : null
  };
});

RPC.prototype._blockToJSON = co(function* _blockToJSON(entry, block, txDetails) {
  var self = this;
  var mtp = yield entry.getMedianTime();
  var nextHash = yield this.chain.db.getNextHash(entry.hash);

  return {
    hash: entry.rhash(),
    confirmations: this.chain.height - entry.height + 1,
    strippedsize: block.getBaseSize(),
    size: block.getSize(),
    weight: block.getWeight(),
    height: entry.height,
    version: entry.version,
    merkleroot: util.revHex(entry.merkleRoot),
    tx: block.txs.map(function(tx) {
      if (txDetails)
        return self._txToJSON(tx, entry);
      return tx.txid();
    }),
    time: entry.ts,
    mediantime: mtp,
    bits: entry.bits,
    difficulty: this._getDifficulty(entry),
    chainwork: entry.chainwork.toString('hex', 64),
    previousblockhash: entry.prevBlock !== encoding.NULL_HASH
      ? util.revHex(entry.prevBlock)
      : null,
    nextblockhash: nextHash ? util.revHex(nextHash) : null
  };
});

RPC.prototype.getchaintips = co(function* getchaintips(args, help) {
  var i, hash, tips, result, entry, fork, main;

  if (help || args.length !== 0)
    throw new RPCError('getchaintips');

  tips = yield this.chain.db.getTips();
  result = [];

  for (i = 0; i < tips.length; i++) {
    hash = tips[i];
    entry = yield this.chain.db.getEntry(hash);
    assert(entry);

    fork = yield this._findFork(entry);
    main = yield entry.isMainChain();

    result.push({
      height: entry.height,
      hash: entry.rhash(),
      branchlen: entry.height - fork.height,
      status: main ? 'active' : 'valid-headers'
    });
  }

  return result;
});

RPC.prototype._findFork = co(function* _findFork(entry) {
  while (entry) {
    if (yield entry.isMainChain())
      return entry;
    entry = yield entry.getPrevious();
  }
  throw new Error('Fork not found.');
});

RPC.prototype.getdifficulty = co(function* getdifficulty(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('getdifficulty');

  return this._getDifficulty();
});

RPC.prototype.getmempoolinfo = co(function* getmempoolinfo(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('getmempoolinfo');

  if (!this.mempool)
    throw new RPCError('No mempool available.');

  return {
    size: this.mempool.totalTX,
    bytes: this.mempool.getSize(),
    usage: this.mempool.getSize(),
    maxmempool: this.mempool.options.maxSize,
    mempoolminfee: Amount.btc(this.mempool.options.minRelay, true)
  };
});

RPC.prototype.getmempoolancestors = co(function* getmempoolancestors(args, help) {
  var out = [];
  var i, hash, verbose, entry, entries;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('getmempoolancestors txid (verbose)');

  if (!this.mempool)
    throw new RPCError('No mempool available.');

  hash = toHash(args[0]);

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

  if (args.length > 1)
    verbose = toBool(args[1], false);

  entry = this.mempool.getEntry(hash);

  if (!entry)
    throw new RPCError('Transaction not in mempool.');

  entries = this.mempool.getAncestors(entry.tx);

  if (verbose) {
    for (i = 0; i < entries.length; i++) {
      entry = entries[i];
      out.push(this._entryToJSON(entry));
    }
  } else {
    for (i = 0; i < entries.length; i++) {
      entry = entries[i];
      out.push(entry.tx.txid());
    }
  }

  return out;
});

RPC.prototype.getmempooldescendants = co(function* getmempooldescendants(args, help) {
  var out = [];
  var i, hash, verbose, entry, entries;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('getmempooldescendants txid (verbose)');

  if (!this.mempool)
    throw new RPCError('No mempool available.');

  hash = toHash(args[0]);

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

  if (args.length > 1)
    verbose = toBool(args[1], false);

  entry = this.mempool.getEntry(hash);

  if (!entry)
    throw new RPCError('Transaction not in mempool.');

  entries = this.mempool.getDescendants(entry.tx);

  if (verbose) {
    for (i = 0; i < entries.length; i++) {
      entry = entries[i];
      out.push(this._entryToJSON(entry));
    }
  } else {
    for (i = 0; i < entries.length; i++) {
      entry = entries[i];
      out.push(entry.tx.txid());
    }
  }

  return out;
});

RPC.prototype.getmempoolentry = co(function* getmempoolentry(args, help) {
  var hash, entry;

  if (help || args.length !== 1)
    throw new RPCError('getmempoolentry txid');

  if (!this.mempool)
    throw new RPCError('No mempool available.');

  hash = toHash(args[0]);

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

  entry = this.mempool.getEntry(hash);

  if (!entry)
    throw new RPCError('Transaction not in mempool.');

  return this._entryToJSON(entry);
});

RPC.prototype.getrawmempool = co(function* getrawmempool(args, help) {
  var verbose;

  if (help || args.length > 1)
    throw new RPCError('getrawmempool ( verbose )');

  verbose = false;

  if (args.length > 0)
    verbose = toBool(args[0], false);

  return this._mempoolToJSON(verbose);
});

RPC.prototype._mempoolToJSON = function _mempoolToJSON(verbose) {
  var out = {};
  var i, hashes, hash, entry;

  if (verbose) {
    hashes = this.mempool.getSnapshot();

    for (i = 0; i < hashes.length; i++) {
      hash = hashes[i];
      entry = this.mempool.getEntry(hash);

      if (!entry)
        continue;

      out[entry.tx.txid()] = this._entryToJSON(entry);
    }

    return out;
  }

  hashes = this.mempool.getSnapshot();

  return hashes.map(util.revHex);
};

RPC.prototype._entryToJSON = function _entryToJSON(entry) {
  return {
    size: entry.size,
    fee: Amount.btc(entry.fee, true),
    modifiedfee: 0,
    time: entry.ts,
    height: entry.height,
    startingpriority: entry.priority,
    currentpriority: entry.getPriority(this.chain.height),
    descendantcount: this.mempool.countDescendants(entry),
    descendantsize: entry.descSize,
    descendantfees: Amount.btc(entry.descFee, true),
    ancestorcount: this.mempool.countAncestors(entry),
    ancestorsize: 0,
    ancestorfees: 0,
    depends: this.mempool.getDepends(entry.tx).map(util.revHex)
  };
};

RPC.prototype.gettxout = co(function* gettxout(args, help) {
  var hash, index, mempool, coin;

  if (help || args.length < 2 || args.length > 3)
    throw new RPCError('gettxout "txid" n ( includemempool )');

  if (this.chain.options.spv)
    throw new RPCError('Cannot get coins in SPV mode.');

  if (this.chain.options.prune)
    throw new RPCError('Cannot get coins when pruned.');

  hash = toHash(args[0]);
  index = toNumber(args[1]);
  mempool = true;

  if (args.length > 2)
    mempool = toBool(args[2], true);

  if (!hash || index < 0)
    throw new RPCError('Invalid parameter.');

  if (mempool)
    coin = this.mempool.getCoin(hash, index);

  if (!coin)
    coin = yield this.chain.db.getCoin(hash, index);

  if (!coin)
    return null;

  return {
    bestblock: this.chain.tip.rhash(),
    confirmations: coin.getDepth(this.chain.height),
    value: Amount.btc(coin.value, true),
    scriptPubKey: this._scriptToJSON(coin.script, true),
    version: coin.version,
    coinbase: coin.coinbase
  };
});

RPC.prototype.gettxoutproof = co(function* gettxoutproof(args, help) {
  var uniq = {};
  var i, txids, block, hash, txid, tx, coins;

  if (help || (args.length !== 1 && args.length !== 2))
    throw new RPCError('gettxoutproof ["txid",...] ( blockhash )');

  if (this.chain.options.spv)
    throw new RPCError('Cannot get coins in SPV mode.');

  if (this.chain.options.prune)
    throw new RPCError('Cannot get coins when pruned.');

  txids = toArray(args[0]);
  hash = args[1];

  if (!txids || txids.length === 0)
    throw new RPCError('Invalid parameter.');

  if (hash) {
    hash = toHash(hash);
    if (!hash)
      throw new RPCError('Invalid parameter.');
  }

  for (i = 0; i < txids.length; i++) {
    txid = toHash(txids[i]);

    if (!txid)
      throw new RPCError('Invalid parameter.');

    if (uniq[txid])
      throw new RPCError('Duplicate txid.');

    uniq[txid] = true;
    txids[i] = txid;
  }

  if (hash) {
    block = yield this.chain.db.getBlock(hash);
  } else if (this.chain.options.indexTX) {
    tx = yield this.chain.db.getMeta(txid);
    if (!tx)
      return;
    block = yield this.chain.db.getBlock(tx.block);
  } else {
    coins = yield this.chain.db.getCoins(txid);
    if (!coins)
      return;
    block = yield this.chain.db.getBlock(coins.height);
  }

  if (!block)
    throw new RPCError('Block not found.');

  for (i = 0; i < txids.length; i++) {
    txid = txids[i];
    if (!block.hasTX(txid))
      throw new RPCError('Block does not contain all txids.');
  }

  block = MerkleBlock.fromHashes(block, txids);

  return block.toRaw().toString('hex');
});

RPC.prototype.verifytxoutproof = co(function* verifytxoutproof(args, help) {
  var out = [];
  var i, block, hash, entry;

  if (help || args.length !== 1)
    throw new RPCError('verifytxoutproof "proof"');

  block = MerkleBlock.fromRaw(toString(args[0]), 'hex');

  if (!block.verify())
    return out;

  entry = yield this.chain.db.getEntry(block.hash('hex'));

  if (!entry)
    throw new RPCError('Block not found in chain.');

  for (i = 0; i < block.matches.length; i++) {
    hash = block.matches[i];
    out.push(util.revHex(hash));
  }

  return out;
});

RPC.prototype.gettxoutsetinfo = co(function* gettxoutsetinfo(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('gettxoutsetinfo');

  if (this.chain.options.spv)
    throw new RPCError('Chainstate not available (SPV mode).');

  return {
    height: this.chain.height,
    bestblock: this.chain.tip.rhash(),
    transactions: this.chain.db.state.tx,
    txouts: this.chain.db.state.coin,
    bytes_serialized: 0,
    hash_serialized: 0,
    total_amount: Amount.btc(this.chain.db.state.value, true)
  };
});

RPC.prototype.verifychain = co(function* verifychain(args, help) {
  if (help || args.length > 2)
    throw new RPCError('verifychain ( checklevel numblocks )');

  if (this.chain.options.spv)
    throw new RPCError('Cannot verify chain in SPV mode.');

  if (this.chain.options.prune)
    throw new RPCError('Cannot verify chain when pruned.');

  return null;
});

/*
 * Mining
 */

RPC.prototype._submitwork = co(function* _submitwork(data) {
  var unlock = yield this.locker.lock();
  try {
    return yield this.__submitwork(data);
  } finally {
    unlock();
  }
});

RPC.prototype.__submitwork = co(function* _submitwork(data) {
  var attempt = this.attempt;
  var block, entry, header, cb, cur;

  if (data.length !== 128)
    throw new RPCError('Invalid parameter.');

  if (!attempt)
    return false;

  data = data.slice(0, 80);

  reverseEndian(data);

  header = Headers.fromAbbr(data);
  block = attempt.block;

  if (header.prevBlock !== block.prevBlock
      || header.bits !== block.bits) {
    return false;
  }

  if (!header.verify())
    return false;

  cb = this.coinbase[header.merkleRoot];

  if (!cb)
    return false;

  cur = block.txs[0];
  block.txs[0] = cb;
  attempt.updateMerkle();

  if (header.merkleRoot !== block.merkleRoot) {
    block.txs[0] = cur;
    attempt.updateMerkle();
    this.logger.warning('Bad calculated merkle root for submitted work.');
    return false;
  }

  block.ts = header.ts;
  attempt.commit(header.nonce);

  try {
    entry = yield this.chain.add(block);
  } catch (err) {
    if (err.type === 'VerifyError') {
      this.logger.warning('RPC block rejected: %s (%s).',
        block.rhash(), err.reason);
      return false;
    }
    throw err;
  }

  if (!entry) {
    this.logger.warning('RPC block rejected: %s (bad-prevblk).',
      block.rhash());
    return false;
  }

  return true;
});

RPC.prototype._creatework = co(function* _creatework(data) {
  var unlock = yield this.locker.lock();
  try {
    return yield this.__creatework(data);
  } finally {
    unlock();
  }
});

RPC.prototype.__creatework = co(function* _creatework() {
  var attempt = yield this._getAttempt(true);
  var data, abbr;

  data = new Buffer(128);
  data.fill(0);

  abbr = attempt.block.abbr();
  abbr.copy(data, 0);

  data[80] = 0x80;
  data.writeUInt32BE(80 * 8, data.length - 4, true);

  reverseEndian(data);

  return {
    data: data.toString('hex'),
    target: attempt.target.toString('hex'),
    height: attempt.height
  };
});

RPC.prototype.getworklp = co(function* getworklp(args, help) {
  yield this._onBlock();
  return yield this._creatework();
});

RPC.prototype.getwork = co(function* getwork(args, help) {
  var data;

  if (args.length > 1)
    throw new RPCError('getwork ( "data" )');

  if (args.length === 1) {
    if (!util.isHex(args[0]))
      throw new RPCError('Invalid parameter.');

    data = new Buffer(args[0], 'hex');

    return yield this._submitwork(data);
  }

  return yield this._creatework();
});

RPC.prototype.submitblock = co(function* submitblock(args, help) {
  var block, tx;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('submitblock "hexdata" ( "jsonparametersobject" )');

  block = Block.fromRaw(toString(args[0]), 'hex');

  // Fix eloipool bug (witness nonce is not present).
  if (this.chain.state.hasWitness() && block.getCommitmentHash()) {
    tx = block.txs[0];
    if (!tx.hasWitness()) {
      this.logger.warning('Submitted block had no witness nonce.');
      this.logger.debug(tx);

      // Recreate witness nonce (all zeroes).
      tx.inputs[0].witness.set(0, encoding.ZERO_HASH);
      tx.inputs[0].witness.compile();

      tx.refresh();
      block.refresh();
    }
  }

  return yield this._submitblock(block);
});

RPC.prototype._submitblock = co(function* submitblock(block) {
  var unlock = yield this.locker.lock();
  try {
    return yield this.__submitblock(block);
  } finally {
    unlock();
  }
});

RPC.prototype.__submitblock = co(function* submitblock(block) {
  var entry;

  this.logger.info('Handling submitted block: %s.', block.rhash());

  try {
    entry = yield this.chain.add(block);
  } catch (err) {
    if (err.type === 'VerifyError') {
      this.logger.warning('RPC block rejected: %s (%s).',
        block.rhash(), err.reason);
      return 'rejected: ' + err.reason;
    }
    throw err;
  }

  if (!entry) {
    this.logger.warning('RPC block rejected: %s (bad-prevblk).',
      block.rhash());
    return 'rejected: bad-prevblk';
  }

  return null;
});

RPC.prototype.getblocktemplate = co(function* getblocktemplate(args, help) {
  var mode = 'template';
  var version = -1;
  var coinbase = false;
  var rules = [];
  var i, opt, lpid, cap, block;
  var coinbasevalue, coinbasetxn;

  if (help || args.length > 1)
    throw new RPCError('getblocktemplate ( "jsonrequestobject" )');

  if (args.length === 1) {
    opt = args[0] || {};

    if (opt.mode != null) {
      mode = opt.mode;
      if (mode !== 'template' && mode !== 'proposal')
        throw new RPCError('Invalid mode.');
    }

    lpid = opt.longpollid;

    if (mode === 'proposal') {
      if (!util.isHex(opt.data))
        throw new RPCError('Invalid parameter.');

      block = Block.fromRaw(opt.data, 'hex');

      return yield this._submitblock(block);
    }

    if (Array.isArray(opt.rules)) {
      for (i = 0; i < opt.rules.length; i++)
        rules.push(toString(opt.rules[i]));
    } else if (util.isNumber(opt.maxversion)) {
      version = opt.maxversion;
    }

    if (Array.isArray(opt.capabilities)) {
      for (i = 0; i < opt.capabilities.length; i++) {
        cap = toString(opt.capabilities[i]);
        switch (cap) {
          case 'coinbasetxn':
            coinbasetxn = true;
            break;
          case 'coinbasevalue':
            coinbasevalue = true;
            break;
          case 'coinbase/append':
            break;
        }
      }

      if (coinbasetxn)
        coinbase = true;
    }
  }

  if (!this.network.selfConnect) {
    if (this.pool.peers.size() === 0)
      throw new RPCError('Bitcoin is not connected!');

    if (!this.chain.synced)
      throw new RPCError('Bitcoin is downloading blocks...');
  }

  yield this._poll(lpid);

  return yield this._template(version, coinbase, rules);
});

RPC.prototype._template = co(function* _template(version, coinbase, rules) {
  var unlock = yield this.locker.lock();
  try {
    return yield this.__template(version, coinbase, rules);
  } finally {
    unlock();
  }
});

RPC.prototype.__template = co(function* _template(version, coinbase, rules) {
  var attempt = yield this._getAttempt(false);
  var scale = attempt.witness ? 1 : consensus.WITNESS_SCALE_FACTOR;
  var block = attempt.block;
  var mutable = ['time', 'transactions', 'prevblock'];
  var txs = [];
  var index = {};
  var vbavailable = {};
  var vbrules = [];
  var i, j, entry, tx, input, output;
  var dep, deps, json, name, deploy;
  var state;

  for (i = 0; i < attempt.items.length; i++) {
    entry = attempt.items[i];
    index[entry.hash] = i;
  }

  for (i = 0; i < attempt.items.length; i++) {
    entry = attempt.items[i];
    tx = entry.tx;
    deps = [];

    for (j = 0; j < tx.inputs.length; j++) {
      input = tx.inputs[j];
      dep = index[input.prevout.hash];
      if (dep != null && deps.indexOf(dep) === -1) {
        assert(dep < i);
        deps.push(dep);
      }
    }

    txs.push({
      data: tx.toRaw().toString('hex'),
      txid: tx.txid(),
      hash: tx.wtxid(),
      depends: deps,
      fee: entry.fee,
      sigops: entry.sigops / scale | 0,
      weight: tx.getWeight()
    });
  }

  if (version >= 2)
    mutable.push('version/force');

  for (i = 0; i < this.network.deploys.length; i++) {
    deploy = this.network.deploys[i];
    state = yield this.chain.getState(this.chain.tip, deploy);
    name = deploy.name;

    switch (state) {
      case common.thresholdStates.DEFINED:
      case common.thresholdStates.FAILED:
        break;
      case common.thresholdStates.LOCKED_IN:
        block.version |= 1 << deploy.bit;
      case common.thresholdStates.STARTED:
        if (!deploy.force) {
          if (rules.indexOf(name) === -1)
            block.version &= ~(1 << deploy.bit);
          name = '!' + name;
        }
        vbavailable[name] = deploy.bit;
        break;
      case common.thresholdStates.ACTIVE:
        if (!deploy.force) {
          if (rules.indexOf(name) === -1)
            throw new RPCError('Client must support ' + name + '.');
          name = '!' + name;
        }
        vbrules.push(name);
        break;
      default:
        assert(false, 'Bad state.');
        break;
    }
  }

  block.version >>>= 0;

  json = {
    capabilities: ['proposal'],
    mutable: mutable,
    version: block.version,
    rules: vbrules,
    vbavailable: vbavailable,
    vbrequired: 0,
    height: attempt.height,
    previousblockhash: util.revHex(block.prevBlock),
    target: util.revHex(attempt.target.toString('hex')),
    bits: util.hex32(block.bits),
    noncerange: '00000000ffffffff',
    curtime: block.ts,
    mintime: block.ts,
    maxtime: block.ts + 7200,
    expires: block.ts + 7200,
    sigoplimit: consensus.MAX_BLOCK_SIGOPS_COST / scale | 0,
    sizelimit: consensus.MAX_BLOCK_SIZE,
    weightlimit: undefined,
    longpollid: this.chain.tip.rhash() + util.pad32(this._totalTX()),
    submitold: false,
    coinbaseaux: {
      flags: attempt.coinbaseFlags.toString('hex')
    },
    coinbasevalue: attempt.coinbase.getOutputValue(),
    coinbasetxn: undefined,
    default_witness_commitment: undefined,
    transactions: txs
  };

  // See:
  // bitcoin/bitcoin#9fc7f0bce94f1cea0239b1543227f22a3f3b9274
  if (attempt.witness) {
    json.sizelimit = consensus.MAX_RAW_BLOCK_SIZE;
    json.weightlimit = consensus.MAX_BLOCK_WEIGHT;
  }

  if (coinbase) {
    tx = attempt.coinbase;

    // We don't include the commitment
    // output (see bip145).
    if (attempt.witness) {
      output = tx.outputs.pop();
      assert(output.script.isCommitment());
    }

    json.coinbasetxn = {
      data: tx.toRaw().toString('hex'),
      txid: tx.txid(),
      hash: tx.wtxid(),
      depends: [],
      fee: 0,
      sigops: tx.getSigopsCost() / scale | 0,
      weight: tx.getWeight()
    };

    if (attempt.witness)
      tx.outputs.push(output);
  }

  if (attempt.witness) {
    tx = attempt.coinbase;
    output = tx.outputs[tx.outputs.length - 1];
    assert(output.script.isCommitment());
    json.default_witness_commitment = output.script.toJSON();
  }

  return json;
});

RPC.prototype._poll = co(function* _poll(lpid) {
  var watched, lastTX;

  if (typeof lpid !== 'string')
    return;

  if (lpid.length !== 74)
    throw new RPCError('Invalid parameter.');

  watched = lpid.slice(0, 64);
  lastTX = +lpid.slice(64, 74);

  if (!util.isHex(watched) || !util.isNumber(lastTX))
    throw new RPCError('Invalid parameter.');

  watched = util.revHex(watched);

  if (this.chain.tip.hash !== watched)
    return;

  yield this._onBlock();
});

RPC.prototype._onBlock = function _onBlock() {
  var self = this;
  return new Promise(function(resolve, reject) {
    self.once('clear block', resolve);
  });
};

RPC.prototype._clearBlock = function _clearBlock() {
  this.attempt = null;
  this.start = 0;
  this.coinbase = {};
  this.emit('clear block');
};

RPC.prototype._bindChain = function _bindChain() {
  var self = this;

  if (this._boundChain)
    return;

  this._boundChain = true;

  this.node.on('connect', function() {
    if (!self.attempt)
      return;

    self._clearBlock();
  });

  if (!this.mempool)
    return;

  this.node.on('tx', function() {
    if (!self.attempt)
      return;

    if (util.now() - self.start > 10)
      self._clearBlock();
  });
};

RPC.prototype._getAttempt = co(function* _getAttempt(update) {
  var attempt = this.attempt;
  var block;

  this._bindChain();

  if (attempt) {
    block = attempt.block;
    if (update) {
      attempt.updateNonce();
      this.coinbase[block.merkleRoot] = attempt.coinbase.clone();
    }
    return attempt;
  }

  attempt = yield this.miner.createBlock();
  block = attempt.block;

  this.attempt = attempt;
  this.start = util.now();
  this.coinbase[block.merkleRoot] = attempt.coinbase.clone();

  return attempt;
});

RPC.prototype._totalTX = function _totalTX() {
  return this.mempool ? this.mempool.totalTX : 0;
};

RPC.prototype.getmininginfo = co(function* getmininginfo(args, help) {
  var attempt = this.attempt;
  var hashps;

  if (help || args.length !== 0)
    throw new RPCError('getmininginfo');

  hashps = yield this._hashps(120, -1);

  return {
    blocks: this.chain.height,
    currentblocksize: attempt ? attempt.block.getBaseSize() : 0,
    currentblocktx: attempt ? attempt.block.txs.length : 0,
    difficulty: this._getDifficulty(),
    errors: '',
    genproclimit: this.proclimit,
    networkhashps: hashps,
    pooledtx: this._totalTX(),
    testnet: this.network !== Network.main,
    chain: 'main',
    generate: this.mining
  };
});

RPC.prototype.getnetworkhashps = co(function* getnetworkhashps(args, help) {
  var lookup = 120;
  var height = -1;

  if (help || args.length > 2)
    throw new RPCError('getnetworkhashps ( blocks height )');

  if (args.length > 0)
    lookup = toNumber(args[0], 120);

  if (args.length > 1)
    height = toNumber(args[1], -1);

  return yield this._hashps(lookup, height);
});

RPC.prototype.prioritisetransaction = co(function* prioritisetransaction(args, help) {
  var hash, pri, fee, entry;

  if (help || args.length !== 3) {
    throw new RPCError('prioritisetransaction'
      + ' <txid> <priority delta> <fee delta>');
  }

  if (!this.mempool)
    throw new RPCError('No mempool available.');

  hash = toHash(args[0]);
  pri = args[1];
  fee = args[2];

  if (!hash)
    throw new RPCError('Invalid parameter');

  if (!util.isNumber(pri) || !util.isNumber(fee))
    throw new RPCError('Invalid parameter');

  entry = this.mempool.getEntry(hash);

  if (!entry)
    throw new RPCError('Transaction not in mempool.');

  entry.priority += pri;
  entry.fee += fee;

  if (entry.priority < 0)
    entry.priority = 0;

  if (entry.fee < 0)
    entry.fee = 0;

  return true;
});

RPC.prototype._hashps = co(function* _hashps(lookup, height) {
  var tip = this.chain.tip;
  var i, minTime, maxTime, workDiff, timeDiff, ps, entry;

  if (height !== -1)
    tip = yield this.chain.db.getEntry(height);

  if (!tip)
    return 0;

  if (lookup <= 0)
    lookup = tip.height % this.network.pow.retargetInterval + 1;

  if (lookup > tip.height)
    lookup = tip.height;

  minTime = tip.ts;
  maxTime = minTime;
  entry = tip;

  for (i = 0; i < lookup; i++) {
    entry = yield entry.getPrevious();

    if (!entry)
      throw new RPCError('Not found.');

    minTime = Math.min(entry.ts, minTime);
    maxTime = Math.max(entry.ts, maxTime);
  }

  if (minTime === maxTime)
    return 0;

  workDiff = tip.chainwork.sub(entry.chainwork);
  timeDiff = maxTime - minTime;
  ps = +workDiff.toString(10) / timeDiff;

  return ps;
});

RPC.prototype.verifyblock = co(function* verifyblock(args, help) {
  var block;

  if (help || args.length !== 1)
    throw new RPCError('verifyblock "block-hex"');

  if (typeof args[0] !== 'string')
    throw new RPCError('Invalid parameters.');

  if (this.chain.options.spv)
    throw new RPCError('Cannot verify block in SPV mode.');

  block = Block.fromRaw(args[0], 'hex');

  try {
    yield this.chain.verifyBlock(block);
  } catch (e) {
    if (e.type === 'VerifyError')
      return e.reason;
    throw e;
  }

  return null;
});

/*
 * Coin generation
 */

RPC.prototype.getgenerate = co(function* getgenerate(args, help) {
  if (help || args.length !== 0)
    throw new RPCError('getgenerate');
  return this.mining;
});

RPC.prototype.setgenerate = co(function* setgenerate(args, help) {
  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('setgenerate mine ( proclimit )');

  this.mining = toBool(args[0]);
  this.proclimit = toNumber(args[1], 0);

  if (this.mining)
    this.miner.start().catch(util.nop);
  else
    yield this.miner.stop();

  return this.mining;
});

RPC.prototype.generate = co(function* generate(args, help) {
  var unlock = yield this.locker.lock();
  try {
    return yield this._generate(args);
  } finally {
    unlock();
  }
});

RPC.prototype._generate = co(function* generate(args, help) {
  var numblocks;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('generate numblocks ( maxtries )');

  numblocks = toNumber(args[0], 1);

  return yield this._generateBlocks(numblocks);
});

RPC.prototype._generateBlocks = co(function* _generateBlocks(blocks, address) {
  var hashes = [];
  var i, block;

  for (i = 0; i < blocks; i++) {
    block = yield this.miner.mineBlock(null, address);
    hashes.push(block.rhash());
    assert(yield this.chain.add(block));
  }

  return hashes;
});

RPC.prototype.generatetoaddress = co(function* generatetoaddress(args, help) {
  var unlock = yield this.locker.lock();
  try {
    return yield this._generatetoaddress(args);
  } finally {
    unlock();
  }
});

RPC.prototype._generatetoaddress = co(function* generatetoaddress(args, help) {
  var numblocks, address;

  if (help || args.length < 2 || args.length > 3)
    throw new RPCError('generatetoaddress numblocks address ( maxtries )');

  numblocks = toNumber(args[0], 1);
  address = Address.fromBase58(toString(args[1]), this.network);

  return yield this._generateBlocks(numblocks, address);
});

/*
 * Raw transactions
 */

RPC.prototype.createrawtransaction = co(function* createrawtransaction(args, help) {
  var inputs, sendTo, tx, locktime;
  var i, input, output, hash, index, sequence;
  var keys, addrs, key, value, address, b58;

  if (help || args.length < 2 || args.length > 3) {
    throw new RPCError('createrawtransaction'
      + ' [{"txid":"id","vout":n},...]'
      + ' {"address":amount,"data":"hex",...}'
      + ' ( locktime )');
  }

  inputs = toArray(args[0]);
  sendTo = toObject(args[1]);

  if (!inputs || !sendTo)
    throw new RPCError('Invalid parameter');

  tx = new TX();

  if (args.length > 2 && args[2] != null) {
    locktime = toNumber(args[2]);
    if (!util.isUInt32(locktime))
      throw new RPCError('Locktime out of range');
    tx.locktime = locktime;
  }

  for (i = 0; i < inputs.length; i++) {
    input = inputs[i];

    if (!input)
      throw new RPCError('Invalid parameter');

    hash = toHash(input.txid);
    index = input.vout;
    sequence = 0xffffffff;

    if (tx.locktime)
      sequence--;

    if (!hash || !util.isUInt32(index))
      throw new RPCError('Invalid parameter');

    if (util.isNumber(input.sequence)) {
      sequence = toNumber(input.sequence);
      if (!util.isUInt32(sequence))
        throw new RPCError('Invalid parameter');
    }

    input = new Input();
    input.prevout.hash = hash;
    input.prevout.index = index;
    input.sequence = sequence;

    tx.inputs.push(input);
  }

  keys = Object.keys(sendTo);
  addrs = {};

  for (i = 0; i < keys.length; i++) {
    key = keys[i];
    value = sendTo[key];

    if (key === 'data') {
      value = new Buffer(value, 'hex');
      output = new Output();
      output.value = 0;
      output.script.fromNulldata(value);
      tx.outputs.push(output);
      continue;
    }

    address = Address.fromBase58(key, this.network);
    b58 = address.toBase58(this.network);

    if (addrs[b58])
      throw new RPCError('Duplicate address');

    addrs[b58] = true;

    output = new Output();
    output.value = toSatoshi(value);
    output.script.fromAddress(address);

    tx.outputs.push(output);
  }

  return tx.toRaw().toString('hex');
});

RPC.prototype.decoderawtransaction = co(function* decoderawtransaction(args, help) {
  var tx;

  if (help || args.length !== 1)
    throw new RPCError('decoderawtransaction "hexstring"');

  tx = TX.fromRaw(toString(args[0]), 'hex');

  return this._txToJSON(tx);
});

RPC.prototype.decodescript = co(function* decodescript(args, help) {
  var data, script, address;

  if (help || args.length !== 1)
    throw new RPCError('decodescript "hex"');

  data = toString(args[0]);
  script = new Script();

  if (data.length > 0)
    script.fromRaw(new Buffer(data, 'hex'));

  address = Address.fromScripthash(script.hash160());

  script = this._scriptToJSON(script);
  script.p2sh = address.toBase58(this.network);

  return script;
});

RPC.prototype.getrawtransaction = co(function* getrawtransaction(args, help) {
  var hash, verbose, json, meta, tx, entry;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('getrawtransaction "txid" ( verbose )');

  hash = toHash(args[0]);

  if (!hash)
    throw new RPCError('Invalid parameter');

  verbose = false;

  if (args.length > 1)
    verbose = toBool(args[1]);

  meta = yield this.node.getMeta(hash);

  if (!meta)
    throw new RPCError('Transaction not found.');

  tx = meta.tx;

  if (!verbose)
    return tx.toRaw().toString('hex');

  if (meta.block)
    entry = yield this.chain.db.getEntry(meta.block);

  json = this._txToJSON(tx, entry);
  json.time = meta.ps;
  json.hex = tx.toRaw().toString('hex');

  return json;
});

RPC.prototype.sendrawtransaction = co(function* sendrawtransaction(args, help) {
  var tx;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('sendrawtransaction "hexstring" ( allowhighfees )');

  if (!util.isHex(args[0]))
    throw new RPCError('Invalid parameter');

  tx = TX.fromRaw(args[0], 'hex');

  this.node.relay(tx);

  return tx.txid();
});

RPC.prototype.signrawtransaction = co(function* signrawtransaction(args, help) {
  var wallet = this.wallet;
  var tx;

  if (help || args.length < 1 || args.length > 4) {
    throw new RPCError('signrawtransaction'
      + ' "hexstring" ('
      + ' [{"txid":"id","vout":n,"scriptPubKey":"hex",'
      + 'redeemScript":"hex"},...] ["privatekey1",...]'
      + ' sighashtype )');
  }

  if (!util.isHex(args[0]))
    throw new RPCError('Invalid parameter');

  tx = MTX.fromRaw(args[0], 'hex');
  tx.view = yield wallet.getCoinView(tx);

  return yield this._signrawtransaction(wallet, tx, args);
});

RPC.prototype._signrawtransaction = co(function* signrawtransaction(wallet, tx, args) {
  var type = Script.hashType.ALL;
  var keys = [];
  var keyMap = {};
  var i, j, k, secret, key;
  var coin, prevout, prev;
  var hash, index, script, value;
  var redeem, op, parts;

  if (args.length > 2 && Array.isArray(args[2])) {
    k = args[2];
    for (i = 0; i < k.length; i++) {
      secret = k[i];

      if (typeof secret !== 'string')
        throw new RPCError('Invalid parameter');

      key = KeyRing.fromSecret(secret, this.network);
      keyMap[key.getPublicKey('hex')] = key;
      keys.push(key);
    }
  }

  if (args.length > 1 && Array.isArray(args[1])) {
    prevout = args[1];

    for (i = 0; i < prevout.length; i++) {
      prev = prevout[i];

      if (!prev || typeof prev !== 'object')
        throw new RPCError('Invalid parameter');

      hash = toHash(prev.txid);
      index = prev.vout;
      script = prev.scriptPubKey;
      value = toSatoshi(prev.amount);

      if (!hash
          || !util.isUInt32(index)
          || !util.isHex(script)) {
        throw new RPCError('Invalid parameter');
      }

      script = Script.fromRaw(script, 'hex');

      coin = new Output();
      coin.script = script;
      coin.value = value;

      tx.view.addOutput(hash, index, coin);

      if (keys.length === 0 || !util.isHex(prev.redeemScript))
        continue;

      if (script.isScripthash() || script.isWitnessScripthash()) {
        redeem = Script.fromRaw(prev.redeemScript, 'hex');
        for (j = 0; j < redeem.code.length; j++) {
          op = redeem.code[j];

          if (!op.data)
            continue;

          key = keyMap[op.data.toString('hex')];

          if (key) {
            key.script = redeem;
            key.witness = script.isWitnessScripthash();
            key.refresh();
            break;
          }
        }
      }
    }
  }

  if (args.length > 3) {
    parts = toString(args[3]).split('|');
    type = Script.hashType[parts[0]];

    if (type == null)
      throw new RPCError('Invalid parameter');

    if (parts.length > 2)
      throw new RPCError('Invalid parameter');

    if (parts.length === 2) {
      if (parts[1] !== 'ANYONECANPAY')
        throw new RPCError('Invalid parameter');
      type |= Script.hashType.ANYONECANPAY;
    }
  }

  yield tx.signAsync(keys, type);
  yield wallet.sign(tx, { type: type });

  return {
    hex: tx.toRaw().toString('hex'),
    complete: tx.isSigned()
  };
});

RPC.prototype.fundrawtransaction = co(function* fundrawtransaction(args, help) {
  var wallet = this.wallet;
  var feeRate = this.feeRate;
  var tx, options, changeAddress;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('fundrawtransaction "hexstring" ( options )');

  if (!util.isHex(args[0]))
    throw new RPCError('Invalid parameter.');

  tx = MTX.fromRaw(args[0], 'hex');

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

  if (args.length > 1) {
    options = toObject(args[1]);
    changeAddress = toString(options.changeAddress);

    if (changeAddress)
      changeAddress = Address.fromBase58(changeAddress, this.network);

    feeRate = options.feeRate;

    if (feeRate != null)
      feeRate = toSatoshi(feeRate);
  }

  options = {
    rate: feeRate,
    changeAddress: changeAddress
  };

  yield wallet.fund(tx, options);

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

RPC.prototype._createRedeem = co(function* _createRedeem(args, help) {
  var wallet = this.wallet;
  var i, m, n, keys, hash, script, key, ring;

  if (!util.isNumber(args[0])
      || !Array.isArray(args[1])
      || args[0] < 1
      || args[1].length < args[0]
      || args[1].length > 16) {
    throw new RPCError('Invalid parameter.');
  }

  m = args[0];
  n = args[1].length;
  keys = args[1];

  for (i = 0; i < keys.length; i++) {
    key = keys[i];

    if (!util.isBase58(key)) {
      if (!util.isHex(key))
        throw new RPCError('Invalid key.');
      keys[i] = new Buffer(key, 'hex');
      continue;
    }

    hash = Address.getHash(key, 'hex');

    if (!hash)
      throw new RPCError('Invalid key.');

    ring = yield wallet.getKey(hash);

    if (!ring)
      throw new RPCError('Invalid key.');

    keys[i] = ring.publicKey;
  }

  try {
    script = Script.fromMultisig(m, n, keys);
  } catch (e) {
    throw new RPCError('Invalid parameters.');
  }

  if (script.getSize() > consensus.MAX_SCRIPT_PUSH)
    throw new RPCError('Redeem script exceeds size limit.');

  return script;
});

/*
 * Utility Functions
 */

RPC.prototype.createmultisig = co(function* createmultisig(args, help) {
  var script, address;

  if (help || args.length < 2 || args.length > 2)
    throw new RPCError('createmultisig nrequired ["key",...]');

  script = yield this._createRedeem(args);
  address = script.getAddress();

  return {
    address: address.toBase58(this.network),
    redeemScript: script.toJSON()
  };
});

RPC.prototype.createwitnessaddress = co(function* createwitnessaddress(args, help) {
  var raw, script, program, address;

  if (help || args.length !== 1)
    throw new RPCError('createwitnessaddress "script"');

  raw = toString(args[1]);
  script = Script.fromRaw(raw, 'hex');
  program = script.forWitness();
  address = program.getAddress();

  return {
    address: address.toBase58(this.network),
    witnessScript: program.toJSON()
  };
});

RPC.prototype.validateaddress = co(function* validateaddress(args, help) {
  var wallet = this.wallet;
  var b58, address, json, path, script;

  if (help || args.length !== 1)
    throw new RPCError('validateaddress "bitcoinaddress"');

  b58 = toString(args[0]);

  try {
    address = Address.fromBase58(b58, this.network);
  } catch (e) {
    return {
      isvalid: false
    };
  }

  path = yield wallet.getPath(address);
  script = Script.fromAddress(address);

  json = {
    isvalid: true,
    address: address.toBase58(this.network),
    scriptPubKey: script.toJSON(),
    ismine: path ? true : false,
    iswatchonly: path ? wallet.watchOnly : false,
    account: undefined,
    hdkeypath: undefined
  };

  if (!path)
    return json;

  json.account = path.name;
  json.hdkeypath = path.toPath();

  return json;
});

RPC.prototype.verifymessage = co(function* verifymessage(args, help) {
  var address, sig, msg, hash, key;

  if (help || args.length !== 3)
    throw new RPCError('verifymessage "bitcoinaddress" "signature" "message"');

  address = toString(args[0]);
  sig = toString(args[1]);
  msg = toString(args[2]);

  hash = Address.getHash(address);

  if (!hash)
    throw new RPCError('Invalid address.');

  sig = new Buffer(sig, 'base64');
  msg = new Buffer(RPC.magic + msg, 'utf8');
  msg = crypto.hash256(msg);

  key = ec.recover(msg, sig, 0, true);

  if (!key)
    return false;

  key = crypto.hash160(key);

  return crypto.ccmp(key, hash);
});

RPC.prototype.signmessagewithprivkey = co(function* signmessagewithprivkey(args, help) {
  var key, msg, sig;

  if (help || args.length !== 2)
    throw new RPCError('signmessagewithprivkey "privkey" "message"');

  key = toString(args[0]);
  msg = toString(args[1]);

  key = KeyRing.fromSecret(key, this.network);
  msg = new Buffer(RPC.magic + msg, 'utf8');
  msg = crypto.hash256(msg);

  sig = key.sign(msg);

  return sig.toString('base64');
});

RPC.prototype.estimatefee = co(function* estimatefee(args, help) {
  var blocks, fee;

  if (help || args.length !== 1)
    throw new RPCError('estimatefee nblocks');

  if (!this.fees)
    throw new RPCError('Fee estimation not available.');

  blocks = toNumber(args[0], 1);

  if (blocks < 1)
    blocks = 1;

  fee = this.fees.estimateFee(blocks, false);

  if (fee === 0)
    fee = -1;
  else
    fee = Amount.btc(fee, true);

  return fee;
});

RPC.prototype.estimatepriority = co(function* estimatepriority(args, help) {
  var blocks, pri;

  if (help || args.length !== 1)
    throw new RPCError('estimatepriority nblocks');

  if (!this.fees)
    throw new RPCError('Priority estimation not available.');

  blocks = toNumber(args[0], 1);

  if (blocks < 1)
    blocks = 1;

  pri = this.fees.estimatePriority(blocks, false);

  return pri;
});

RPC.prototype.estimatesmartfee = co(function* estimatesmartfee(args, help) {
  var blocks, fee;

  if (help || args.length !== 1)
    throw new RPCError('estimatesmartfee nblocks');

  if (!this.fees)
    throw new RPCError('Fee estimation not available.');

  blocks = toNumber(args[0], 1);

  if (blocks < 1)
    blocks = 1;

  fee = this.fees.estimateFee(blocks, true);

  if (fee === 0)
    fee = -1;
  else
    fee = Amount.btc(fee, true);

  return {
    fee: fee,
    blocks: blocks
  };
});

RPC.prototype.estimatesmartpriority = co(function* estimatesmartpriority(args, help) {
  var blocks, pri;

  if (help || args.length !== 1)
    throw new RPCError('estimatesmartpriority nblocks');

  if (!this.fees)
    throw new RPCError('Priority estimation not available.');

  blocks = toNumber(args[0], 1);

  if (blocks < 1)
    blocks = 1;

  pri = this.fees.estimatePriority(blocks, true);

  return {
    priority: pri,
    blocks: blocks
  };
});

RPC.prototype.invalidateblock = co(function* invalidateblock(args, help) {
  var hash;

  if (help || args.length !== 1)
    throw new RPCError('invalidateblock "hash"');

  hash = toHash(args[0]);

  if (!hash)
    throw new RPCError('Block not found.');

  this.chain.setInvalid(hash);

  return null;
});

RPC.prototype.reconsiderblock = co(function* reconsiderblock(args, help) {
  var hash;

  if (help || args.length !== 1)
    throw new RPCError('reconsiderblock "hash"');

  hash = toHash(args[0]);

  if (!hash)
    throw new RPCError('Block not found.');

  this.chain.removeInvalid(hash);

  return null;
});

RPC.prototype.setmocktime = co(function* setmocktime(args, help) {
  var ts, delta;

  if (help || args.length !== 1)
    throw new RPCError('setmocktime timestamp');

  ts = toNumber(args[0]);

  if (ts < 0)
    throw new RPCError('Invalid parameter.');

  this.network.time.offset = 0;

  delta = this.network.now() - ts;

  this.network.time.offset = -delta;

  return null;
});

/*
 * Wallet
 */

RPC.prototype.resendwallettransactions = co(function* resendwallettransactions(args, help) {
  var wallet = this.wallet;
  var hashes = [];
  var i, tx, txs;

  if (help || args.length !== 0)
    throw new RPCError('resendwallettransactions');

  txs = yield wallet.resend();

  for (i = 0; i < txs.length; i++) {
    tx = txs[i];
    hashes.push(tx.txid());
  }

  return hashes;
});

RPC.prototype.addmultisigaddress = co(function* addmultisigaddress(args, help) {
  if (help || args.length < 2 || args.length > 3) {
    throw new RPCError('addmultisigaddress'
      + ' nrequired ["key",...] ( "account" )');
  }

  // Impossible to implement in bcoin (no address book).
  throw new Error('Not implemented.');
});

RPC.prototype.addwitnessaddress = co(function* addwitnessaddress(args, help) {
  if (help || args.length < 1 || args.length > 1)
    throw new RPCError('addwitnessaddress "address"');

  // Unlikely to be implemented.
  throw new Error('Not implemented.');
});

RPC.prototype.backupwallet = co(function* backupwallet(args, help) {
  var dest;

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

  dest = toString(args[0]);

  yield this.walletdb.backup(dest);

  return null;
});

RPC.prototype.dumpprivkey = co(function* dumpprivkey(args, help) {
  var wallet = this.wallet;
  var hash, ring;

  if (help || args.length !== 1)
    throw new RPCError('dumpprivkey "bitcoinaddress"');

  hash = Address.getHash(toString(args[0]), 'hex');

  if (!hash)
    throw new RPCError('Invalid address.');

  ring = yield wallet.getPrivateKey(hash);

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

  return ring.toSecret();
});

RPC.prototype.dumpwallet = co(function* dumpwallet(args, help) {
  var wallet = this.wallet;
  var i, file, time, address, fmt, str, out, hash, hashes, ring;

  if (help || args.length !== 1)
    throw new RPCError('dumpwallet "filename"');

  if (!args[0] || typeof args[0] !== 'string')
    throw new RPCError('Invalid parameter.');

  file = toString(args[0]);
  time = util.date();
  out = [
    util.fmt('# Wallet Dump created by Bcoin %s', pkg.version),
    util.fmt('# * Created on %s', time),
    util.fmt('# * Best block at time of backup was %d (%s),',
      this.chain.height, this.chain.tip.rhash()),
    util.fmt('#   mined on %s', util.date(this.chain.tip.ts)),
    util.fmt('# * File: %s', file),
    ''
  ];

  hashes = yield wallet.getAddressHashes();

  for (i = 0; i < hashes.length; i++) {
    hash = hashes[i];
    ring = yield wallet.getPrivateKey(hash);

    if (!ring)
      continue;

    address = ring.getAddress('base58');
    fmt = '%s %s label= addr=%s';

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

    str = util.fmt(fmt, ring.toSecret(), time, address);

    out.push(str);
  }

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

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

  if (fs.unsupported)
    return out;

  yield writeFile(file, out);

  return out;
});

RPC.prototype.encryptwallet = co(function* encryptwallet(args, help) {
  var wallet = this.wallet;
  var passphrase;

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

  if (wallet.master.encrypted)
    throw new RPCError('Already running with an encrypted wallet');

  passphrase = toString(args[0]);

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

  yield wallet.setPassphrase(passphrase);

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

RPC.prototype.getaccountaddress = co(function* getaccountaddress(args, help) {
  var wallet = this.wallet;
  var account;

  if (help || args.length !== 1)
    throw new RPCError('getaccountaddress "account"');

  account = toString(args[0]);

  if (!account)
    account = 'default';

  account = yield wallet.getAccount(account);

  if (!account)
    return '';

  return account.receive.getAddress('base58');
});

RPC.prototype.getaccount = co(function* getaccount(args, help) {
  var wallet = this.wallet;
  var hash, path;

  if (help || args.length !== 1)
    throw new RPCError('getaccount "bitcoinaddress"');

  hash = Address.getHash(args[0], 'hex');

  if (!hash)
    throw new RPCError('Invalid address.');

  path = yield wallet.getPath(hash);

  if (!path)
    return '';

  return path.name;
});

RPC.prototype.getaddressesbyaccount = co(function* getaddressesbyaccount(args, help) {
  var wallet = this.wallet;
  var i, path, account, address, addrs, paths;

  if (help || args.length !== 1)
    throw new RPCError('getaddressesbyaccount "account"');

  account = toString(args[0]);

  if (!account)
    account = 'default';

  addrs = [];

  paths = yield wallet.getPaths(account);

  for (i = 0; i < paths.length; i++) {
    path = paths[i];
    address = path.toAddress();
    addrs.push(address.toBase58(this.network));
  }

  return addrs;
});

RPC.prototype.getbalance = co(function* getbalance(args, help) {
  var wallet = this.wallet;
  var minconf = 0;
  var account, value, balance;

  if (help || args.length > 3)
    throw new RPCError('getbalance ( "account" minconf includeWatchonly )');

  if (args.length >= 1) {
    account = toString(args[0]);

    if (!account)
      account = 'default';

    if (account === '*')
      account = null;
  }

  if (args.length >= 2)
    minconf = toNumber(args[1], 0);

  balance = yield wallet.getBalance(account);

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

  return Amount.btc(value, true);
});

RPC.prototype.getnewaddress = co(function* getnewaddress(args, help) {
  var wallet = this.wallet;
  var account, address;

  if (help || args.length > 1)
    throw new RPCError('getnewaddress ( "account" )');

  if (args.length === 1)
    account = toString(args[0]);

  if (!account)
    account = 'default';

  address = yield wallet.createReceive(account);

  return address.getAddress('base58');
});

RPC.prototype.getrawchangeaddress = co(function* getrawchangeaddress(args, help) {
  var wallet = this.wallet;
  var address;

  if (help || args.length > 1)
    throw new RPCError('getrawchangeaddress');

  address = yield wallet.createChange();

  return address.getAddress('base58');
});

RPC.prototype.getreceivedbyaccount = co(function* getreceivedbyaccount(args, help) {
  var wallet = this.wallet;
  var minconf = 0;
  var total = 0;
  var filter = {};
  var lastConf = -1;
  var i, j, path, wtx, output, conf, hash, account, paths, txs;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('getreceivedbyaccount "account" ( minconf )');

  account = toString(args[0]);

  if (!account)
    account = 'default';

  if (args.length === 2)
    minconf = toNumber(args[1], 0);

  paths = yield wallet.getPaths(account);

  for (i = 0; i < paths.length; i++) {
    path = paths[i];
    filter[path.hash] = true;
  }

  txs = yield wallet.getHistory(account);

  for (i = 0; i < txs.length; i++) {
    wtx = txs[i];

    conf = wtx.getDepth(this.chain.height);

    if (conf < minconf)
      continue;

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

    for (j = 0; j < wtx.tx.outputs.length; j++) {
      output = wtx.tx.outputs[j];
      hash = output.getHash('hex');
      if (hash && filter[hash])
        total += output.value;
    }
  }

  return Amount.btc(total, true);
});

RPC.prototype.getreceivedbyaddress = co(function* getreceivedbyaddress(args, help) {
  var wallet = this.wallet;
  var minconf = 0;
  var total = 0;
  var i, j, hash, wtx, output, txs;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('getreceivedbyaddress "bitcoinaddress" ( minconf )');

  hash = Address.getHash(toString(args[0]), 'hex');

  if (!hash)
    throw new RPCError('Invalid address');

  if (args.length === 2)
    minconf = toNumber(args[1], 0);

  txs = yield wallet.getHistory();

  for (i = 0; i < txs.length; i++) {
    wtx = txs[i];

    if (wtx.getDepth(this.chain.height) < minconf)
      continue;

    for (j = 0; j < wtx.tx.outputs.length; j++) {
      output = wtx.tx.outputs[j];
      if (output.getHash('hex') === hash)
        total += output.value;
    }
  }

  return Amount.btc(total, true);
});

RPC.prototype._toWalletTX = co(function* _toWalletTX(wtx) {
  var wallet = this.wallet;
  var details = yield wallet.toDetails(wtx);
  var det = [];
  var sent = 0;
  var received = 0;
  var receive = true;
  var i, member;

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

  for (i = 0; i < details.inputs.length; i++) {
    member = details.inputs[i];
    if (member.path) {
      receive = false;
      break;
    }
  }

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

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

      det.push({
        account: member.path.name,
        address: member.address.toBase58(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.toBase58(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.ts,
    txid: util.revHex(details.hash),
    walletconflicts: [],
    time: details.ps,
    timereceived: details.ps,
    'bip125-replaceable': 'no',
    details: det,
    hex: details.tx.toRaw().toString('hex')
  };
});

RPC.prototype.gettransaction = co(function* gettransaction(args, help) {
  var wallet = this.wallet;
  var hash, wtx;

  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('gettransaction "txid" ( includeWatchonly )');

  hash = toHash(args[0]);

  if (!hash)
    throw new RPCError('Invalid parameter');

  wtx = yield wallet.getTX(hash);

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

  return yield this._toWalletTX(wtx);
});

RPC.prototype.abandontransaction = co(function* abandontransaction(args, help) {
  var wallet = this.wallet;
  var hash, result;

  if (help || args.length !== 1)
    throw new RPCError('abandontransaction "txid"');

  hash = toHash(args[0]);

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

  result = yield wallet.abandon(hash);

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

  return null;
});

RPC.prototype.getunconfirmedbalance = co(function* getunconfirmedbalance(args, help) {
  var wallet = this.wallet;
  var balance;

  if (help || args.length > 0)
    throw new RPCError('getunconfirmedbalance');

  balance = yield wallet.getBalance();

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

RPC.prototype.getwalletinfo = co(function* getwalletinfo(args, help) {
  var wallet = this.wallet;
  var balance;

  if (help || args.length !== 0)
    throw new RPCError('getwalletinfo');

  balance = yield wallet.getBalance();

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

RPC.prototype.importprivkey = co(function* importprivkey(args, help) {
  var wallet = this.wallet;
  var secret, label, rescan, key;

  if (help || args.length < 1 || args.length > 3)
    throw new RPCError('importprivkey "bitcoinprivkey" ( "label" rescan )');

  secret = toString(args[0]);

  if (args.length > 1)
    label = toString(args[1]);

  if (args.length > 2)
    rescan = toBool(args[2]);

  if (rescan && this.chain.options.prune)
    throw new RPCError('Cannot rescan when pruned.');

  key = KeyRing.fromSecret(secret, this.network);

  yield wallet.importKey(0, key);

  if (rescan)
    yield this.walletdb.rescan(0);

  return null;
});

RPC.prototype.importwallet = co(function* importwallet(args, help) {
  var wallet = this.wallet;
  var file, keys, lines, line, parts;
  var i, secret, time, label, addr;
  var data, key, rescan;

  if (help || args.length !== 1)
    throw new RPCError('importwallet "filename" ( rescan )');

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

  file = toString(args[0]);

  if (args.length > 1)
    rescan = toBool(args[1]);

  if (rescan && this.chain.options.prune)
    throw new RPCError('Cannot rescan when pruned.');

  data = yield readFile(file, 'utf8');

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

  for (i = 0; i < lines.length; i++) {
    line = lines[i].trim();

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

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

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

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

    secret = KeyRing.fromSecret(parts[0], this.network);

    time = +parts[1];
    label = parts[2];
    addr = parts[3];

    keys.push(secret);
  }

  for (i = 0; i < keys.length; i++) {
    key = keys[i];
    yield wallet.importKey(0, key);
  }

  if (rescan)
    yield this.walletdb.rescan(0);

  return null;
});

RPC.prototype.importaddress = co(function* importaddress(args, help) {
  var wallet = this.wallet;
  var addr, label, rescan, p2sh;

  if (help || args.length < 1 || args.length > 4)
    throw new RPCError('importaddress "address" ( "label" rescan p2sh )');

  addr = toString(args[0]);

  if (args.length > 1)
    label = toString(args[1]);

  if (args.length > 2)
    rescan = toBool(args[2]);

  if (args.length > 3)
    p2sh = toBool(args[3]);

  if (rescan && this.chain.options.prune)
    throw new RPCError('Cannot rescan when pruned.');

  addr = Address.fromBase58(addr, this.network);

  yield wallet.importAddress(0, addr);

  if (rescan)
    yield this.walletdb.rescan(0);

  return null;
});

RPC.prototype.importpubkey = co(function* importpubkey(args, help) {
  var wallet = this.wallet;
  var pubkey, label, rescan, key;

  if (help || args.length < 1 || args.length > 4)
    throw new RPCError('importpubkey "pubkey" ( "label" rescan )');

  pubkey = toString(args[0]);

  if (!util.isHex(pubkey))
    throw new RPCError('Invalid parameter.');

  if (args.length > 1)
    label = toString(args[1]);

  if (args.length > 2)
    rescan = toBool(args[2]);

  if (rescan && this.chain.options.prune)
    throw new RPCError('Cannot rescan when pruned.');

  pubkey = new Buffer(pubkey, 'hex');

  key = KeyRing.fromPublic(pubkey, this.network);

  yield wallet.importKey(0, key);

  if (rescan)
    yield this.walletdb.rescan(0);

  return null;
});

RPC.prototype.keypoolrefill = co(function* keypoolrefill(args, help) {
  if (help || args.length > 1)
    throw new RPCError('keypoolrefill ( newsize )');
  return null;
});

RPC.prototype.listaccounts = co(function* listaccounts(args, help) {
  var wallet = this.wallet;
  var i, map, accounts, account, balance;

  if (help || args.length > 2)
    throw new RPCError('listaccounts ( minconf includeWatchonly)');

  map = {};
  accounts = yield wallet.getAccounts();

  for (i = 0; i < accounts.length; i++) {
    account = accounts[i];
    balance = yield wallet.getBalance(account);
    map[account] = Amount.btc(balance.unconfirmed, true);
  }

  return map;
});

RPC.prototype.listaddressgroupings = co(function* listaddressgroupings(args, help) {
  if (help)
    throw new RPCError('listaddressgroupings');
  throw new Error('Not implemented.');
});

RPC.prototype.listlockunspent = co(function* listlockunspent(args, help) {
  var wallet = this.wallet;
  var i, outpoints, outpoint, out;

  if (help || args.length > 0)
    throw new RPCError('listlockunspent');

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

  for (i = 0; i < outpoints.length; i++) {
    outpoint = outpoints[i];
    out.push({
      txid: outpoint.txid(),
      vout: outpoint.index
    });
  }

  return out;
});

RPC.prototype.listreceivedbyaccount = co(function* listreceivedbyaccount(args, help) {
  var minconf = 0;
  var includeEmpty = false;

  if (help || args.length > 3) {
    throw new RPCError('listreceivedbyaccount'
      + ' ( minconf includeempty includeWatchonly )');
  }

  if (args.length > 0)
    minconf = toNumber(args[0], 0);

  if (args.length > 1)
    includeEmpty = toBool(args[1], false);

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

RPC.prototype.listreceivedbyaddress = co(function* listreceivedbyaddress(args, help) {
  var minconf = 0;
  var includeEmpty = false;

  if (help || args.length > 3) {
    throw new RPCError('listreceivedbyaddress'
      + ' ( minconf includeempty includeWatchonly )');
  }

  if (args.length > 0)
    minconf = toNumber(args[0], 0);

  if (args.length > 1)
    includeEmpty = toBool(args[1], false);

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

RPC.prototype._listReceived = co(function* _listReceived(minconf, empty, account) {
  var wallet = this.wallet;
  var out = [];
  var result = [];
  var map = {};
  var paths = yield wallet.getPaths();
  var i, j, path, wtx, output, conf, hash;
  var entry, address, keys, key, item, txs;

  for (i = 0; i < paths.length; i++) {
    path = paths[i];
    address = path.toAddress();
    map[path.hash] = {
      involvesWatchonly: wallet.watchOnly,
      address: address.toBase58(this.network),
      account: path.name,
      amount: 0,
      confirmations: -1,
      label: '',
    };
  }

  txs = yield wallet.getHistory();

  for (i = 0; i < txs.length; i++) {
    wtx = txs[i];

    conf = wtx.getDepth(this.chain.height);

    if (conf < minconf)
      continue;

    for (j = 0; j < wtx.tx.outputs.length; j++) {
      output = wtx.tx.outputs[j];
      address = output.getAddress();

      if (!address)
        continue;

      hash = address.getHash('hex');
      entry = map[hash];

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

  keys = Object.keys(map);

  for (i = 0; i < keys.length; i++) {
    key = keys[i];
    entry = map[key];
    out.push(entry);
  }

  if (account) {
    map = {};

    for (i = 0; i < out.length; i++) {
      entry = out[i];
      item = map[entry.account];
      if (!item) {
        map[entry.account] = entry;
        entry.address = undefined;
        continue;
      }
      item.amount += entry.amount;
    }

    out = [];
    keys = Object.keys(map);

    for (i = 0; i < keys.length; i++) {
      key = keys[i];
      entry = map[key];
      out.push(entry);
    }
  }

  for (i = 0; i < out.length; i++) {
    entry = out[i];

    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;
});

RPC.prototype.listsinceblock = co(function* listsinceblock(args, help) {
  var wallet = this.wallet;
  var minconf = 0;
  var out = [];
  var i, block, highest, height;
  var txs, wtx, json;

  if (help) {
    throw new RPCError('listsinceblock'
      + ' ( "blockhash" target-confirmations includeWatchonly)');
  }

  if (args.length > 0) {
    block = toHash(args[0]);
    if (!block)
      throw new RPCError('Invalid parameter.');
  }

  if (args.length > 1)
    minconf = toNumber(args[1], 0);

  height = yield this.chain.db.getHeight(block);

  if (height === -1)
    height = this.chain.height;

  txs = yield wallet.getHistory();

  for (i = 0; i < txs.length; i++) {
    wtx = txs[i];

    if (wtx.height < height)
      continue;

    if (wtx.getDepth(this.chain.height) < minconf)
      continue;

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

    json = yield this._toListTX(wtx);

    out.push(json);
  }

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

RPC.prototype._toListTX = co(function* _toListTX(wtx) {
  var wallet = this.wallet;
  var sent = 0;
  var received = 0;
  var receive = true;
  var sendMember, recMember, sendIndex, recIndex;
  var i, member, index;
  var details = yield wallet.toDetails(wtx);

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

  for (i = 0; i < details.inputs.length; i++) {
    member = details.inputs[i];
    if (member.path) {
      receive = false;
      break;
    }
  }

  for (i = 0; i < details.outputs.length; i++) {
    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;
  }

  if (receive) {
    member = recMember;
    index = recIndex;
  } else {
    member = sendMember;
    index = sendIndex;
  }

  // In the odd case where we send to ourselves.
  if (!member) {
    assert(!receive);
    member = recMember;
    index = recIndex;
  }

  return {
    account: member.path ? member.path.name : '',
    address: member.address
      ? member.address.toBase58(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(),
    blockhash: details.block ? util.revHex(details.block) : null,
    blockindex: details.index,
    blocktime: details.ts,
    txid: util.revHex(details.hash),
    walletconflicts: [],
    time: details.ps,
    timereceived: details.ps,
    'bip125-replaceable': 'no'
  };
});

RPC.prototype.listtransactions = co(function* listtransactions(args, help) {
  var wallet = this.wallet;
  var account = null;
  var count = 10;
  var i, txs, wtx, json;

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

  if (args.length > 0) {
    account = toString(args[0]);
    if (!account)
      account = 'default';
  }

  if (args.length > 1) {
    count = toNumber(args[1], 10);
    if (count < 0)
      count = 10;
  }

  txs = yield wallet.getHistory();

  sortTX(txs);

  for (i = 0; i < txs.length; i++) {
    wtx = txs[i];
    json = yield this._toListTX(wtx);
    txs[i] = json;
  }

  return txs;
});

RPC.prototype.listunspent = co(function* listunspent(args, help) {
  var wallet = this.wallet;
  var minDepth = 1;
  var maxDepth = 9999999;
  var out = [];
  var i, addresses, addrs, depth, address, hash, coins, coin, ring;

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

  if (args.length > 0)
    minDepth = toNumber(args[0], 1);

  if (args.length > 1)
    maxDepth = toNumber(args[1], maxDepth);

  if (args.length > 2)
    addrs = toArray(args[2]);

  if (addrs) {
    addresses = {};
    for (i = 0; i < addrs.length; i++) {
      address = toString(addrs[i]);
      hash = Address.getHash(address, 'hex');

      if (!hash)
        throw new RPCError('Invalid address.');

      if (addresses[hash])
        throw new RPCError('Duplicate address.');

      addresses[hash] = true;
    }
  }

  coins = yield wallet.getCoins();

  sortCoins(coins);

  for (i = 0; i < coins.length; i++ ) {
    coin = coins[i];
    depth = coin.getDepth(this.chain.height);

    if (!(depth >= minDepth && depth <= maxDepth))
      continue;

    address = coin.getAddress();

    if (!address)
      continue;

    hash = coin.getHash('hex');

    if (addresses) {
      if (!hash || !addresses[hash])
        continue;
    }

    ring = yield wallet.getKey(hash);

    out.push({
      txid: coin.txid(),
      vout: coin.index,
      address: address ? address.toBase58(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;
});

RPC.prototype.lockunspent = co(function* lockunspent(args, help) {
  var wallet = this.wallet;
  var i, unlock, outputs, output, outpoint;

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

  unlock = toBool(args[0]);

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

  outputs = toArray(args[1]);

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

  for (i = 0; i < outputs.length; i++) {
    output = outputs[i];

    if (!output || typeof output !== 'object')
      throw new RPCError('Invalid parameter.');

    outpoint = new Outpoint();
    outpoint.hash = toHash(output.txid);
    outpoint.index = toNumber(output.vout);

    if (!outpoint.hash)
      throw new RPCError('Invalid parameter.');

    if (outpoint.index < 0)
      throw new RPCError('Invalid parameter.');

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

    wallet.lockCoin(outpoint);
  }

  return true;
});

RPC.prototype.move = co(function* move(args, help) {
  // Not implementing: stupid and deprecated.
  throw new Error('Not implemented.');
});

RPC.prototype._send = co(function* _send(account, address, amount, subtractFee) {
  var wallet = this.wallet;
  var tx, options;

  options = {
    account: account,
    subtractFee: subtractFee,
    rate: this.feeRate,
    outputs: [{
      address: address,
      value: amount
    }]
  };

  tx = yield wallet.send(options);

  return tx.txid();
});

RPC.prototype.sendfrom = co(function* sendfrom(args, help) {
  var account, address, amount;

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

  account = toString(args[0]);
  address = Address.fromBase58(toString(args[1]), this.network);
  amount = toSatoshi(args[2]);

  if (!account)
    account = 'default';

  return yield this._send(account, address, amount, false);
});

RPC.prototype.sendmany = co(function* sendmany(args, help) {
  var wallet = this.wallet;
  var minconf = 1;
  var outputs = [];
  var uniq = {};
  var account, sendTo, comment, subtractFee;
  var i, keys, tx, key, value, address;
  var hash, output, options;

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

  account = toString(args[0]);
  sendTo = toObject(args[1]);

  if (!account)
    account = 'default';

  if (!sendTo)
    throw new RPCError('Invalid parameter.');

  if (args.length > 2)
    minconf = toNumber(args[2], 1);

  if (args.length > 3)
    comment = toString(args[3]);

  if (args.length > 4) {
    subtractFee = args[4];
    if (typeof subtractFee !== 'boolean') {
      if (!util.isNumber(subtractFee))
        throw new RPCError('Invalid parameter.');
    }
  }

  keys = Object.keys(sendTo);

  for (i = 0; i < keys.length; i++) {
    key = keys[i];
    value = toSatoshi(sendTo[key]);
    address = Address.fromBase58(key, this.network);
    hash = address.getHash('hex');

    if (uniq[hash])
      throw new RPCError('Invalid parameter.');

    uniq[hash] = true;

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

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

  tx = yield wallet.send(options);

  return tx.txid();
});

RPC.prototype.sendtoaddress = co(function* sendtoaddress(args, help) {
  var address, amount, subtractFee;

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

  address = Address.fromBase58(toString(args[0]), this.network);
  amount = toSatoshi(args[1]);
  subtractFee = toBool(args[4]);

  return yield this._send(null, address, amount, subtractFee);
});

RPC.prototype.setaccount = co(function* setaccount(args, help) {
  if (help || args.length < 1 || args.length > 2)
    throw new RPCError('setaccount "bitcoinaddress" "account"');

  // Impossible to implement in bcoin:
  throw new Error('Not implemented.');
});

RPC.prototype.settxfee = co(function* settxfee(args, help) {
  if (help || args.length < 1 || args.length > 1)
    throw new RPCError('settxfee amount');

  this.feeRate = toSatoshi(args[0]);

  return true;
});

RPC.prototype.signmessage = co(function* signmessage(args, help) {
  var wallet = this.wallet;
  var address, msg, sig, ring;

  if (help || args.length !== 2)
    throw new RPCError('signmessage "bitcoinaddress" "message"');

  address = toString(args[0]);
  msg = toString(args[1]);

  address = Address.getHash(address, 'hex');

  if (!address)
    throw new RPCError('Invalid address.');

  ring = yield wallet.getKey(address);

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

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

  msg = new Buffer(RPC.magic + msg, 'utf8');
  msg = crypto.hash256(msg);

  sig = ring.sign(msg);

  return sig.toString('base64');
});

RPC.prototype.walletlock = co(function* walletlock(args, help) {
  var wallet = this.wallet;

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

  if (!wallet.master.encrypted)
    throw new RPCError('Wallet is not encrypted.');

  yield wallet.lock();

  return null;
});

RPC.prototype.walletpassphrasechange = co(function* walletpassphrasechange(args, help) {
  var wallet = this.wallet;
  var old, new_;

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

  if (!wallet.master.encrypted)
    throw new RPCError('Wallet is not encrypted.');

  old = toString(args[0]);
  new_ = toString(args[1]);

  if (old.length < 1 || new_.length < 1)
    throw new RPCError('Invalid parameter');

  yield wallet.setPassphrase(old, new_);

  return null;
});

RPC.prototype.walletpassphrase = co(function* walletpassphrase(args, help) {
  var wallet = this.wallet;
  var passphrase, timeout;

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

  if (!wallet.master.encrypted)
    throw new RPCError('Wallet is not encrypted.');

  passphrase = toString(args[0]);
  timeout = toNumber(args[1]);

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

  if (timeout < 0)
    throw new RPCError('Invalid parameter');

  yield wallet.unlock(passphrase, timeout);

  return null;
});

RPC.prototype.importprunedfunds = co(function* importprunedfunds(args, help) {
  var tx, block, hash, label, height;

  if (help || args.length < 2 || args.length > 3) {
    throw new RPCError('importprunedfunds'
      + ' "rawtransaction" "txoutproof" ( "label" )');
  }

  tx = args[0];
  block = args[1];

  if (!util.isHex(tx) || !util.isHex(block))
    throw new RPCError('Invalid parameter.');

  tx = TX.fromRaw(tx, 'hex');
  block = MerkleBlock.fromRaw(block, 'hex');
  hash = block.hash('hex');

  if (args.length === 3)
    label = toString(args[2]);

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

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

  height = yield this.chain.db.getHeight(hash);

  if (height === -1)
    throw new RPCError('Invalid proof.');

  block = {
    hash: hash,
    ts: block.ts,
    height: height
  };

  if (!(yield this.walletdb.addTX(tx, block)))
    throw new RPCError('No tracked address for TX.');

  return null;
});

RPC.prototype.removeprunedfunds = co(function* removeprunedfunds(args, help) {
  var wallet = this.wallet;
  var hash;

  if (help || args.length !== 1)
    throw new RPCError('removeprunedfunds "txid"');

  hash = toHash(args[0]);

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

  if (!(yield wallet.remove(hash)))
    throw new RPCError('Transaction not in wallet.');

  return null;
});

RPC.prototype.getmemory = co(function* getmemory(args, help) {
  var mem;

  if (help || args.length !== 0)
    throw new RPCError('getmemory');

  if (!process.memoryUsage)
    return {};

  mem = process.memoryUsage();

  return {
    rss: util.mb(mem.rss),
    jsheap: util.mb(mem.heapUsed),
    jsheaptotal: util.mb(mem.heapTotal),
    nativeheap: util.mb(mem.rss - mem.heapTotal)
  };
});

RPC.prototype.selectwallet = co(function* selectwallet(args, help) {
  var id, wallet;

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

  id = toString(args[0]);
  wallet = yield this.walletdb.get(id);

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

  this.wallet = wallet;

  return null;
});

RPC.prototype.setloglevel = co(function* setloglevel(args, help) {
  var name, level;

  if (help || args.length !== 1)
    throw new RPCError('setloglevel "level"');

  name = toString(args[0]);
  level = Logger.levels[name];

  if (level == null)
    throw new RPCError('Bad log level.');

  this.logger.level = level;

  return null;
});

/*
 * Helpers
 */

function RPCError(msg) {
  Error.call(this);

  if (Error.captureStackTrace)
    Error.captureStackTrace(this, RPCError);

  this.type = 'RPCError';
  this.message = msg;
}

util.inherits(RPCError, Error);

function toBool(obj, def) {
  if (typeof obj === 'boolean' || typeof obj === 'number')
    return !!obj;
  return def || false;
}

function toNumber(obj, def) {
  if (util.isNumber(obj))
    return obj;
  return def != null ? def : -1;
}

function toString(obj, def) {
  if (typeof obj === 'string')
    return obj;
  return def != null ? def : '';
}

function toArray(obj, def) {
  if (Array.isArray(obj))
    return obj;
  return def != null ? def : null;
}

function toObject(obj, def) {
  if (obj && typeof obj === 'object')
    return obj;
  return def != null ? def : null;
}

function toHash(obj) {
  if (!isHash(obj))
    return null;
  return util.revHex(obj);
}

function isHash(obj) {
  return util.isHex(obj) && obj.length === 64;
}

function toSatoshi(obj) {
  if (typeof obj !== 'number')
    throw new RPCError('Bad BTC amount.');
  return Amount.value(obj, true);
}

function reverseEndian(data) {
  var i, field;
  for (i = 0; i < data.length; i += 4) {
    field = data.readUInt32LE(i, true);
    data.writeUInt32BE(field, i, true);
  }
}

function writeFile(file, data) {
  return new Promise(function(resolve, reject) {
    fs.writeFile(file, data, co.wrap(resolve, reject));
  });
}

function readFile(file, enc) {
  return new Promise(function(resolve, reject) {
    fs.readFile(file, enc, co.wrap(resolve, reject));
  });
}

function sortTX(txs) {
  return txs.sort(function(a, b) {
    return a.ps - b.ps;
  });
}

function sortCoins(coins) {
  return coins.sort(function(a, b) {
    a = a.height === -1 ? 0x7fffffff : a.height;
    b = b.height === -1 ? 0x7fffffff : b.height;
    return a - b;
  });
}

/*
 * Expose
 */

module.exports = RPC;
