Source: node/rpc.js

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

'use strict';

const assert = require('bsert');
const bweb = require('bweb');
const {Lock} = require('bmutex');
const IP = require('binet');
const Validator = require('bval');
const {BufferMap, BufferSet} = require('buffer-map');
const hash160 = require('bcrypto/lib/hash160');
const {safeEqual} = require('bcrypto/lib/safe');
const secp256k1 = require('bcrypto/lib/secp256k1');
const util = require('../utils/util');
const messageUtil = require('../utils/message');
const common = require('../blockchain/common');
const Amount = require('../btc/amount');
const NetAddress = require('../net/netaddress');
const Script = require('../script/script');
const Address = require('../primitives/address');
const Block = require('../primitives/block');
const Headers = require('../primitives/headers');
const Input = require('../primitives/input');
const KeyRing = require('../primitives/keyring');
const MerkleBlock = require('../primitives/merkleblock');
const MTX = require('../primitives/mtx');
const Network = require('../protocol/network');
const Outpoint = require('../primitives/outpoint');
const Output = require('../primitives/output');
const TX = require('../primitives/tx');
const consensus = require('../protocol/consensus');
const pkg = require('../pkg');
const RPCBase = bweb.RPC;
const RPCError = bweb.RPCError;

/*
 * Constants
 */

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

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

  // P2P client errors
  CLIENT_NOT_CONNECTED: -9,
  CLIENT_IN_INITIAL_DOWNLOAD: -10,
  CLIENT_NODE_ALREADY_ADDED: -23,
  CLIENT_NODE_NOT_ADDED: -24,
  CLIENT_NODE_NOT_CONNECTED: -29,
  CLIENT_INVALID_IP_OR_SUBNET: -30,
  CLIENT_P2P_DISABLED: -31
};

/**
 * Bitcoin RPC
 * @alias module:http.RPC
 * @extends bweb.RPC
 */

class RPC extends RPCBase {
  /**
   * Create RPC.
   * @param {Node} node
   */

  constructor(node) {
    super();

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

    this.node = node;
    this.network = node.network;
    this.workers = node.workers;
    this.chain = node.chain;
    this.mempool = node.mempool;
    this.pool = node.pool;
    this.fees = node.fees;
    this.miner = node.miner;
    this.logger = node.logger.context('node-rpc');
    this.locker = new Lock();

    this.mining = false;
    this.procLimit = 0;
    this.attempt = null;
    this.lastActivity = 0;
    this.boundChain = false;
    this.nonce1 = 0;
    this.nonce2 = 0;
    this.merkleMap = new BufferMap();
    this.pollers = [];

    this.init();
  }

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

  handleCall(cmd, query) {
    if (cmd.method !== 'getwork'
        && cmd.method !== 'getblocktemplate'
        && cmd.method !== 'getbestblockhash') {
      this.logger.debug('Handling RPC call: %s.', cmd.method);
      if (cmd.method !== 'submitblock'
          && cmd.method !== 'getmemorypool') {
        this.logger.debug(cmd.params);
      }
    }

    if (cmd.method === 'getwork') {
      if (query.longpoll)
        cmd.method = 'getworklp';
    }
  }

  init() {
    this.add('stop', this.stop);
    this.add('help', this.help);

    this.add('getblockchaininfo', this.getBlockchainInfo);
    this.add('getbestblockhash', this.getBestBlockHash);
    this.add('getblockcount', this.getBlockCount);
    this.add('getblock', this.getBlock);
    this.add('getblockbyheight', this.getBlockByHeight);
    this.add('getblockhash', this.getBlockHash);
    this.add('getblockheader', this.getBlockHeader);
    this.add('getblockfilter', this.getBlockFilter);
    this.add('getchaintips', this.getChainTips);
    this.add('getdifficulty', this.getDifficulty);
    this.add('getmempoolancestors', this.getMempoolAncestors);
    this.add('getmempooldescendants', this.getMempoolDescendants);
    this.add('getmempoolentry', this.getMempoolEntry);
    this.add('getmempoolinfo', this.getMempoolInfo);
    this.add('getrawmempool', this.getRawMempool);
    this.add('gettxout', this.getTXOut);
    this.add('gettxoutsetinfo', this.getTXOutSetInfo);
    this.add('pruneblockchain', this.pruneBlockchain);
    this.add('verifychain', this.verifyChain);

    this.add('invalidateblock', this.invalidateBlock);
    this.add('reconsiderblock', this.reconsiderBlock);

    this.add('getnetworkhashps', this.getNetworkHashPS);
    this.add('getmininginfo', this.getMiningInfo);
    this.add('prioritisetransaction', this.prioritiseTransaction);
    this.add('getwork', this.getWork);
    this.add('getworklp', this.getWorkLongpoll);
    this.add('getblocktemplate', this.getBlockTemplate);
    this.add('submitblock', this.submitBlock);
    this.add('verifyblock', this.verifyBlock);

    this.add('setgenerate', this.setGenerate);
    this.add('getgenerate', this.getGenerate);
    this.add('generate', this.generate);
    this.add('generatetoaddress', this.generateToAddress);

    this.add('estimatefee', this.estimateFee);
    this.add('estimatepriority', this.estimatePriority);
    this.add('estimatesmartfee', this.estimateSmartFee);
    this.add('estimatesmartpriority', this.estimateSmartPriority);

    this.add('getinfo', this.getInfo);
    this.add('validateaddress', this.validateAddress);
    this.add('createmultisig', this.createMultisig);
    this.add('createwitnessaddress', this.createWitnessAddress);
    this.add('verifymessage', this.verifyMessage);
    this.add('signmessagewithprivkey', this.signMessageWithPrivkey);

    this.add('setmocktime', this.setMockTime);

    this.add('getconnectioncount', this.getConnectionCount);
    this.add('ping', this.ping);
    this.add('getpeerinfo', this.getPeerInfo);
    this.add('addnode', this.addNode);
    this.add('disconnectnode', this.disconnectNode);
    this.add('getaddednodeinfo', this.getAddedNodeInfo);
    this.add('getnettotals', this.getNetTotals);
    this.add('getnetworkinfo', this.getNetworkInfo);
    this.add('setban', this.setBan);
    this.add('listbanned', this.listBanned);
    this.add('clearbanned', this.clearBanned);

    this.add('getrawtransaction', this.getRawTransaction);
    this.add('createrawtransaction', this.createRawTransaction);
    this.add('decoderawtransaction', this.decodeRawTransaction);
    this.add('decodescript', this.decodeScript);
    this.add('sendrawtransaction', this.sendRawTransaction);
    this.add('signrawtransaction', this.signRawTransaction);

    this.add('gettxoutproof', this.getTXOutProof);
    this.add('verifytxoutproof', this.verifyTXOutProof);

    this.add('getmemoryinfo', this.getMemoryInfo);
    this.add('setloglevel', this.setLogLevel);
  }

  /*
   * Overall control/query calls
   */

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

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

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

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

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

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

    this.node.close().catch((err) => {
      setImmediate(() => {
        throw err;
      });
    });

    return 'Stopping.';
  }

  /*
   * P2P networking
   */

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

    const hosts = this.pool.hosts;
    const locals = [];

    for (const local of hosts.local.values()) {
      locals.push({
        address: local.addr.host,
        port: local.addr.port,
        score: local.score
      });
    }

    return {
      version: pkg.version,
      subversion: this.pool.options.agent,
      protocolversion: this.pool.options.version,
      localservices: hex32(this.pool.options.services),
      localservicenames: this.pool.getServiceNames(),
      localrelay: !this.pool.options.noRelay,
      timeoffset: this.network.time.offset,
      networkactive: this.pool.connected,
      connections: this.pool.peers.size(),
      networks: [],
      relayfee: Amount.btc(this.network.minRelay, true),
      incrementalfee: 0,
      localaddresses: locals,
      warnings: ''
    };
  }

  async addNode(args, help) {
    if (help || args.length !== 2)
      throw new RPCError(errs.MISC_ERROR, 'addnode "node" "add|remove|onetry"');

    const valid = new Validator(args);
    const node = valid.str(0, '');
    const cmd = valid.str(1, '');

    switch (cmd) {
      case 'add': {
        this.pool.hosts.addNode(node);
        ; // fall through
      }
      case 'onetry': {
        const addr = parseNetAddress(node, this.network);

        if (!this.pool.peers.get(addr.hostname)) {
          const peer = this.pool.createOutbound(addr);
          this.pool.peers.add(peer);
        }

        break;
      }
      case 'remove': {
        this.pool.hosts.removeNode(node);
        break;
      }
    }

    return null;
  }

  async disconnectNode(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'disconnectnode "node"');

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

    const addr = parseIP(str, this.network);
    const peer = this.pool.peers.get(addr.hostname);

    if (peer)
      peer.destroy();

    return null;
  }

  async getAddedNodeInfo(args, help) {
    if (help || args.length > 1)
      throw new RPCError(errs.MISC_ERROR, 'getaddednodeinfo ( "node" )');

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

    let target;
    if (args.length === 1)
      target = parseIP(addr, this.network);

    const result = [];

    for (const node of hosts.nodes) {
      if (target) {
        if (node.host !== target.host)
          continue;

        if (node.port !== target.port)
          continue;
      }

      const peer = this.pool.peers.get(node.hostname);

      if (!peer || !peer.connected) {
        result.push({
          addednode: node.hostname,
          connected: false,
          addresses: []
        });
        continue;
      }

      result.push({
        addednode: node.hostname,
        connected: peer.connected,
        addresses: [
          {
            address: peer.hostname(),
            connected: peer.outbound
              ? 'outbound'
              : 'inbound'
          }
        ]
      });
    }

    if (target && result.length === 0) {
      throw new RPCError(errs.CLIENT_NODE_NOT_ADDED,
        'Node has not been added.');
    }

    return result;
  }

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

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

  async getNetTotals(args, help) {
    let sent = 0;
    let recv = 0;

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

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

    return {
      totalbytesrecv: recv,
      totalbytessent: sent,
      timemillis: Date.now()
    };
  }

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

    const peers = [];

    for (let peer = this.pool.peers.head(); peer; peer = peer.next) {
      const offset = this.network.time.known.get(peer.hostname()) || 0;
      const hashes = [];

      for (const hash in peer.blockMap.keys()) {
        const str = util.revHex(hash);
        hashes.push(str);
      }

      peer.getName();

      peers.push({
        id: peer.id,
        addr: peer.hostname(),
        addrlocal: !peer.local.isNull()
          ? peer.local.hostname
          : undefined,
        name: peer.name || undefined,
        services: hex32(peer.services),
        servicenames: peer.getServiceNames(),
        relaytxes: !peer.noRelay,
        lastsend: peer.lastSend / 1000 | 0,
        lastrecv: peer.lastRecv / 1000 | 0,
        bytessent: peer.socket.bytesWritten,
        bytesrecv: peer.socket.bytesRead,
        conntime: peer.time !== 0 ? (Date.now() - peer.time) / 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: hashes,
        whitelisted: false
      });
    }

    return peers;
  }

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

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

    return null;
  }

  async setBan(args, help) {
    const valid = new Validator(args);
    const str = valid.str(0, '');
    const action = valid.str(1, '');

    if (help
        || args.length < 2
        || (action !== 'add' && action !== 'remove')) {
      throw new RPCError(errs.MISC_ERROR,
        'setban "ip(/netmask)" "add|remove" (bantime) (absolute)');
    }

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

    switch (action) {
      case 'add':
        this.pool.ban(addr);
        break;
      case 'remove':
        this.pool.unban(addr);
        break;
    }

    return null;
  }

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

    const banned = [];

    for (const [host, time] of this.pool.hosts.banned) {
      banned.push({
        address: host,
        banned_until: time + this.pool.options.banTime,
        ban_created: time,
        ban_reason: ''
      });
    }

    return banned;
  }

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

    this.pool.hosts.clearBanned();

    return null;
  }

  /* Block chain and UTXO */
  async getBlockchainInfo(args, help) {
    if (help || args.length !== 0)
      throw new RPCError(errs.MISC_ERROR, 'getblockchaininfo');

    return {
      chain: this.network.type !== 'testnet'
        ? this.network.type
        : 'test',
      blocks: this.chain.height,
      headers: this.chain.height,
      bestblockhash: this.chain.tip.rhash(),
      difficulty: toDifficulty(this.chain.tip.bits),
      mediantime: await this.chain.getMedianTime(this.chain.tip),
      verificationprogress: this.chain.getProgress(),
      chainwork: this.chain.tip.chainwork.toString('hex', 64),
      pruned: this.chain.options.prune,
      softforks: this.getSoftforks(),
      bip9_softforks: await this.getBIP9Softforks(),
      pruneheight: this.chain.options.prune
        ? Math.max(0, this.chain.height - this.network.block.keepBlocks)
        : null
    };
  }

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

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

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

    return this.chain.tip.height;
  }

  async getBlock(args, help) {
    if (help || args.length < 1 || args.length > 3)
      throw new RPCError(errs.MISC_ERROR, 'getblock "hash" ( verbose )');

    const valid = new Validator(args);
    const hash = valid.brhash(0);
    const verbose = valid.bool(1, true);
    const details = valid.bool(2, false);

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

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

    if (!entry)
      throw new RPCError(errs.MISC_ERROR, 'Block not found.');

    const block = await this.chain.getBlock(entry.hash);

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

      if (this.chain.options.prune) {
        throw new RPCError(errs.MISC_ERROR,
          'Block not available (pruned data)');
      }

      throw new RPCError(errs.MISC_ERROR, 'Can\'t read block from disk');
    }

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

    return await this.blockToJSON(entry, block, details);
  }

  async getBlockByHeight(args, help) {
    if (help || args.length < 1 || args.length > 3) {
      throw new RPCError(errs.MISC_ERROR,
        'getblockbyheight "height" ( verbose )');
    }

    const valid = new Validator(args);
    const height = valid.u32(0, -1);
    const verbose = valid.bool(1, true);
    const details = valid.bool(2, false);

    if (height === -1)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid block height.');

    const entry = await this.chain.getEntry(height);

    if (!entry)
      throw new RPCError(errs.MISC_ERROR, 'Block not found.');

    const block = await this.chain.getBlock(entry.hash);

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

      if (this.chain.options.prune) {
        throw new RPCError(errs.MISC_ERROR,
          'Block not available (pruned data)');
      }

      throw new RPCError(errs.DATABASE_ERROR, 'Can\'t read block from disk');
    }

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

    return await this.blockToJSON(entry, block, details);
  }

  async getBlockHash(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'getblockhash index');

    const valid = new Validator(args);
    const height = valid.u32(0);

    if (height == null || height > this.chain.height)
      throw new RPCError(errs.INVALID_PARAMETER, 'Block height out of range.');

    const hash = await this.chain.getHash(height);

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

    return util.revHex(hash);
  }

  async getBlockHeader(args, help) {
    if (help || args.length < 1 || args.length > 2)
      throw new RPCError(errs.MISC_ERROR, 'getblockheader "hash" ( verbose )');

    const valid = new Validator(args);
    const hash = valid.brhash(0);
    const verbose = valid.bool(1, true);

    if (!hash)
      throw new RPCError(errs.MISC_ERROR, 'Invalid block hash.');

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

    if (!entry)
      throw new RPCError(errs.MISC_ERROR, 'Block not found.');

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

    return await this.headerToJSON(entry);
  }

  async getBlockFilter(args, help) {
    if (help || args.length < 1 || args.length > 2)
      throw new RPCError(errs.MISC_ERROR, 'getblockfilter "hash"');

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

    if (!hash)
      throw new RPCError(errs.MISC_ERROR, 'Invalid block hash.');

    const filter = await this.node.getBlockFilter(hash);

    if (!filter)
      throw new RPCError(errs.MISC_ERROR, 'Block filter not found.');

    return filter.toJSON();
  }

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

    const tips = await this.chain.getTips();
    const result = [];

    for (const hash of tips) {
      const entry = await this.chain.getEntry(hash);

      assert(entry);

      const fork = await this.findFork(entry);
      const main = await this.chain.isMainChain(entry);

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

    return result;
  }

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

    return toDifficulty(this.chain.tip.bits);
  }

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

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

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

  async getMempoolAncestors(args, help) {
    if (help || args.length < 1 || args.length > 2)
      throw new RPCError(errs.MISC_ERROR, 'getmempoolancestors txid (verbose)');

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

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

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

    const entry = this.mempool.getEntry(hash);

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

    const entries = this.mempool.getAncestors(entry);
    const out = [];

    if (verbose) {
      for (const entry of entries)
        out.push(this.entryToJSON(entry));
    } else {
      for (const entry of entries)
        out.push(entry.txid());
    }

    return out;
  }

  async getMempoolDescendants(args, help) {
    if (help || args.length < 1 || args.length > 2) {
      throw new RPCError(errs.MISC_ERROR,
        'getmempooldescendants txid (verbose)');
    }

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

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

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

    const entry = this.mempool.getEntry(hash);

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

    const entries = this.mempool.getDescendants(entry);
    const out = [];

    if (verbose) {
      for (const entry of entries)
        out.push(this.entryToJSON(entry));
    } else {
      for (const entry of entries)
        out.push(entry.txid());
    }

    return out;
  }

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

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

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

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

    const entry = this.mempool.getEntry(hash);

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

    return this.entryToJSON(entry);
  }

  async getRawMempool(args, help) {
    if (help || args.length > 1)
      throw new RPCError(errs.MISC_ERROR, 'getrawmempool ( verbose )');

    const valid = new Validator(args);
    const verbose = valid.bool(0, false);

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

    if (verbose) {
      const out = {};

      for (const entry of this.mempool.map.values())
        out[entry.txid()] = this.entryToJSON(entry);

      return out;
    }

    const hashes = this.mempool.getSnapshot();

    return hashes.map(util.revHex);
  }

  async getTXOut(args, help) {
    if (help || args.length < 2 || args.length > 3) {
      throw new RPCError(errs.MISC_ERROR,
        'gettxout "txid" n ( includemempool )');
    }

    const valid = new Validator(args);
    const hash = valid.brhash(0);
    const index = valid.u32(1);
    const mempool = valid.bool(2, true);

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

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

    if (!hash || index == null)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid outpoint.');

    let coin;
    if (mempool) {
      if (!this.mempool)
        throw new RPCError(errs.MISC_ERROR, 'No mempool available.');
      coin = this.mempool.getCoin(hash, index);
    }

    if (!coin)
      coin = await this.chain.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
    };
  }

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

    const valid = new Validator(args);
    const txids = valid.array(0);
    const hash = valid.brhash(1);

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

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

    if (!txids || txids.length === 0)
      throw new RPCError(errs.INVALID_PARAMETER, 'Invalid TXIDs.');

    const items = new Validator(txids);
    const set = new BufferSet();
    const hashes = [];

    let last = null;

    for (let i = 0; i < txids.length; i++) {
      const hash = items.brhash(i);

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

      if (set.has(hash))
        throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate txid.');

      set.add(hash);
      hashes.push(hash);

      last = hash;
    }

    let block = null;

    if (hash) {
      block = await this.chain.getBlock(hash);
    } else if (await this.node.hasTX(last)) {
      const tx = await this.node.getMeta(last);
      if (tx)
        block = await this.chain.getBlock(tx.block);
    } else {
      const coin = await this.chain.getCoin(last, 0);
      if (coin)
        block = await this.chain.getBlock(coin.height);
    }

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

    for (const hash of hashes) {
      if (!block.hasTX(hash)) {
        throw new RPCError(errs.VERIFY_ERROR,
          'Block does not contain all txids.');
      }
    }

    block = MerkleBlock.fromHashes(block, hashes);

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

  async verifyTXOutProof(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'verifytxoutproof "proof"');

    const valid = new Validator(args);
    const data = valid.buf(0);

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

    const block = MerkleBlock.fromRaw(data);

    if (!block.verify())
      return [];

    const entry = await this.chain.getEntry(block.hash());

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

    const tree = block.getTree();
    const out = [];

    for (const hash of tree.matches)
      out.push(util.revHex(hash));

    return out;
  }

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

    if (this.chain.options.spv) {
      throw new RPCError(errs.MISC_ERROR,
        '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)
    };
  }

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

    if (this.chain.options.spv)
      throw new RPCError(errs.MISC_ERROR, 'Cannot prune chain in SPV mode.');

    if (this.chain.options.prune)
      throw new RPCError(errs.MISC_ERROR, 'Chain is already pruned.');

    if (this.chain.height < this.network.block.pruneAfterHeight)
      throw new RPCError(errs.MISC_ERROR, 'Chain is too short for pruning.');

    try {
      await this.chain.prune();
    } catch (e) {
      throw new RPCError(errs.DATABASE_ERROR, e.message);
    }
  }

  async verifyChain(args, help) {
    if (help || args.length > 2) {
      throw new RPCError(errs.MISC_ERROR,
        'verifychain ( checklevel numblocks )');
    }

    const valid = new Validator(args);
    const level = valid.u32(0);
    const blocks = valid.u32(1);

    if (level == null || blocks == null)
      throw new RPCError(errs.TYPE_ERROR, 'Missing parameters.');

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

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

    return null;
  }

  /*
   * Mining
   */

  async submitWork(data) {
    const unlock = await this.locker.lock();
    try {
      return await this._submitWork(data);
    } finally {
      unlock();
    }
  }

  async _submitWork(data) {
    const attempt = this.attempt;

    if (!attempt)
      return false;

    if (data.length !== 128)
      throw new RPCError(errs.INVALID_PARAMETER, 'Invalid work size.');

    const raw = data.slice(0, 80);
    swap32(raw);

    const header = Headers.fromHead(raw);

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

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

    const nonces = this.merkleMap.get(header.merkleRoot);

    if (!nonces)
      return false;

    const [n1, n2] = nonces;
    const nonce = header.nonce;
    const time = header.time;

    const proof = attempt.getProof(n1, n2, time, nonce);

    if (!proof.verify(attempt.target))
      return false;

    const block = attempt.commit(proof);

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

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

    return true;
  }

  async createWork(data) {
    const unlock = await this.locker.lock();
    try {
      return await this._createWork(data);
    } finally {
      unlock();
    }
  }

  async _createWork() {
    const attempt = await this.updateWork();
    const n1 = this.nonce1;
    const n2 = this.nonce2;
    const time = attempt.time;

    const data = Buffer.allocUnsafe(128);
    data.fill(0);

    const root = attempt.getRoot(n1, n2);
    const head = attempt.getHeader(root, time, 0);

    head.copy(data, 0);

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

    swap32(data);

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

  async getWorkLongpoll(args, help) {
    await this.longpoll();
    return await this.createWork();
  }

  async getWork(args, help) {
    if (args.length > 1)
      throw new RPCError(errs.MISC_ERROR, 'getwork ( "data" )');

    if (args.length === 1) {
      const valid = new Validator(args);
      const data = valid.buf(0);

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

      return await this.submitWork(data);
    }

    return await this.createWork();
  }

  async submitBlock(args, help) {
    if (help || args.length < 1 || args.length > 2) {
      throw new RPCError(errs.MISC_ERROR,
        'submitblock "hexdata" ( "jsonparametersobject" )');
    }

    const valid = new Validator(args);
    const data = valid.buf(0);

    const block = Block.fromRaw(data);

    return await this.addBlock(block);
  }

  async getBlockTemplate(args, help) {
    if (help || args.length > 1) {
      throw new RPCError(errs.MISC_ERROR,
        'getblocktemplate ( "jsonrequestobject" )');
    }

    const validator = new Validator(args);
    const options = validator.obj(0, {});
    const valid = new Validator(options);
    const mode = valid.str('mode', 'template');

    if (mode !== 'template' && mode !== 'proposal')
      throw new RPCError(errs.INVALID_PARAMETER, 'Invalid mode.');

    if (mode === 'proposal') {
      const data = valid.buf('data');

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

      const block = Block.fromRaw(data);

      if (!block.prevBlock.equals(this.chain.tip.hash))
        return 'inconclusive-not-best-prevblk';

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

      return null;
    }

    let maxVersion = valid.u32('maxversion', -1);
    let rules = valid.array('rules');

    if (rules)
      maxVersion = -1;

    const capabilities = valid.array('capabilities');
    let coinbase = false;

    if (capabilities) {
      let txnCap = false;
      let valueCap = false;

      for (const capability of capabilities) {
        if (typeof capability !== 'string')
          throw new RPCError(errs.TYPE_ERROR, 'Invalid capability.');

        switch (capability) {
          case 'coinbasetxn':
            txnCap = true;
            break;
          case 'coinbasevalue':
            // Prefer value if they support it.
            valueCap = true;
            break;
        }
      }

      // BIP22 states that we can't have coinbasetxn
      // _and_ coinbasevalue in the same template.
      // The problem is, many clients _say_ they
      // support coinbasetxn when they don't (ckpool).
      // To make matters worse, some clients will
      // parse an undefined `coinbasevalue` as zero.
      // Because of all of this, coinbasetxn is
      // disabled for now.
      valueCap = true;

      if (txnCap && !valueCap) {
        if (this.miner.addresses.length === 0) {
          throw new RPCError(errs.MISC_ERROR,
            'No addresses available for coinbase.');
        }
        coinbase = true;
      }
    }

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

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

    const lpid = valid.str('longpollid');

    if (lpid)
      await this.handleLongpoll(lpid);

    if (!rules)
      rules = [];

    return await this.createTemplate(maxVersion, coinbase, rules);
  }

  async createTemplate(maxVersion, coinbase, rules) {
    const unlock = await this.locker.lock();
    try {
      return await this._createTemplate(maxVersion, coinbase, rules);
    } finally {
      unlock();
    }
  }

  async _createTemplate(maxVersion, coinbase, rules) {
    const attempt = await this.getTemplate();
    const scale = attempt.witness ? 1 : consensus.WITNESS_SCALE_FACTOR;

    // Default mutable fields.
    const mutable = ['time', 'transactions', 'prevblock'];

    // The miner doesn't support
    // versionbits. Force them to
    // encode our version.
    if (maxVersion >= 2)
      mutable.push('version/force');

    // Allow the miner to change
    // our provided coinbase.
    // Note that these are implied
    // without `coinbasetxn`.
    if (coinbase) {
      mutable.push('coinbase');
      mutable.push('coinbase/append');
      mutable.push('generation');
    }

    // Build an index of every transaction.
    const index = new BufferMap();
    for (let i = 0; i < attempt.items.length; i++) {
      const entry = attempt.items[i];
      index.set(entry.hash, i + 1);
    }

    // Calculate dependencies for each transaction.
    const txs = [];
    for (let i = 0; i < attempt.items.length; i++) {
      const entry = attempt.items[i];
      const tx = entry.tx;
      const deps = [];

      for (let j = 0; j < tx.inputs.length; j++) {
        const input = tx.inputs[j];
        const dep = index.get(input.prevout.hash);

        if (dep == null)
          continue;

        if (deps.indexOf(dep) === -1) {
          assert(dep < i + 1);
          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 (this.chain.options.bip91) {
      rules.push('segwit');
      rules.push('segsignal');
    }

    if (this.chain.options.bip148)
      rules.push('segwit');

    // Calculate version based on given rules.
    let version = attempt.version;
    const vbavailable = {};
    const vbrules = [];

    for (const deploy of this.network.deploys) {
      const state = await this.chain.getState(this.chain.tip, deploy);
      let name = deploy.name;

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

    version >>>= 0;

    const json = {
      capabilities: ['proposal'],
      mutable: mutable,
      version: version,
      rules: vbrules,
      vbavailable: vbavailable,
      vbrequired: 0,
      height: attempt.height,
      previousblockhash: util.revHex(attempt.prevBlock),
      target: util.revHex(attempt.target),
      bits: hex32(attempt.bits),
      noncerange: '00000000ffffffff',
      curtime: attempt.time,
      mintime: attempt.mtp + 1,
      maxtime: attempt.time + 7200,
      expires: attempt.time + 7200,
      sigoplimit: consensus.MAX_BLOCK_SIGOPS_COST / scale | 0,
      sizelimit: consensus.MAX_BLOCK_SIZE,
      weightlimit: undefined,
      longpollid: this.chain.tip.rhash() + hex32(this.totalTX()),
      submitold: false,
      coinbaseaux: {
        flags: attempt.coinbaseFlags.toString('hex')
      },
      coinbasevalue: undefined,
      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;
    }

    // The client wants a coinbasetxn
    // instead of a coinbasevalue.
    if (coinbase) {
      const tx = attempt.toCoinbase();
      const input = tx.inputs[0];

      // Pop off the nonces.
      input.script.pop();
      input.script.compile();

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

        // Also not including the witness nonce.
        input.witness.clear();
      }

      tx.refresh();

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

    if (rules.indexOf('segwit') !== -1)
      json.default_witness_commitment = attempt.getWitnessScript().toJSON();

    return json;
  }

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

    const attempt = this.attempt;

    let size = 0;
    let weight = 0;
    let txs = 0;
    let diff = 0;

    if (attempt) {
      weight = attempt.weight;
      txs = attempt.items.length + 1;
      diff = attempt.getDifficulty();
      size = 1000;
      for (const item of attempt.items)
        size += item.tx.getBaseSize();
    }

    return {
      blocks: this.chain.height,
      currentblocksize: size,
      currentblockweight: weight,
      currentblocktx: txs,
      difficulty: diff,
      errors: '',
      genproclimit: this.procLimit,
      networkhashps: await this.getHashRate(120),
      pooledtx: this.totalTX(),
      testnet: this.network !== Network.main,
      chain: this.network.type !== 'testnet'
        ? this.network.type
        : 'test',
      generate: this.mining
    };
  }

  async getNetworkHashPS(args, help) {
    if (help || args.length > 2)
      throw new RPCError(errs.MISC_ERROR, 'getnetworkhashps ( blocks height )');

    const valid = new Validator(args);
    const lookup = valid.u32(0, 120);
    const height = valid.u32(1);

    return await this.getHashRate(lookup, height);
  }

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

    const valid = new Validator(args);
    const hash = valid.brhash(0);
    const pri = valid.i64(1);
    const fee = valid.i64(2);

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

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

    if (pri == null || fee == null)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid fee or priority.');

    const entry = this.mempool.getEntry(hash);

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

    this.mempool.prioritise(entry, pri, fee);

    return true;
  }

  async verifyBlock(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'verifyblock "block-hex"');

    const valid = new Validator(args);
    const data = valid.buf(0);

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

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

    const block = Block.fromRaw(data);

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

    return null;
  }

  /*
   * Coin generation
   */

  async getGenerate(args, help) {
    if (help || args.length !== 0)
      throw new RPCError(errs.MISC_ERROR, 'getgenerate');
    return this.mining;
  }

  async setGenerate(args, help) {
    if (help || args.length < 1 || args.length > 2)
      throw new RPCError(errs.MISC_ERROR, 'setgenerate mine ( proclimit )');

    const valid = new Validator(args);
    const mine = valid.bool(0, false);
    const limit = valid.u32(1, 0);

    if (mine && this.miner.addresses.length === 0) {
      throw new RPCError(errs.MISC_ERROR,
        'No addresses available for coinbase.');
    }

    this.mining = mine;
    this.procLimit = limit;

    if (mine) {
      this.miner.cpu.start();
      return true;
    }

    await this.miner.cpu.stop();

    return false;
  }

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

    const valid = new Validator(args);
    const blocks = valid.u32(0, 1);
    const tries = valid.u32(1);

    if (this.miner.addresses.length === 0) {
      throw new RPCError(errs.MISC_ERROR,
        'No addresses available for coinbase.');
    }

    return await this.mineBlocks(blocks, null, tries);
  }

  async generateToAddress(args, help) {
    if (help || args.length < 2 || args.length > 3) {
      throw new RPCError(errs.MISC_ERROR,
        'generatetoaddress numblocks address ( maxtries )');
    }

    const valid = new Validator(args);
    const blocks = valid.u32(0, 1);
    const str = valid.str(1, '');
    const tries = valid.u32(2);

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

    return await this.mineBlocks(blocks, addr, tries);
  }

  /*
   * Raw transactions
   */

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

    const valid = new Validator(args);
    const inputs = valid.array(0);
    const sendTo = valid.obj(1);
    const locktime = valid.u32(2);

    if (!inputs || !sendTo) {
      throw new RPCError(errs.TYPE_ERROR,
        'Invalid parameters (inputs and sendTo).');
    }

    const tx = new MTX();

    if (locktime != null)
      tx.locktime = locktime;

    for (const obj of inputs) {
      const valid = new Validator(obj);
      const hash = valid.brhash('txid');
      const index = valid.u32('vout');
      let sequence = valid.u32('sequence', 0xffffffff);

      if (tx.locktime)
        sequence--;

      if (!hash || index == null)
        throw new RPCError(errs.TYPE_ERROR, 'Invalid outpoint.');

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

      tx.inputs.push(input);
    }

    const sends = new Validator(sendTo);
    const uniq = new Set();

    for (const key of Object.keys(sendTo)) {
      if (key === 'data') {
        const value = sends.buf(key);

        if (!value)
          throw new RPCError(errs.TYPE_ERROR, 'Invalid nulldata..');

        const output = new Output();
        output.value = 0;
        output.script.fromNulldata(value);
        tx.outputs.push(output);

        continue;
      }

      const addr = parseAddress(key, this.network);
      const b58 = addr.toString(this.network);

      if (uniq.has(b58))
        throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate address');

      uniq.add(b58);

      const value = sends.ufixed(key, 8);

      if (value == null)
        throw new RPCError(errs.TYPE_ERROR, 'Invalid output value.');

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

      tx.outputs.push(output);
    }

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

  async decodeRawTransaction(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'decoderawtransaction "hexstring"');

    const valid = new Validator(args);
    const data = valid.buf(0);

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

    const tx = TX.fromRaw(data);

    return this.txToJSON(tx);
  }

  async decodeScript(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'decodescript "hex"');

    const valid = new Validator(args);
    const data = valid.buf(0);

    let script = new Script();

    if (data)
      script = Script.fromRaw(data);

    const addr = Address.fromScripthash(script.hash160());

    const json = this.scriptToJSON(script);
    json.p2sh = addr.toString(this.network);

    return json;
  }

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

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

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

    const meta = await this.node.getMeta(hash);

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

    const tx = meta.tx;

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

    let entry;
    if (meta.block)
      entry = await this.chain.getEntry(meta.block);

    const json = this.txToJSON(tx, entry);
    json.time = meta.mtime;
    json.hex = tx.toRaw().toString('hex');

    return json;
  }

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

    const valid = new Validator(args);
    const data = valid.buf(0);

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

    const tx = TX.fromRaw(data);

    this.node.relay(tx);

    return tx.txid();
  }

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

    const valid = new Validator(args);
    const data = valid.buf(0);
    const prevout = valid.array(1);
    const secrets = valid.array(2);
    const sighash = valid.str(3);

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

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

    const tx = MTX.fromRaw(data);
    tx.view = await this.mempool.getSpentView(tx);

    const map = new BufferMap();
    const keys = [];

    if (secrets) {
      const valid = new Validator(secrets);
      for (let i = 0; i < secrets.length; i++) {
        const secret = valid.str(i, '');
        const key = parseSecret(secret, this.network);
        map.set(key.getPublicKey(), key);
        keys.push(key);
      }
    }

    if (prevout) {
      for (const prev of prevout) {
        const valid = new Validator(prev);
        const hash = valid.brhash('txid');
        const index = valid.u32('vout');
        const scriptRaw = valid.buf('scriptPubKey');
        const value = valid.ufixed('amount', 8);
        const redeemRaw = valid.buf('redeemScript');

        if (!hash || index == null || !scriptRaw || value == null)
          throw new RPCError(errs.INVALID_PARAMETER, 'Invalid UTXO.');

        const outpoint = new Outpoint(hash, index);

        const script = Script.fromRaw(scriptRaw);
        const coin = Output.fromScript(script, value);

        tx.view.addOutput(outpoint, coin);

        if (keys.length === 0 || !redeemRaw)
          continue;

        if (!script.isScripthash() && !script.isWitnessScripthash())
          continue;

        if (!redeemRaw) {
          throw new RPCError(errs.INVALID_PARAMETER,
            'P2SH requires redeem script.');
        }

        const redeem = Script.fromRaw(redeemRaw);

        for (const op of redeem.code) {
          if (!op.data)
            continue;

          const key = map.get(op.data);

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

    let type = Script.hashType.ALL;
    if (sighash) {
      const parts = sighash.split('|');

      if (parts.length < 1 || parts.length > 2)
        throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.');

      type = Script.hashType[parts[0]];

      if (type == null)
        throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.');

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

    await tx.signAsync(keys, type, this.workers);

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

  /*
   * Utility Functions
   */

  async createMultisig(args, help) {
    if (help || args.length < 2 || args.length > 2) {
      throw new RPCError(errs.MISC_ERROR,
        'createmultisig nrequired ["key",...]');
    }

    const valid = new Validator(args);
    const keys = valid.array(1, []);
    const m = valid.u32(0, 0);
    const n = keys.length;

    if (m < 1 || n < m || n > 16)
      throw new RPCError(errs.INVALID_PARAMETER, 'Invalid m and n values.');

    const items = new Validator(keys);

    for (let i = 0; i < keys.length; i++) {
      const key = items.buf(i);

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

      if (!secp256k1.publicKeyVerify(key))
        throw new RPCError(errs.INVALID_ADDRESS_OR_KEY, 'Invalid key.');

      keys[i] = key;
    }

    const script = Script.fromMultisig(m, n, keys);

    if (script.getSize() > consensus.MAX_SCRIPT_PUSH) {
      throw new RPCError(errs.VERIFY_ERROR,
        'Redeem script exceeds size limit.');
    }

    const addr = script.getAddress();

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

  async createWitnessAddress(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'createwitnessaddress "script"');

    const valid = new Validator(args);
    const raw = valid.buf(0);

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

    const script = Script.fromRaw(raw);
    const program = script.forWitness();
    const addr = program.getAddress();

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

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

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

    let addr;
    try {
      addr = Address.fromString(str, this.network);
    } catch (e) {
      return {
        isvalid: false
      };
    }

    const script = Script.fromAddress(addr);
    const isWitness = addr.isProgram();
    const isScript = script.isScripthash() || script.isWitnessScripthash();

    const result = {
      isvalid: true,
      address: addr.toString(this.network),
      scriptPubKey: script.toJSON(),
      isscript: isScript,
      iswitness: isWitness
    };

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

    return result;
  }

  async verifyMessage(args, help) {
    if (help || args.length !== 3) {
      throw new RPCError(errs.MISC_ERROR,
        'verifymessage "bitcoinaddress" "signature" "message"');
    }

    const valid = new Validator(args);
    const b58 = valid.str(0, '');
    const sig = valid.buf(1, null, 'base64');
    const str = valid.str(2);

    if (!sig || !str)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid parameters.');

    const addr = parseAddress(b58, this.network);
    const key = messageUtil.recover(str, sig);

    if (!key)
      return false;

    return safeEqual(hash160.digest(key), addr.hash) === 1;
  }

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

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

    const key = parseSecret(wif, this.network);
    const sig = messageUtil.sign(str, key);

    return sig.toString('base64');
  }

  async estimateFee(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'estimatefee nblocks');

    const valid = new Validator(args);
    const blocks = valid.u32(0, 1);

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

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

    if (fee === 0)
      return -1;

    return Amount.btc(fee, true);
  }

  async estimatePriority(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'estimatepriority nblocks');

    const valid = new Validator(args);
    const blocks = valid.u32(0, 1);

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

    return this.fees.estimatePriority(blocks, false);
  }

  async estimateSmartFee(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'estimatesmartfee nblocks');

    const valid = new Validator(args);
    const blocks = valid.u32(0, 1);

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

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

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

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

  async estimateSmartPriority(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'estimatesmartpriority nblocks');

    const valid = new Validator(args);
    const blocks = valid.u32(0, 1);

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

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

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

  async invalidateBlock(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'invalidateblock "hash"');

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

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

    await this.chain.invalidate(hash);

    return null;
  }

  async reconsiderBlock(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'reconsiderblock "hash"');

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

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

    this.chain.removeInvalid(hash);

    return null;
  }

  async setMockTime(args, help) {
    if (help || args.length !== 1)
      throw new RPCError(errs.MISC_ERROR, 'setmocktime timestamp');

    const valid = new Validator(args);
    const time = valid.u32(0);

    if (time == null)
      throw new RPCError(errs.TYPE_ERROR, 'Invalid timestamp.');

    this.network.time.offset = 0;

    const delta = this.network.now() - time;

    this.network.time.offset = -delta;

    return null;
  }

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

    return this.logger.memoryUsage();
  }

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

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

    this.logger.setLevel(level);

    return null;
  }

  /*
   * Helpers
   */

  async handleLongpoll(lpid) {
    if (lpid.length !== 72)
      throw new RPCError(errs.INVALID_PARAMETER, 'Invalid longpoll ID.');

    const watched = lpid.slice(0, 64);
    const lastTX = parseInt(lpid.slice(64, 72), 16);

    if ((lastTX >>> 0) !== lastTX)
      throw new RPCError(errs.INVALID_PARAMETER, 'Invalid longpoll ID.');

    const hash = util.revHex(watched);

    if (!this.chain.tip.hash.equals(hash))
      return;

    await this.longpoll();
  }

  longpoll() {
    return new Promise((resolve, reject) => {
      this.pollers.push({ resolve, reject });
    });
  }

  refreshBlock() {
    const pollers = this.pollers;

    this.attempt = null;
    this.lastActivity = 0;
    this.merkleMap.clear();
    this.nonce1 = 0;
    this.nonce2 = 0;
    this.pollers = [];

    for (const job of pollers)
      job.resolve();
  }

  bindChain() {
    if (this.boundChain)
      return;

    this.boundChain = true;

    this.node.on('connect', () => {
      if (!this.attempt)
        return;

      this.refreshBlock();
    });

    if (!this.mempool)
      return;

    this.node.on('tx', () => {
      if (!this.attempt)
        return;

      if (util.now() - this.lastActivity > 10)
        this.refreshBlock();
    });
  }

  async getTemplate() {
    this.bindChain();

    let attempt = this.attempt;

    if (attempt) {
      this.miner.updateTime(attempt);
    } else {
      attempt = await this.miner.createBlock();
      this.attempt = attempt;
      this.lastActivity = util.now();
    }

    return attempt;
  }

  async updateWork() {
    this.bindChain();

    let attempt = this.attempt;

    if (attempt) {
      if (attempt.address.isNull()) {
        throw new RPCError(errs.MISC_ERROR,
          'No addresses available for coinbase.');
      }

      this.miner.updateTime(attempt);

      if (++this.nonce2 === 0x100000000) {
        this.nonce2 = 0;
        this.nonce1++;
      }

      const n1 = this.nonce1;
      const n2 = this.nonce2;

      const root = attempt.getRoot(n1, n2);

      this.merkleMap.set(root, [n1, n2]);

      return attempt;
    }

    if (this.miner.addresses.length === 0) {
      throw new RPCError(errs.MISC_ERROR,
        'No addresses available for coinbase.');
    }

    attempt = await this.miner.createBlock();

    const n1 = this.nonce1;
    const n2 = this.nonce2;

    const root = attempt.getRoot(n1, n2);

    this.attempt = attempt;
    this.lastActivity = util.now();
    this.merkleMap.set(root, [n1, n2]);

    return attempt;
  }

  async addBlock(block) {
    const unlock1 = await this.locker.lock();
    const unlock2 = await this.chain.locker.lock();
    try {
      return await this._addBlock(block);
    } finally {
      unlock2();
      unlock1();
    }
  }

  async _addBlock(block) {
    this.logger.info('Handling submitted block: %h.', block.hash());

    const prev = await this.chain.getEntry(block.prevBlock);

    if (prev) {
      const state = await this.chain.getDeployments(block.time, prev);

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

          // Recreate witness nonce (all zeroes).
          input.witness.push(consensus.ZERO_HASH);
          input.witness.compile();

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

    let entry;
    try {
      entry = await this.chain._add(block);
    } catch (err) {
      if (err.type === 'VerifyError') {
        this.logger.warning('RPC block rejected: %h (%s).',
          block.hash(), err.reason);
        return `rejected: ${err.reason}`;
      }
      throw err;
    }

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

    return null;
  }

  totalTX() {
    return this.mempool ? this.mempool.map.size : 0;
  }

  getSoftforks() {
    return [
      toDeployment('bip34', 2, this.chain.state.hasBIP34()),
      toDeployment('bip66', 3, this.chain.state.hasBIP66()),
      toDeployment('bip65', 4, this.chain.state.hasCLTV())
    ];
  }

  async getBIP9Softforks() {
    const tip = this.chain.tip;
    const forks = {};

    for (const deployment of this.network.deploys) {
      const state = await this.chain.getState(tip, deployment);
      let status;

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

  async getHashRate(lookup, height) {
    let tip = this.chain.tip;

    if (height != null)
      tip = await this.chain.getEntry(height);

    if (!tip)
      return 0;

    assert(typeof lookup === 'number');
    assert(lookup >= 0);

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

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

    let min = tip.time;
    let max = min;
    let entry = tip;

    for (let i = 0; i < lookup; i++) {
      entry = await this.chain.getPrevious(entry);

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

      min = Math.min(entry.time, min);
      max = Math.max(entry.time, max);
    }

    const diff = max - min;

    if (diff === 0)
      return 0;

    const work = tip.chainwork.sub(entry.chainwork);

    return Number(work.toString()) / diff;
  }

  async mineBlocks(blocks, addr, tries) {
    const unlock = await this.locker.lock();
    try {
      return await this._mineBlocks(blocks, addr, tries);
    } finally {
      unlock();
    }
  }

  async _mineBlocks(blocks, addr, tries) {
    const hashes = [];

    for (let i = 0; i < blocks; i++) {
      const block = await this.miner.mineBlock(null, addr);
      const entry = await this.chain.add(block);
      assert(entry);
      hashes.push(entry.rhash());
    }

    return hashes;
  }

  async findFork(entry) {
    while (entry) {
      if (await this.chain.isMainChain(entry))
        return entry;
      entry = await this.chain.getPrevious(entry);
    }
    throw new Error('Fork not found.');
  }

  txToJSON(tx, entry) {
    let height = -1;
    let time = 0;
    let hash = null;
    let conf = 0;

    if (entry) {
      height = entry.height;
      time = entry.time;
      hash = entry.rhash();
      conf = this.chain.height - height + 1;
    }

    const vin = [];

    for (const input of tx.inputs) {
      const json = {
        coinbase: undefined,
        txid: undefined,
        scriptSig: undefined,
        txinwitness: undefined,
        sequence: input.sequence
      };

      if (tx.isCoinbase()) {
        json.coinbase = input.script.toJSON();
      } else {
        json.txid = input.prevout.txid();
        json.vout = input.prevout.index;
        json.scriptSig = {
          asm: input.script.toASM(),
          hex: input.script.toJSON()
        };
      }

      if (input.witness.items.length > 0) {
        json.txinwitness = input.witness.items.map((item) => {
          return item.toString('hex');
        });
      }

      vin.push(json);
    }

    const vout = [];

    for (let i = 0; i < tx.outputs.length; i++) {
      const output = tx.outputs[i];
      vout.push({
        value: Amount.btc(output.value, true),
        n: i,
        scriptPubKey: this.scriptToJSON(output.script, true)
      });
    }

    return {
      txid: tx.txid(),
      hash: tx.wtxid(),
      size: tx.getSize(),
      vsize: tx.getVirtualSize(),
      version: tx.version,
      locktime: tx.locktime,
      vin: vin,
      vout: vout,
      blockhash: hash,
      confirmations: conf,
      time: time,
      blocktime: time,
      hex: undefined
    };
  }

  scriptToJSON(script, hex) {
    const type = script.getType();

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

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

    const [m] = script.getMultisig();

    if (m !== -1)
      json.reqSigs = m;

    const addr = script.getAddress();

    if (addr) {
      const str = addr.toString(this.network);
      json.addresses.push(str);
    }

    return json;
  }

  async headerToJSON(entry) {
    const mtp = await this.chain.getMedianTime(entry);
    const next = await this.chain.getNextHash(entry.hash);

    let confirmations = -1;
    if (await this.chain.isMainChain(entry))
      confirmations = this.chain.height - entry.height + 1;

    return {
      hash: entry.rhash(),
      confirmations: confirmations,
      height: entry.height,
      version: entry.version,
      versionHex: hex32(entry.version),
      merkleroot: util.revHex(entry.merkleRoot),
      time: entry.time,
      mediantime: mtp,
      nonce: entry.nonce,
      bits: hex32(entry.bits),
      difficulty: toDifficulty(entry.bits),
      chainwork: entry.chainwork.toString('hex', 64),
      previousblockhash: !entry.prevBlock.equals(consensus.ZERO_HASH)
        ? util.revHex(entry.prevBlock)
        : null,
      nextblockhash: next ? util.revHex(next) : null
    };
  }

  async blockToJSON(entry, block, details) {
    const mtp = await this.chain.getMedianTime(entry);
    const next = await this.chain.getNextHash(entry.hash);

    let confirmations = -1;
    if (await this.chain.isMainChain(entry))
      confirmations = this.chain.height - entry.height + 1;

    const txs = [];

    for (const tx of block.txs) {
      if (details) {
        const json = this.txToJSON(tx, entry);
        txs.push(json);
        continue;
      }
      txs.push(tx.txid());
    }

    return {
      hash: entry.rhash(),
      confirmations: confirmations,
      strippedsize: block.getBaseSize(),
      size: block.getSize(),
      weight: block.getWeight(),
      height: entry.height,
      version: entry.version,
      versionHex: hex32(entry.version),
      merkleroot: util.revHex(entry.merkleRoot),
      coinbase: block.txs[0].inputs[0].script.toJSON(),
      tx: txs,
      time: entry.time,
      mediantime: mtp,
      nonce: entry.nonce,
      bits: hex32(entry.bits),
      difficulty: toDifficulty(entry.bits),
      chainwork: entry.chainwork.toString('hex', 64),
      nTx: txs.length,
      previousblockhash: !entry.prevBlock.equals(consensus.ZERO_HASH)
        ? util.revHex(entry.prevBlock)
        : null,
      nextblockhash: next ? util.revHex(next) : null
    };
  }

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

/*
 * Helpers
 */

function swap32(data) {
  for (let i = 0; i < data.length; i += 4) {
    const field = data.readUInt32LE(i, true);
    data.writeUInt32BE(field, i, true);
  }
  return data;
}

function toDeployment(id, version, status) {
  return {
    id: id,
    version: version,
    reject: {
      status: status
    }
  };
}

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

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

function parseIP(addr, network) {
  try {
    return IP.fromHostname(addr, network.port);
  } catch (e) {
    throw new RPCError(errs.CLIENT_INVALID_IP_OR_SUBNET,
      'Invalid IP address or subnet.');
  }
}

function parseNetAddress(addr, network) {
  try {
    return NetAddress.fromHostname(addr, network);
  } catch (e) {
    throw new RPCError(errs.CLIENT_INVALID_IP_OR_SUBNET,
      'Invalid IP address or subnet.');
  }
}

function toDifficulty(bits) {
  let shift = (bits >>> 24) & 0xff;
  let diff = 0x0000ffff / (bits & 0x00ffffff);

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

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

  return diff;
}

function hex32(num) {
  assert(num >= 0);

  num = num.toString(16);

  assert(num.length <= 8);

  while (num.length < 8)
    num = '0' + num;

  return num;
}

/*
 * Expose
 */

module.exports = RPC;