# Source: src/main/js/daemon/MoneroDaemonRpc.js

    const assert = require("assert");
    const BigInteger = require("../common/biginteger").BigInteger;
    const GenUtils = require("../common/GenUtils");
    const LibraryUtils = require("../common/LibraryUtils");
    const TaskLooper = require("../common/TaskLooper");
    const MoneroAltChain = require("./model/MoneroAltChain");
    const MoneroBan = require("./model/MoneroBan");
    const MoneroBlock = require("./model/MoneroBlock");
    const MoneroBlockHeader = require("./model/MoneroBlockHeader");
    const MoneroBlockTemplate = require("./model/MoneroBlockTemplate");
    const MoneroDaemon = require("./MoneroDaemon");
    const MoneroDaemonInfo = require("./model/MoneroDaemonInfo");
    const MoneroDaemonListener = require("./model/MoneroDaemonListener");
    const MoneroDaemonSyncInfo = require("./model/MoneroDaemonSyncInfo");
    const MoneroError = require("../common/MoneroError");
    const MoneroFeeEstimate = require("./model/MoneroFeeEstimate");
    const MoneroHardForkInfo = require("./model/MoneroHardForkInfo");
    const MoneroKeyImage = require("./model/MoneroKeyImage");
    const MoneroMinerTxSum = require("./model/MoneroMinerTxSum");
    const MoneroMiningStatus = require("./model/MoneroMiningStatus");
    const MoneroNetworkType = require("./model/MoneroNetworkType");
    const MoneroOutput = require("./model/MoneroOutput");
    const MoneroOutputHistogramEntry = require("./model/MoneroOutputHistogramEntry");
    const MoneroPeer = require("./model/MoneroPeer");
    const MoneroPruneResult = require("./model/MoneroPruneResult");
    const MoneroRpcConnection = require("../common/MoneroRpcConnection");
    const MoneroSubmitTxResult = require("./model/MoneroSubmitTxResult");
    const MoneroTx = require("./model/MoneroTx");
    const MoneroTxPoolStats = require("./model/MoneroTxPoolStats");
    const MoneroUtils = require("../common/MoneroUtils");
    const MoneroVersion = require("./model/MoneroVersion");

    /**
     * Copyright (c) woodser
     *
     * Permission is hereby granted, free of charge, to any person obtaining a copy
     * of this software and associated documentation files (the "Software"), to deal
     * in the Software without restriction, including without limitation the rights
     * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     * copies of the Software, and to permit persons to whom the Software is
     * furnished to do so, subject to the following conditions:
     *
     * The above copyright notice and this permission notice shall be included in all
     * copies or substantial portions of the Software.
     *
     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     * SOFTWARE.
     */

    /**
     * Implements a MoneroDaemon as a client of monerod.
     *
     * @implements {MoneroDaemon}
     * @hideconstructor
     */
    class MoneroDaemonRpc extends MoneroDaemon {

      /**
       * <p>Construct a daemon RPC client (for internal use).<p>
       *
       * @param {string|object|MoneroRpcConnection} uriOrConfig - uri of monerod or JS config object or MoneroRpcConnection
       * @param {string} uriOrConfig.uri - uri of monerod
       * @param {string} uriOrConfig.username - username to authenticate with monerod (optional)
       * @param {string} uriOrConfig.password - password to authenticate with monerod (optional)
       * @param {boolean} uriOrConfig.rejectUnauthorized - rejects self-signed certificates if true (default true)
       * @param {number} uriOrConfig.pollInterval - poll interval to query for updates in ms (default 5000)
       * @param {string} username - username to authenticate with monerod (optional)
       * @param {string} password - password to authenticate with monerod (optional)
       * @param {boolean} rejectUnauthorized - rejects self-signed certificates if true (default true)
       * @param {number} pollInterval - poll interval to query for updates in ms (default 5000)
       * @param {boolean} proxyToWorker - runs the daemon client in a worker if true (default true)
       */
      constructor(uriOrConfig, username, password, rejectUnauthorized, pollInterval, proxyToWorker) {
        super();
        if (GenUtils.isArray(uriOrConfig)) throw new Error("Use monerojs.connectToDaemonRpc(...) to use terminal parameters");
        this.config = MoneroDaemonRpc._normalizeConfig(uriOrConfig, username, password, rejectUnauthorized, pollInterval, proxyToWorker);
        if (this.config.proxyToWorker) throw new Error("Use monerojs.connectToDaemonRpc(...) to proxy to worker");
        let rpcConfig = Object.assign({}, this.config);
        delete rpcConfig.proxyToWorker;
        delete rpcConfig.pollInterval;
        this.rpc = new MoneroRpcConnection(rpcConfig);
        this.listeners = [];      // block listeners
        this.cachedHeaders = {};  // cached headers for fetching blocks in bound chunks
      }

      /**
       * <p>Create a client connected to monerod (for internal use).</p>
       *
       * @param {string|string[]|object|MoneroRpcConnection} uriOrConfig - uri of monerod or terminal parameters or JS config object or MoneroRpcConnection
       * @param {string} uriOrConfig.uri - uri of monerod
       * @param {string} uriOrConfig.username - username to authenticate with monerod (optional)
       * @param {string} uriOrConfig.password - password to authenticate with monerod (optional)
       * @param {boolean} uriOrConfig.rejectUnauthorized - rejects self-signed certificates if true (default true)
       * @param {number} uriOrConfig.pollInterval - poll interval to query for updates in ms (default 5000)
       * @param {boolean} uriOrConfig.proxyToWorker - run the daemon client in a worker if true (default true)
       * @param {string} username - username to authenticate with monerod (optional)
       * @param {string} password - password to authenticate with monerod (optional)
       * @param {boolean} rejectUnauthorized - rejects self-signed certificates if true (default true)
       * @param {number} pollInterval - poll interval to query for updates in ms (default 5000)
       * @param {boolean} proxyToWorker - runs the daemon client in a worker if true (default true)
       * @return {MoneroDaemonRpc} the daemon RPC client
       */
      static async _connectToDaemonRpc(uriOrConfig, username, password, rejectUnauthorized, pollInterval, proxyToWorker) {
        if (GenUtils.isArray(uriOrConfig)) return MoneroDaemonRpc._startMonerodProcess(uriOrConfig, rejectUnauthorized, pollInterval, proxyToWorker); // handle array as terminal command
        let config = MoneroDaemonRpc._normalizeConfig(uriOrConfig, username, password, rejectUnauthorized, pollInterval, proxyToWorker);
        if (config.proxyToWorker) return MoneroDaemonRpcProxy.connect(config);
        else return new MoneroDaemonRpc(config);
      }

      static async _startMonerodProcess(cmd, rejectUnauthorized, pollInterval, proxyToWorker) {
        assert(GenUtils.isArray(cmd), "Must provide string array with command line parameters");

        // start process
        this.process = require('child_process').spawn(cmd[0], cmd.slice(1), {});
        this.process.stdout.setEncoding('utf8');
        this.process.stderr.setEncoding('utf8');

        // return promise which resolves after starting monerod
        let uri;
        let that = this;
        let output = "";
        return new Promise(function(resolve, reject) {

          // handle stdout
          that.process.stdout.on('data', async function(data) {
            let line = data.toString();
            LibraryUtils.log(2, line);
            output += line + '\n'; // capture output in case of error

            // extract uri from e.g. "I Binding on 127.0.0.1 (IPv4):38085"
            let uriLineContains = "Binding on ";
            let uriLineContainsIdx = line.indexOf(uriLineContains);
            if (uriLineContainsIdx >= 0) {
              let host = line.substring(uriLineContainsIdx + uriLineContains.length, line.lastIndexOf(' '));
              let unformattedLine = line.replace(/\u001b\[.*?m/g, '').trim(); // remove color formatting
              let port = unformattedLine.substring(unformattedLine.lastIndexOf(':') + 1);
              let sslIdx = cmd.indexOf("--rpc-ssl");
              let sslEnabled = sslIdx >= 0 ? "enabled" == cmd[sslIdx + 1].toLowerCase() : false;
              uri = (sslEnabled ? "https" : "http") + "://" + host + ":" + port;
            }

            // read success message
            if (line.indexOf("core RPC server started ok") >= 0) {

              // get username and password from params
              let userPassIdx = cmd.indexOf("--rpc-login");
              let userPass = userPassIdx >= 0 ? cmd[userPassIdx + 1] : undefined;
              let username = userPass === undefined ? undefined : userPass.substring(0, userPass.indexOf(':'));
              let password = userPass === undefined ? undefined : userPass.substring(userPass.indexOf(':') + 1);

              // create client connected to internal process
              let daemon = await that._connectToDaemonRpc(uri, username, password, rejectUnauthorized, pollInterval, proxyToWorker);
              daemon.process = that.process;

              // resolve promise with client connected to internal process
              this.isResolved = true;
              resolve(daemon);
            }
          });

          // handle stderr
          that.process.stderr.on('data', function(data) {
            if (LibraryUtils.getLogLevel() >= 2) console.error(data);
          });

          // handle exit
          that.process.on("exit", function(code) {
            if (!this.isResolved) reject(new Error("monerod process terminated with exit code " + code + (output ? ":\n\n" + output : "")));
          });

          // handle error
          that.process.on("error", function(err) {
            if (err.message.indexOf("ENOENT") >= 0) reject(new Error("monerod does not exist at path '" + cmd[0] + "'"));
            if (!this.isResolved) reject(err);
          });

          // handle uncaught exception
          that.process.on("uncaughtException", function(err, origin) {
            console.error("Uncaught exception in monerod process: " + err.message);
            console.error(origin);
            reject(err);
          });
        });
      }

      /**
       * Get the internal process running monerod.
       *
       * @return the process running monerod, undefined if not created from new process
       */
      getProcess() {
        return this.process;
      }

      /**
       * Stop the internal process running monerod, if applicable.
       *
       * @param {boolean} force specifies if the process should be destroyed forcibly
       * @return {Promise<number|undefined>} the exit code from stopping the process
       */
      async stopProcess(force) {
        if (this.process === undefined) throw new MoneroError("MoneroDaemonRpc instance not created from new process");
        let listenersCopy = GenUtils.copyArray(this.getListeners());
        for (let listener of listenersCopy) await this.removeListener(listener);
        return GenUtils.killProcess(this.process, force ? "sigkill" : undefined);
      }

      async addListener(listener) {
        assert(listener instanceof MoneroDaemonListener, "Listener must be instance of MoneroDaemonListener");
        this.listeners.push(listener);
        this._refreshListening();
      }

      async removeListener(listener) {
        assert(listener instanceof MoneroDaemonListener, "Listener must be instance of MoneroDaemonListener");
        let idx = this.listeners.indexOf(listener);
        if (idx > -1) this.listeners.splice(idx, 1);
        else throw new MoneroError("Listener is not registered with daemon");
        this._refreshListening();
      }

      getListeners() {
        return this.listeners;
      }

      /**
       * Get the daemon's RPC connection.
       *
       * @return {MoneroRpcConnection} the daemon's rpc connection
       */
      async getRpcConnection() {
        return this.rpc;
      }

      async isConnected() {
        try {
          await this.getVersion();
          return true;
        } catch (e) {
          return false;
        }
      }

      async getVersion() {
        let resp = await this.rpc.sendJsonRequest("get_version");
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return new MoneroVersion(resp.result.version, resp.result.release);
      }

      async isTrusted() {
        let resp = await this.rpc.sendPathRequest("get_height");
        MoneroDaemonRpc._checkResponseStatus(resp);
        return !resp.untrusted;
      }

      async getHeight() {
        let resp = await this.rpc.sendJsonRequest("get_block_count");
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return resp.result.count;
      }

      async getBlockHash(height) {
        return (await this.rpc.sendJsonRequest("on_get_block_hash", [height])).result;  // TODO monero-wallet-rpc: no status returned
      }

      async getBlockTemplate(walletAddress, reserveSize) {
        assert(walletAddress && typeof walletAddress === "string", "Must specify wallet address to be mined to");
        let resp = await this.rpc.sendJsonRequest("get_block_template", {wallet_address: walletAddress, reserve_size: reserveSize});
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return MoneroDaemonRpc._convertRpcBlockTemplate(resp.result);
      }

      async getLastBlockHeader() {
        let resp = await this.rpc.sendJsonRequest("get_last_block_header");
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return MoneroDaemonRpc._convertRpcBlockHeader(resp.result.block_header);
      }

      async getBlockHeaderByHash(blockHash) {
        let resp = await this.rpc.sendJsonRequest("get_block_header_by_hash", {hash: blockHash});
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return MoneroDaemonRpc._convertRpcBlockHeader(resp.result.block_header);
      }

      async getBlockHeaderByHeight(height) {
        let resp = await this.rpc.sendJsonRequest("get_block_header_by_height", {height: height});
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return MoneroDaemonRpc._convertRpcBlockHeader(resp.result.block_header);
      }

      async getBlockHeadersByRange(startHeight, endHeight) {

        // fetch block headers
        let resp = await this.rpc.sendJsonRequest("get_block_headers_range", {
          start_height: startHeight,
          end_height: endHeight
        });
        MoneroDaemonRpc._checkResponseStatus(resp.result);

        // build headers
        let headers = [];
        for (let rpcHeader of resp.result.headers) {
          headers.push(MoneroDaemonRpc._convertRpcBlockHeader(rpcHeader));
        }
        return headers;
      }

      async getBlockByHash(blockHash) {
        let resp = await this.rpc.sendJsonRequest("get_block", {hash: blockHash});
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return MoneroDaemonRpc._convertRpcBlock(resp.result);
      }

      async getBlockByHeight(height) {
        let resp = await this.rpc.sendJsonRequest("get_block", {height: height});
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return MoneroDaemonRpc._convertRpcBlock(resp.result);
      }

      async getBlocksByHeight(heights) {

        // fetch blocks in binary
        let respBin = await this.rpc.sendBinaryRequest("get_blocks_by_height.bin", {heights: heights});

        // convert binary blocks to json
        let rpcBlocks = await MoneroUtils.binaryBlocksToJson(respBin);
        MoneroDaemonRpc._checkResponseStatus(rpcBlocks);

        // build blocks with transactions
        assert.equal(rpcBlocks.txs.length, rpcBlocks.blocks.length);
        let blocks = [];
        for (let blockIdx = 0; blockIdx < rpcBlocks.blocks.length; blockIdx++) {

          // build block
          let block = MoneroDaemonRpc._convertRpcBlock(rpcBlocks.blocks[blockIdx]);
          block.setHeight(heights[blockIdx]);
          blocks.push(block);

          // build transactions
          let txs = [];
          for (let txIdx = 0; txIdx < rpcBlocks.txs[blockIdx].length; txIdx++) {
            let tx = new MoneroTx();
            txs.push(tx);
            tx.setHash(rpcBlocks.blocks[blockIdx].tx_hashes[txIdx]);
            tx.setIsConfirmed(true);
            tx.setInTxPool(false);
            tx.setIsMinerTx(false);
            tx.setRelay(true);
            tx.setIsRelayed(true);
            tx.setIsFailed(false);
            tx.setIsDoubleSpend(false);
            MoneroDaemonRpc._convertRpcTx(rpcBlocks.txs[blockIdx][txIdx], tx);
          }

          // merge into one block
          block.setTxs([]);
          for (let tx of txs) {
            if (tx.getBlock()) block.merge(tx.getBlock());
            else block.getTxs().push(tx.setBlock(block));
          }
        }

        return blocks;
      }

      async getBlocksByRange(startHeight, endHeight) {
        if (startHeight === undefined) startHeight = 0;
        if (endHeight === undefined) endHeight = await this.getHeight() - 1;
        let heights = [];
        for (let height = startHeight; height <= endHeight; height++) heights.push(height);
        return await this.getBlocksByHeight(heights);
      }

      async getBlocksByRangeChunked(startHeight, endHeight, maxChunkSize) {
        if (startHeight === undefined) startHeight = 0;
        if (endHeight === undefined) endHeight = await this.getHeight() - 1;
        let lastHeight = startHeight - 1;
        let blocks = [];
        while (lastHeight < endHeight) {
          for (let block of await this._getMaxBlocks(lastHeight + 1, endHeight, maxChunkSize)) {
            blocks.push(block);
          }
          lastHeight = blocks[blocks.length - 1].getHeight();
        }
        return blocks;
      }

      async getTxs(txHashes, prune) {

        // validate input
        assert(Array.isArray(txHashes) && txHashes.length > 0, "Must provide an array of transaction hashes");
        assert(prune === undefined || typeof prune === "boolean", "Prune must be a boolean or undefined");

        // fetch transactions
        let resp = await this.rpc.sendPathRequest("get_transactions", {
          txs_hashes: txHashes,
          decode_as_json: true,
          prune: prune
        });
        try {
          MoneroDaemonRpc._checkResponseStatus(resp);
        } catch (e) {
          if (e.message.indexOf("Failed to parse hex representation of transaction hash") >= 0) throw new MoneroError("Invalid transaction hash");
          throw e;
        }

        // build transaction models
        let txs = [];
        if (resp.txs) {
          for (let txIdx = 0; txIdx < resp.txs.length; txIdx++) {
            let tx = new MoneroTx();
            tx.setIsMinerTx(false);
            txs.push(MoneroDaemonRpc._convertRpcTx(resp.txs[txIdx], tx));
          }
        }

        return txs;
      }

      async getTxHexes(txHashes, prune) {
        let hexes = [];
        for (let tx of await this.getTxs(txHashes, prune)) hexes.push(prune ? tx.getPrunedHex() : tx.getFullHex());
        return hexes;
      }

      async getMinerTxSum(height, numBlocks) {
        if (height === undefined) height = 0;
        else assert(height >= 0, "Height must be an integer >= 0");
        if (numBlocks === undefined) numBlocks = await this.getHeight();
        else assert(numBlocks >= 0, "Count must be an integer >= 0");
        let resp = await this.rpc.sendJsonRequest("get_coinbase_tx_sum", {height: height, count: numBlocks});
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        let txSum = new MoneroMinerTxSum();
        txSum.setEmissionSum(new BigInteger(resp.result.emission_amount));
        txSum.setFeeSum(new BigInteger(resp.result.fee_amount));
        return txSum;
      }

      async getFeeEstimate(graceBlocks) {
        let resp = await this.rpc.sendJsonRequest("get_fee_estimate", {grace_blocks: graceBlocks});
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        let feeEstimate = new MoneroFeeEstimate();
        feeEstimate.setFee(new BigInteger(resp.result.fee));
        let fees = [];
        for (let i = 0; i < resp.result.fees.length; i++) fees.push(new BigInteger(resp.result.fees[i]));
        feeEstimate.setFees(fees);
        feeEstimate.setQuantizationMask(new BigInteger(resp.result.quantization_mask));
        return feeEstimate;
      }

      async submitTxHex(txHex, doNotRelay) {
        let resp = await this.rpc.sendPathRequest("send_raw_transaction", {tx_as_hex: txHex, do_not_relay: doNotRelay});
        let result = MoneroDaemonRpc._convertRpcSubmitTxResult(resp);

        // set isGood based on status
        try {
          MoneroDaemonRpc._checkResponseStatus(resp);
          result.setIsGood(true);
        } catch(e) {
          result.setIsGood(false);
        }
        return result;
      }

      async relayTxsByHash(txHashes) {
        let resp = await this.rpc.sendJsonRequest("relay_tx", {txids: txHashes});
        MoneroDaemonRpc._checkResponseStatus(resp.result);
      }

      async getTxPool() {

        // send rpc request
        let resp = await this.rpc.sendPathRequest("get_transaction_pool");
        MoneroDaemonRpc._checkResponseStatus(resp);

        // build txs
        let txs = [];
        if (resp.transactions) {
          for (let rpcTx of resp.transactions) {
            let tx = new MoneroTx();
            txs.push(tx);
            tx.setIsConfirmed(false);
            tx.setIsMinerTx(false);
            tx.setInTxPool(true);
            tx.setNumConfirmations(0);
            MoneroDaemonRpc._convertRpcTx(rpcTx, tx);
          }
        }

        return txs;
      }

      async getTxPoolHashes() {
        throw new MoneroError("Not implemented");
      }

      async getTxPoolBacklog() {
        throw new MoneroError("Not implemented");
      }

      async getTxPoolStats() {
        let resp = await this.rpc.sendPathRequest("get_transaction_pool_stats");
        MoneroDaemonRpc._checkResponseStatus(resp);
        return MoneroDaemonRpc._convertRpcTxPoolStats(resp.pool_stats);
      }

      async flushTxPool(hashes) {
        if (hashes) hashes = GenUtils.listify(hashes);
        let resp = await this.rpc.sendJsonRequest("flush_txpool", {txids: hashes});
        MoneroDaemonRpc._checkResponseStatus(resp.result);
      }

      async getKeyImageSpentStatuses(keyImages) {
        if (keyImages === undefined || keyImages.length === 0) throw new MoneroError("Must provide key images to check the status of");
        let resp = await this.rpc.sendPathRequest("is_key_image_spent", {key_images: keyImages});
        MoneroDaemonRpc._checkResponseStatus(resp);
        return resp.spent_status;
      }

      async getOutputHistogram(amounts, minCount, maxCount, isUnlocked, recentCutoff) {

        // send rpc request
        let resp = await this.rpc.sendJsonRequest("get_output_histogram", {
          amounts: amounts,
          min_count: minCount,
          max_count: maxCount,
          unlocked: isUnlocked,
          recent_cutoff: recentCutoff
        });
        MoneroDaemonRpc._checkResponseStatus(resp.result);

        // build histogram entries from response
        let entries = [];
        if (!resp.result.histogram) return entries;
        for (let rpcEntry of resp.result.histogram) {
          entries.push(MoneroDaemonRpc._convertRpcOutputHistogramEntry(rpcEntry));
        }
        return entries;
      }

      async getOutputDistribution(amounts, cumulative, startHeight, endHeight) {
        throw new MoneroError("Not implemented (response 'distribution' field is binary)");

    //    let amountStrs = [];
    //    for (let amount of amounts) amountStrs.push(amount.toJSValue());
    //    console.log(amountStrs);
    //    console.log(cumulative);
    //    console.log(startHeight);
    //    console.log(endHeight);
    //
    //    // send rpc request
    //    console.log("*********** SENDING REQUEST *************");
    //    if (startHeight === undefined) startHeight = 0;
    //    let resp = await this.rpc.sendJsonRequest("get_output_distribution", {
    //      amounts: amountStrs,
    //      cumulative: cumulative,
    //      from_height: startHeight,
    //      to_height: endHeight
    //    });
    //
    //    console.log("RESPONSE");
    //    console.log(resp);
    //
    //    // build distribution entries from response
    //    let entries = [];
    //    if (!resp.result.distributions) return entries;
    //    for (let rpcEntry of resp.result.distributions) {
    //      let entry = MoneroDaemonRpc._convertRpcOutputDistributionEntry(rpcEntry);
    //      entries.push(entry);
    //    }
    //    return entries;
      }

      async getInfo() {
        let resp = await this.rpc.sendJsonRequest("get_info");
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return MoneroDaemonRpc._convertRpcInfo(resp.result);
      }

      async getSyncInfo() {
        let resp = await this.rpc.sendJsonRequest("sync_info");
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return MoneroDaemonRpc._convertRpcSyncInfo(resp.result);
      }

      async getHardForkInfo() {
        let resp = await this.rpc.sendJsonRequest("hard_fork_info");
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        return MoneroDaemonRpc._convertRpcHardForkInfo(resp.result);
      }

      async getAltChains() {

    //    // mocked response for test
    //    let resp = {
    //        status: "OK",
    //        chains: [
    //          {
    //            block_hash: "697cf03c89a9b118f7bdf11b1b3a6a028d7b3617d2d0ed91322c5709acf75625",
    //            difficulty: 14114729638300280,
    //            height: 1562062,
    //            length: 2
    //          }
    //        ]
    //    }

        let resp = await this.rpc.sendJsonRequest("get_alternate_chains");
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        let chains = [];
        if (!resp.result.chains) return chains;
        for (let rpcChain of resp.result.chains) chains.push(MoneroDaemonRpc._convertRpcAltChain(rpcChain));
        return chains;
      }

      async getAltBlockHashes() {

    //    // mocked response for test
    //    let resp = {
    //        status: "OK",
    //        untrusted: false,
    //        blks_hashes: ["9c2277c5470234be8b32382cdf8094a103aba4fcd5e875a6fc159dc2ec00e011","637c0e0f0558e284493f38a5fcca3615db59458d90d3a5eff0a18ff59b83f46f","6f3adc174a2e8082819ebb965c96a095e3e8b63929ad9be2d705ad9c086a6b1c","697cf03c89a9b118f7bdf11b1b3a6a028d7b3617d2d0ed91322c5709acf75625"]
    //    }

        let resp = await this.rpc.sendPathRequest("get_alt_blocks_hashes");
        MoneroDaemonRpc._checkResponseStatus(resp);
        if (!resp.blks_hashes) return [];
        return resp.blks_hashes;
      }

      async getDownloadLimit() {
        return (await this._getBandwidthLimits())[0];
      }

      async setDownloadLimit(limit) {
        if (limit == -1) return await this.resetDownloadLimit();
        if (!(GenUtils.isInt(limit) && limit > 0)) throw new MoneroError("Download limit must be an integer greater than 0");
        return (await this._setBandwidthLimits(limit, 0))[0];
      }

      async resetDownloadLimit() {
        return (await this._setBandwidthLimits(-1, 0))[0];
      }

      async getUploadLimit() {
        return (await this._getBandwidthLimits())[1];
      }

      async setUploadLimit(limit) {
        if (limit == -1) return await this.resetUploadLimit();
        if (!(GenUtils.isInt(limit) && limit > 0)) throw new MoneroError("Upload limit must be an integer greater than 0");
        return (await this._setBandwidthLimits(0, limit))[1];
      }

      async resetUploadLimit() {
        return (await this._setBandwidthLimits(0, -1))[1];
      }

      async getPeers() {
        let resp = await this.rpc.sendJsonRequest("get_connections");
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        let peers = [];
        if (!resp.result.connections) return peers;
        for (let rpcConnection of resp.result.connections) {
          peers.push(MoneroDaemonRpc._convertRpcConnection(rpcConnection));
        }
        return peers;
      }

      async getKnownPeers() {

        // tx config
        let resp = await this.rpc.sendPathRequest("get_peer_list");
        MoneroDaemonRpc._checkResponseStatus(resp);

        // build peers
        let peers = [];
        if (resp.gray_list) {
          for (let rpcPeer of resp.gray_list) {
            let peer = MoneroDaemonRpc._convertRpcPeer(rpcPeer);
            peer.setIsOnline(false); // gray list means offline last checked
            peers.push(peer);
          }
        }
        if (resp.white_list) {
          for (let rpcPeer of resp.white_list) {
            let peer = MoneroDaemonRpc._convertRpcPeer(rpcPeer);
            peer.setIsOnline(true); // white list means online last checked
            peers.push(peer);
          }
        }
        return peers;
      }

      async setOutgoingPeerLimit(limit) {
        if (!(GenUtils.isInt(limit) && limit >= 0)) throw new MoneroError("Outgoing peer limit must be >= 0");
        let resp = await this.rpc.sendPathRequest("out_peers", {out_peers: limit});
        MoneroDaemonRpc._checkResponseStatus(resp);
      }

      async setIncomingPeerLimit(limit) {
        if (!(GenUtils.isInt(limit) && limit >= 0)) throw new MoneroError("Incoming peer limit must be >= 0");
        let resp = await this.rpc.sendPathRequest("in_peers", {in_peers: limit});
        MoneroDaemonRpc._checkResponseStatus(resp);
      }

      async getPeerBans() {
        let resp = await this.rpc.sendJsonRequest("get_bans");
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        let bans = [];
        for (let rpcBan of resp.result.bans) {
          let ban = new MoneroBan();
          ban.setHost(rpcBan.host);
          ban.setIp(rpcBan.ip);
          ban.setSeconds(rpcBan.seconds);
          bans.push(ban);
        }
        return bans;
      }

      async setPeerBans(bans) {
        let rpcBans = [];
        for (let ban of bans) rpcBans.push(MoneroDaemonRpc._convertToRpcBan(ban));
        let resp = await this.rpc.sendJsonRequest("set_bans", {bans: rpcBans});
        MoneroDaemonRpc._checkResponseStatus(resp.result);
      }

      async startMining(address, numThreads, isBackground, ignoreBattery) {
        assert(address, "Must provide address to mine to");
        assert(GenUtils.isInt(numThreads) && numThreads > 0, "Number of threads must be an integer greater than 0");
        assert(isBackground === undefined || typeof isBackground === "boolean");
        assert(ignoreBattery === undefined || typeof ignoreBattery === "boolean");
        let resp = await this.rpc.sendPathRequest("start_mining", {
          miner_address: address,
          threads_count: numThreads,
          do_background_mining: isBackground,
          ignore_battery: ignoreBattery,
        });
        MoneroDaemonRpc._checkResponseStatus(resp);
      }

      async stopMining() {
        let resp = await this.rpc.sendPathRequest("stop_mining");
        MoneroDaemonRpc._checkResponseStatus(resp);
      }

      async getMiningStatus() {
        let resp = await this.rpc.sendPathRequest("mining_status");
        MoneroDaemonRpc._checkResponseStatus(resp);
        return MoneroDaemonRpc._convertRpcMiningStatus(resp);
      }

      async submitBlocks(blockBlobs) {
        assert(Array.isArray(blockBlobs) && blockBlobs.length > 0, "Must provide an array of mined block blobs to submit");
        let resp = await this.rpc.sendJsonRequest("submit_block", blockBlobs);
        MoneroDaemonRpc._checkResponseStatus(resp.result);
      }

      async pruneBlockchain(check) {
        let resp = await this.rpc.sendJsonRequest("prune_blockchain", {check: check}, 0);
        MoneroDaemonRpc._checkResponseStatus(resp.result);
        let result = new MoneroPruneResult();
        result.setIsPruned(resp.result.pruned);
        result.setPruningSeed(resp.result.pruning_seed);
        return result;
      }

      async checkForUpdate() {
        let resp = await this.rpc.sendPathRequest("update", {command: "check"});
        MoneroDaemonRpc._checkResponseStatus(resp);
        return MoneroDaemonRpc._convertRpcUpdateCheckResult(resp);
      }

      async downloadUpdate(path) {
        let resp = await this.rpc.sendPathRequest("update", {command: "download", path: path});
        MoneroDaemonRpc._checkResponseStatus(resp);
        return MoneroDaemonRpc._convertRpcUpdateDownloadResult(resp);
      }

      async stop() {
        let resp = await this.rpc.sendPathRequest("stop_daemon");
        MoneroDaemonRpc._checkResponseStatus(resp);
      }

      async waitForNextBlockHeader() {
        let that = this;
        return new Promise(async function(resolve) {
          await that.addListener(new class extends MoneroDaemonListener {
            async onBlockHeader(header) {
              await that.removeListener(this);
              resolve(header);
            }
          });
        });
      }

      // ----------- ADD JSDOC FOR SUPPORTED DEFAULT IMPLEMENTATIONS --------------

      async getTx() { return super.getTx(...arguments); }
      async getTxHex() { return super.getTxHex(...arguments); }
      async getKeyImageSpentStatus() { return super.getKeyImageSpentStatus(...arguments); }
      async setPeerBan() { return super.setPeerBan(...arguments); }
      async submitBlock() { return super.submitBlock(...arguments); }

      // ------------------------------- PRIVATE ----------------------------------

      _refreshListening() {
        if (this.pollListener == undefined && this.listeners.length) this.pollListener = new DaemonPoller(this);
        if (this.pollListener !== undefined) this.pollListener.setIsPolling(this.listeners.length > 0);
      }

      async _getBandwidthLimits() {
        let resp = await this.rpc.sendPathRequest("get_limit");
        MoneroDaemonRpc._checkResponseStatus(resp);
        return [resp.limit_down, resp.limit_up];
      }

      async _setBandwidthLimits(downLimit, upLimit) {
        if (downLimit === undefined) downLimit = 0;
        if (upLimit === undefined) upLimit = 0;
        let resp = await this.rpc.sendPathRequest("set_limit", {limit_down: downLimit, limit_up: upLimit});
        MoneroDaemonRpc._checkResponseStatus(resp);
        return [resp.limit_down, resp.limit_up];
      }

      /**
       * Get a contiguous chunk of blocks starting from a given height up to a maximum
       * height or amount of block data fetched from the blockchain, whichever comes first.
       *
       * @param {number} startHeight - start height to retrieve blocks (default 0)
       * @param {number} maxHeight - maximum end height to retrieve blocks (default blockchain height)
       * @param {number} maxReqSize - maximum amount of block data to fetch from the blockchain in bytes (default 3,000,000 bytes)
       * @return {MoneroBlock[]} are the resulting chunk of blocks
       */
      async _getMaxBlocks(startHeight, maxHeight, maxReqSize) {
        if (startHeight === undefined) startHeight = 0;
        if (maxHeight === undefined) maxHeight = await this.getHeight() - 1;
        if (maxReqSize === undefined) maxReqSize = MoneroDaemonRpc.MAX_REQ_SIZE;

        // determine end height to fetch
        let reqSize = 0;
        let endHeight = startHeight - 1;
        while (reqSize < maxReqSize && endHeight < maxHeight) {

          // get header of next block
          let header = await this._getBlockHeaderByHeightCached(endHeight + 1, maxHeight);

          // block cannot be bigger than max request size
          assert(header.getSize() <= maxReqSize, "Block exceeds maximum request size: " + header.getSize());

          // done iterating if fetching block would exceed max request size
          if (reqSize + header.getSize() > maxReqSize) break;

          // otherwise block is included
          reqSize += header.getSize();
          endHeight++;
        }
        return endHeight >= startHeight ? await this.getBlocksByRange(startHeight, endHeight) : [];
      }

      /**
       * Retrieves a header by height from the cache or fetches and caches a header
       * range if not already in the cache.
       *
       * @param {number} height - height of the header to retrieve from the cache
       * @param {number} maxHeight - maximum height of headers to cache
       */
      async _getBlockHeaderByHeightCached(height, maxHeight) {

        // get header from cache
        let cachedHeader = this.cachedHeaders[height];
        if (cachedHeader) return cachedHeader;

        // fetch and cache headers if not in cache
        let endHeight = Math.min(maxHeight, height + MoneroDaemonRpc.NUM_HEADERS_PER_REQ - 1);  // TODO: could specify end height to cache to optimize small requests (would like to have time profiling in place though)
        let headers = await this.getBlockHeadersByRange(height, endHeight);
        for (let header of headers) {
          this.cachedHeaders[header.getHeight()] = header;
        }

        // return the cached header
        return this.cachedHeaders[height];
      }

      // --------------------------------- STATIC ---------------------------------

      static _normalizeConfig(uriOrConfigOrConnection, username, password, rejectUnauthorized, pollInterval, proxyToWorker) {
        let config;
        if (typeof uriOrConfigOrConnection === "string") config = {uri: uriOrConfigOrConnection, username: username, password: password, proxyToWorker: proxyToWorker, rejectUnauthorized: rejectUnauthorized, pollInterval: pollInterval};
        else {
          if (typeof uriOrConfigOrConnection !== "object") throw new MoneroError("Invalid configuration to create rpc client; must be string, object, or MoneroRpcConnection");
          if (username || password || rejectUnauthorized || pollInterval || proxyToWorker) throw new MoneroError("Can provide config object or params or new MoneroDaemonRpc(...) but not both");
          if (uriOrConfigOrConnection instanceof MoneroRpcConnection) config = Object.assign({}, uriOrConfigOrConnection.getConfig());
          else config = Object.assign({}, uriOrConfigOrConnection);
        }
        if (config.server) {
          config = Object.assign(config, new MoneroRpcConnection(config.server).getConfig());
          delete config.server;
        }
        if (config.pollInterval === undefined) config.pollInterval = 5000; // TODO: move to config
        if (config.proxyToWorker === undefined) config.proxyToWorker = true;
        return config;
      }

      static _checkResponseStatus(resp) {
        if (resp.status !== "OK") throw new MoneroError(resp.status);
      }

      static _convertRpcBlockHeader(rpcHeader) {
        if (!rpcHeader) return undefined;
        let header = new MoneroBlockHeader();
        for (let key of Object.keys(rpcHeader)) {
          let val = rpcHeader[key];
          if (key === "block_size") GenUtils.safeSet(header, header.getSize, header.setSize, val);
          else if (key === "depth") GenUtils.safeSet(header, header.getDepth, header.setDepth, val);
          else if (key === "difficulty") { }  // handled by wide_difficulty
          else if (key === "cumulative_difficulty") { } // handled by wide_cumulative_difficulty
          else if (key === "difficulty_top64") { }  // handled by wide_difficulty
          else if (key === "cumulative_difficulty_top64") { } // handled by wide_cumulative_difficulty
          else if (key === "wide_difficulty") header.setDifficulty(GenUtils.reconcile(header.getDifficulty(), MoneroDaemonRpc._prefixedHexToBI(val)));
          else if (key === "wide_cumulative_difficulty") header.setCumulativeDifficulty(GenUtils.reconcile(header.getCumulativeDifficulty(), MoneroDaemonRpc._prefixedHexToBI(val)));
          else if (key === "hash") GenUtils.safeSet(header, header.getHash, header.setHash, val);
          else if (key === "height") GenUtils.safeSet(header, header.getHeight, header.setHeight, val);
          else if (key === "major_version") GenUtils.safeSet(header, header.getMajorVersion, header.setMajorVersion, val);
          else if (key === "minor_version") GenUtils.safeSet(header, header.getMinorVersion, header.setMinorVersion, val);
          else if (key === "nonce") GenUtils.safeSet(header, header.getNonce, header.setNonce, val);
          else if (key === "num_txes") GenUtils.safeSet(header, header.getNumTxs, header.setNumTxs, val);
          else if (key === "orphan_status") GenUtils.safeSet(header, header.getOrphanStatus, header.setOrphanStatus, val);
          else if (key === "prev_hash" || key === "prev_id") GenUtils.safeSet(header, header.getPrevHash, header.setPrevHash, val);
          else if (key === "reward") GenUtils.safeSet(header, header.getReward, header.setReward, BigInteger.parse(val));
          else if (key === "timestamp") GenUtils.safeSet(header, header.getTimestamp, header.setTimestamp, val);
          else if (key === "block_weight") GenUtils.safeSet(header, header.getWeight, header.setWeight, val);
          else if (key === "long_term_weight") GenUtils.safeSet(header, header.getLongTermWeight, header.setLongTermWeight, val);
          else if (key === "pow_hash") GenUtils.safeSet(header, header.getPowHash, header.setPowHash, val === "" ? undefined : val);
          else if (key === "tx_hashes") {}  // used in block model, not header model
          else if (key === "miner_tx") {}   // used in block model, not header model
          else if (key === "miner_tx_hash") header.setMinerTxHash(val);
          else console.log("WARNING: ignoring unexpected block header field: '" + key + "': " + val);
        }
        return header;
      }

      static _convertRpcBlock(rpcBlock) {

        // build block
        let block = new MoneroBlock(MoneroDaemonRpc._convertRpcBlockHeader(rpcBlock.block_header ? rpcBlock.block_header : rpcBlock));
        block.setHex(rpcBlock.blob);
        block.setTxHashes(rpcBlock.tx_hashes === undefined ? [] : rpcBlock.tx_hashes);

        // build miner tx
        let rpcMinerTx = rpcBlock.json ? JSON.parse(rpcBlock.json).miner_tx : rpcBlock.miner_tx;  // may need to be parsed from json
        let minerTx = new MoneroTx();
        block.setMinerTx(minerTx);
        minerTx.setIsConfirmed(true);
        minerTx.setIsMinerTx(true);
        MoneroDaemonRpc._convertRpcTx(rpcMinerTx, minerTx);

        return block;
      }

      /**
       * Transfers RPC tx fields to a given MoneroTx without overwriting previous values.
       *
       * TODO: switch from safe set
       *
       * @param rpcTx - RPC map containing transaction fields
       * @param tx  - MoneroTx to populate with values (optional)
       * @returns tx - same tx that was passed in or a new one if none given
       */
      static _convertRpcTx(rpcTx, tx) {
        if (rpcTx === undefined) return undefined;
        if (tx === undefined) tx = new MoneroTx();

    //    console.log("******** BUILDING TX ***********");
    //    console.log(rpcTx);
    //    console.log(tx.toString());

        // initialize from rpc map
        let header;
        for (let key of Object.keys(rpcTx)) {
          let val = rpcTx[key];
          if (key === "tx_hash" || key === "id_hash") GenUtils.safeSet(tx, tx.getHash, tx.setHash, val);
          else if (key === "block_timestamp") {
            if (!header) header = new MoneroBlockHeader();
            GenUtils.safeSet(header, header.getTimestamp, header.setTimestamp, val);
          }
          else if (key === "block_height") {
            if (!header) header = new MoneroBlockHeader();
            GenUtils.safeSet(header, header.getHeight, header.setHeight, val);
          }
          else if (key === "last_relayed_time") GenUtils.safeSet(tx, tx.getLastRelayedTimestamp, tx.setLastRelayedTimestamp, val);
          else if (key === "receive_time" || key === "received_timestamp") GenUtils.safeSet(tx, tx.getReceivedTimestamp, tx.setReceivedTimestamp, val);
          else if (key === "confirmations") GenUtils.safeSet(tx, tx.getNumConfirmations, tx.setNumConfirmations, val);
          else if (key === "in_pool") {
            GenUtils.safeSet(tx, tx.isConfirmed, tx.setIsConfirmed, !val);
            GenUtils.safeSet(tx, tx.inTxPool, tx.setInTxPool, val);
          }
          else if (key === "double_spend_seen") GenUtils.safeSet(tx, tx.isDoubleSpendSeen, tx.setIsDoubleSpend, val);
          else if (key === "version") GenUtils.safeSet(tx, tx.getVersion, tx.setVersion, val);
          else if (key === "extra") {
            if (typeof val === "string") console.log("WARNING: extra field as string not being asigned to int[]: " + key + ": " + val); // TODO: how to set string to int[]? - or, extra is string which can encode int[]
            else GenUtils.safeSet(tx, tx.getExtra, tx.setExtra, val);
          }
          else if (key === "vin") {
            if (val.length !== 1 || !val[0].gen) {  // ignore miner input TODO: why?
              tx.setInputs(val.map(rpcVin => MoneroDaemonRpc._convertRpcOutput(rpcVin, tx)));
            }
          }
          else if (key === "vout") tx.setOutputs(val.map(rpcOutput => MoneroDaemonRpc._convertRpcOutput(rpcOutput, tx)));
          else if (key === "rct_signatures") {
            GenUtils.safeSet(tx, tx.getRctSignatures, tx.setRctSignatures, val);
            if (val.txnFee) GenUtils.safeSet(tx, tx.getFee, tx.setFee, BigInteger.parse(val.txnFee));
          }
          else if (key === "rctsig_prunable") GenUtils.safeSet(tx, tx.getRctSigPrunable, tx.setRctSigPrunable, val);
          else if (key === "unlock_time") GenUtils.safeSet(tx, tx.getUnlockTime, tx.setUnlockTime, val);
          else if (key === "as_json" || key === "tx_json") { }  // handled last so tx is as initialized as possible
          else if (key === "as_hex" || key === "tx_blob") GenUtils.safeSet(tx, tx.getFullHex, tx.setFullHex, val ? val : undefined);
          else if (key === "blob_size") GenUtils.safeSet(tx, tx.getSize, tx.setSize, val);
          else if (key === "weight") GenUtils.safeSet(tx, tx.getWeight, tx.setWeight, val);
          else if (key === "fee") GenUtils.safeSet(tx, tx.getFee, tx.setFee, BigInteger.parse(val));
          else if (key === "relayed") GenUtils.safeSet(tx, tx.isRelayed, tx.setIsRelayed, val);
          else if (key === "output_indices") GenUtils.safeSet(tx, tx.getOutputIndices, tx.setOutputIndices, val);
          else if (key === "do_not_relay") GenUtils.safeSet(tx, tx.getRelay, tx.setRelay, !val);
          else if (key === "kept_by_block") GenUtils.safeSet(tx, tx.isKeptByBlock, tx.setIsKeptByBlock, val);
          else if (key === "signatures") GenUtils.safeSet(tx, tx.getSignatures, tx.setSignatures, val);
          else if (key === "last_failed_height") {
            if (val === 0) GenUtils.safeSet(tx, tx.isFailed, tx.setIsFailed, false);
            else {
              GenUtils.safeSet(tx, tx.isFailed, tx.setIsFailed, true);
              GenUtils.safeSet(tx, tx.getLastFailedHeight, tx.setLastFailedHeight, val);
            }
          }
          else if (key === "last_failed_id_hash") {
            if (val === MoneroDaemonRpc.DEFAULT_ID) GenUtils.safeSet(tx, tx.isFailed, tx.setIsFailed, false);
            else {
              GenUtils.safeSet(tx, tx.isFailed, tx.setIsFailed, true);
              GenUtils.safeSet(tx, tx.getLastFailedHash, tx.setLastFailedHash, val);
            }
          }
          else if (key === "max_used_block_height") GenUtils.safeSet(tx, tx.getMaxUsedBlockHeight, tx.setMaxUsedBlockHeight, val);
          else if (key === "max_used_block_id_hash") GenUtils.safeSet(tx, tx.getMaxUsedBlockHash, tx.setMaxUsedBlockHash, val);
          else if (key === "prunable_hash") GenUtils.safeSet(tx, tx.getPrunableHash, tx.setPrunableHash, val ? val : undefined);
          else if (key === "prunable_as_hex") GenUtils.safeSet(tx, tx.getPrunableHex, tx.setPrunableHex, val ? val : undefined);
          else if (key === "pruned_as_hex") GenUtils.safeSet(tx, tx.getPrunedHex, tx.setPrunedHex, val ? val : undefined);
          else console.log("WARNING: ignoring unexpected field in rpc tx: " + key + ": " + val);
        }

        // link block and tx
        if (header) tx.setBlock(new MoneroBlock(header).setTxs([tx]));

        // TODO monerod: unconfirmed txs misreport block height and timestamp
        if (tx.getBlock() && tx.getBlock().getHeight() !== undefined && tx.getBlock().getHeight() === tx.getBlock().getTimestamp()) {
          tx.setBlock(undefined);
          tx.setIsConfirmed(false);
        }

        // initialize remaining known fields
        if (tx.isConfirmed()) {
          GenUtils.safeSet(tx, tx.isRelayed, tx.setIsRelayed, true);
          GenUtils.safeSet(tx, tx.getRelay, tx.setRelay, true);
          GenUtils.safeSet(tx, tx.isFailed, tx.setIsFailed, false);
        } else {
          tx.setNumConfirmations(0);
        }
        if (tx.isFailed() === undefined) tx.setIsFailed(false);
        if (tx.getOutputIndices() && tx.getOutputs())  {
          assert.equal(tx.getOutputs().length, tx.getOutputIndices().length);
          for (let i = 0; i < tx.getOutputs().length; i++) {
            tx.getOutputs()[i].setIndex(tx.getOutputIndices()[i]);  // transfer output indices to outputs
          }
        }
        if (rpcTx.as_json) MoneroDaemonRpc._convertRpcTx(JSON.parse(rpcTx.as_json), tx);
        if (rpcTx.tx_json) MoneroDaemonRpc._convertRpcTx(JSON.parse(rpcTx.tx_json), tx);
        if (!tx.isRelayed()) tx.setLastRelayedTimestamp(undefined);  // TODO monerod: returns last_relayed_timestamp despite relayed: false, self inconsistent

        // return built transaction
        return tx;
      }

      static _convertRpcOutput(rpcOutput, tx) {
        let output = new MoneroOutput();
        output.setTx(tx);
        for (let key of Object.keys(rpcOutput)) {
          let val = rpcOutput[key];
          if (key === "gen") throw new MoneroError("Output with 'gen' from daemon rpc is miner tx which we ignore (i.e. each miner input is undefined)");
          else if (key === "key") {
            GenUtils.safeSet(output, output.getAmount, output.setAmount, new BigInteger(val.amount));
            GenUtils.safeSet(output, output.getKeyImage, output.setKeyImage, new MoneroKeyImage(val.k_image));
            GenUtils.safeSet(output, output.getRingOutputIndices, output.setRingOutputIndices, val.key_offsets);
          }
          else if (key === "amount") GenUtils.safeSet(output, output.getAmount, output.setAmount, BigInteger.parse(val));
          else if (key === "target") {
            let pubKey = val.key === undefined ? val.tagged_key.key : val.key; // TODO (monerod): rpc json uses {tagged_key={key=...}}, binary blocks use {key=...}
            GenUtils.safeSet(output, output.getStealthPublicKey, output.setStealthPublicKey, pubKey);
          }
          else console.log("WARNING: ignoring unexpected field output: " + key + ": " + val);
        }
        return output;
      }

      static _convertRpcBlockTemplate(rpcTemplate) {
        let template = new MoneroBlockTemplate();
        for (let key of Object.keys(rpcTemplate)) {
          let val = rpcTemplate[key];
          if (key === "blockhashing_blob") template.setBlockTemplateBlob(val);
          else if (key === "blocktemplate_blob") template.setBlockHashingBlob(val);
          else if (key === "difficulty") template.setDifficulty(BigInteger.parse(val));
          else if (key === "expected_reward") template.setExpectedReward(val);
          else if (key === "difficulty") { }  // handled by wide_difficulty
          else if (key === "difficulty_top64") { }  // handled by wide_difficulty
          else if (key === "wide_difficulty") template.setDifficulty(GenUtils.reconcile(template.getDifficulty(), MoneroDaemonRpc._prefixedHexToBI(val)));
          else if (key === "height") template.setHeight(val);
          else if (key === "prev_hash") template.setPrevHash(val);
          else if (key === "reserved_offset") template.setReservedOffset(val);
          else if (key === "status") {}  // handled elsewhere
          else if (key === "untrusted") {}  // handled elsewhere
          else if (key === "seed_height") template.setSeedHeight(val);
          else if (key === "seed_hash") template.setSeedHash(val);
          else if (key === "next_seed_hash") template.setNextSeedHash(val);
          else console.log("WARNING: ignoring unexpected field in block template: " + key + ": " + val);
        }
        if ("" === template.getNextSeedHash()) template.setNextSeedHash(undefined);
        return template;
      }

      static _convertRpcInfo(rpcInfo) {
        if (!rpcInfo) return undefined;
        let info = new MoneroDaemonInfo();
        for (let key of Object.keys(rpcInfo)) {
          let val = rpcInfo[key];
          if (key === "version") info.setVersion(val);
          else if (key === "alt_blocks_count") info.setNumAltBlocks(val);
          else if (key === "block_size_limit") info.setBlockSizeLimit(val);
          else if (key === "block_size_median") info.setBlockSizeMedian(val);
          else if (key === "block_weight_limit") info.setBlockWeightLimit(val);
          else if (key === "block_weight_median") info.setBlockWeightMedian(val);
          else if (key === "bootstrap_daemon_address") { if (val) info.setBootstrapDaemonAddress(val); }
          else if (key === "difficulty") { }  // handled by wide_difficulty
          else if (key === "cumulative_difficulty") { } // handled by wide_cumulative_difficulty
          else if (key === "difficulty_top64") { }  // handled by wide_difficulty
          else if (key === "cumulative_difficulty_top64") { } // handled by wide_cumulative_difficulty
          else if (key === "wide_difficulty") info.setDifficulty(GenUtils.reconcile(info.getDifficulty(), MoneroDaemonRpc._prefixedHexToBI(val)));
          else if (key === "wide_cumulative_difficulty") info.setCumulativeDifficulty(GenUtils.reconcile(info.getCumulativeDifficulty(), MoneroDaemonRpc._prefixedHexToBI(val)));
          else if (key === "free_space") info.setFreeSpace(BigInteger.parse(val));
          else if (key === "database_size") info.setDatabaseSize(val);
          else if (key === "grey_peerlist_size") info.setNumOfflinePeers(val);
          else if (key === "height") info.setHeight(val);
          else if (key === "height_without_bootstrap") info.setHeightWithoutBootstrap(val);
          else if (key === "incoming_connections_count") info.setNumIncomingConnections(val);
          else if (key === "offline") info.setIsOffline(val);
          else if (key === "outgoing_connections_count") info.setNumOutgoingConnections(val);
          else if (key === "rpc_connections_count") info.setNumRpcConnections(val);
          else if (key === "start_time") info.setStartTimestamp(val);
          else if (key === "adjusted_time") info.setAdjustedTimestamp(val);
          else if (key === "status") {}  // handled elsewhere
          else if (key === "target") info.setTarget(val);
          else if (key === "target_height") info.setTargetHeight(val);
          else if (key === "top_block_hash") info.setTopBlockHash(val);
          else if (key === "tx_count") info.setNumTxs(val);
          else if (key === "tx_pool_size") info.setNumTxsPool(val);
          else if (key === "untrusted") {} // handled elsewhere
          else if (key === "was_bootstrap_ever_used") info.setWasBootstrapEverUsed(val);
          else if (key === "white_peerlist_size") info.setNumOnlinePeers(val);
          else if (key === "update_available") info.setUpdateAvailable(val);
          else if (key === "nettype") GenUtils.safeSet(info, info.getNetworkType, info.setNetworkType, MoneroDaemon.parseNetworkType(val));
          else if (key === "mainnet") { if (val) GenUtils.safeSet(info, info.getNetworkType, info.setNetworkType, MoneroNetworkType.MAINNET); }
          else if (key === "testnet") { if (val) GenUtils.safeSet(info, info.getNetworkType, info.setNetworkType, MoneroNetworkType.TESTNET); }
          else if (key === "stagenet") { if (val) GenUtils.safeSet(info, info.getNetworkType, info.setNetworkType, MoneroNetworkType.STAGENET); }
          else if (key === "credits") info.setCredits(BigInteger.parse(val));
          else if (key === "top_block_hash" || key === "top_hash") info.setTopBlockHash(GenUtils.reconcile(info.getTopBlockHash(), "" === val ? undefined : val))
          else if (key === "busy_syncing") info.setIsBusySyncing(val);
          else if (key === "synchronized") info.setIsSynchronized(val);
          else if (key === "restricted") info.setIsRestricted(val);
          else console.log("WARNING: Ignoring unexpected info field: " + key + ": " + val);
        }
        return info;
      }

      /**
       * Initializes sync info from RPC sync info.
       *
       * @param rpcSyncInfo - rpc map to initialize the sync info from
       * @return {MoneroDaemonSyncInfo} is sync info initialized from the map
       */
      static _convertRpcSyncInfo(rpcSyncInfo) {
        let syncInfo = new MoneroDaemonSyncInfo();
        for (let key of Object.keys(rpcSyncInfo)) {
          let val = rpcSyncInfo[key];
          if (key === "height") syncInfo.setHeight(val);
          else if (key === "peers") {
            syncInfo.setPeers([]);
            let rpcConnections = val;
            for (let rpcConnection of rpcConnections) {
              syncInfo.getPeers().push(MoneroDaemonRpc._convertRpcConnection(rpcConnection.info));
            }
          }
          else if (key === "spans") {
            syncInfo.setSpans([]);
            let rpcSpans = val;
            for (let rpcSpan of rpcSpans) {
              syncInfo.getSpans().push(MoneroDaemonRpc._convertRpcConnectionSpan(rpcSpan));
            }
          } else if (key === "status") {}   // handled elsewhere
          else if (key === "target_height") syncInfo.setTargetHeight(BigInteger.parse(val));
          else if (key === "next_needed_pruning_seed") syncInfo.setNextNeededPruningSeed(val);
          else if (key === "overview") {  // this returns [] without pruning
            let overview;
            try {
              overview = JSON.parse(val);
              if (overview !== undefined && overview.length > 0) console.error("Ignoring non-empty 'overview' field (not implemented): " + overview); // TODO
            } catch (e) {
              console.error("Failed to parse 'overview' field: " + overview + ": " + e.message);
            }
          }
          else if (key === "credits") syncInfo.setCredits(BigInteger.parse(val));
          else if (key === "top_hash") syncInfo.setTopBlockHash("" === val ? undefined : val);
          else if (key === "untrusted") {}  // handled elsewhere
          else console.log("WARNING: ignoring unexpected field in sync info: " + key + ": " + val);
        }
        return syncInfo;
      }

      static _convertRpcHardForkInfo(rpcHardForkInfo) {
        let info = new MoneroHardForkInfo();
        for (let key of Object.keys(rpcHardForkInfo)) {
          let val = rpcHardForkInfo[key];
          if (key === "earliest_height") info.setEarliestHeight(val);
          else if (key === "enabled") info.setIsEnabled(val);
          else if (key === "state") info.setState(val);
          else if (key === "status") {}     // handled elsewhere
          else if (key === "untrusted") {}  // handled elsewhere
          else if (key === "threshold") info.setThreshold(val);
          else if (key === "version") info.setVersion(val);
          else if (key === "votes") info.setNumVotes(val);
          else if (key === "voting") info.setVoting(val);
          else if (key === "window") info.setWindow(val);
          else if (key === "credits") info.setCredits(BigInteger.parse(val));
          else if (key === "top_hash") info.setTopBlockHash("" === val ? undefined : val);
          else console.log("WARNING: ignoring unexpected field in hard fork info: " + key + ": " + val);
        }
        return info;
      }

      static _convertRpcConnectionSpan(rpcConnectionSpan) {
        let span = new MoneroConnectionSpan();
        for (let key of Object.keys(rpcConnectionSpan)) {
          let val = rpcConnectionSpan[key];
          if (key === "connection_id") span.setConnectionId(val);
          else if (key === "nblocks") span.setNumBlocks(val);
          else if (key === "rate") span.setRate(val);
          else if (key === "remote_address") { if (val !== "") span.setRemoteAddress(val); }
          else if (key === "size") span.setSize(val);
          else if (key === "speed") span.setSpeed(val);
          else if (key === "start_block_height") span.setStartHeight(val);
          else console.log("WARNING: ignoring unexpected field in daemon connection span: " + key + ": " + val);
        }
        return span;
      }

      static _convertRpcOutputHistogramEntry(rpcEntry) {
        let entry = new MoneroOutputHistogramEntry();
        for (let key of Object.keys(rpcEntry)) {
          let val = rpcEntry[key];
          if (key === "amount") entry.setAmount(BigInteger.parse(val));
          else if (key === "total_instances") entry.setNumInstances(val);
          else if (key === "unlocked_instances") entry.setNumUnlockedInstances(val);
          else if (key === "recent_instances") entry.setNumRecentInstances(val);
          else console.log("WARNING: ignoring unexpected field in output histogram: " + key + ": " + val);
        }
        return entry;
      }

      static _convertRpcSubmitTxResult(rpcResult) {
        assert(rpcResult);
        let result = new MoneroSubmitTxResult();
        for (let key of Object.keys(rpcResult)) {
          let val = rpcResult[key];
          if (key === "double_spend") result.setIsDoubleSpend(val);
          else if (key === "fee_too_low") result.setIsFeeTooLow(val);
          else if (key === "invalid_input") result.setHasInvalidInput(val);
          else if (key === "invalid_output") result.setHasInvalidOutput(val);
          else if (key === "too_few_outputs") result.setHasTooFewOutputs(val);
          else if (key === "low_mixin") result.setIsMixinTooLow(val);
          else if (key === "not_relayed") result.setIsRelayed(!val);
          else if (key === "overspend") result.setIsOverspend(val);
          else if (key === "reason") result.setReason(val === "" ? undefined : val);
          else if (key === "too_big") result.setIsTooBig(val);
          else if (key === "sanity_check_failed") result.setSanityCheckFailed(val);
          else if (key === "credits") result.setCredits(BigInteger.parse(val))
          else if (key === "status" || key === "untrusted") {}  // handled elsewhere
          else if (key === "top_hash") result.setTopBlockHash("" === val ? undefined : val);
          else if (key === "tx_extra_too_big") result.setIsTxExtraTooBig(val);
          else console.log("WARNING: ignoring unexpected field in submit tx hex result: " + key + ": " + val);
        }
        return result;
      }

      static _convertRpcTxPoolStats(rpcStats) {
        assert(rpcStats);
        let stats = new MoneroTxPoolStats();
        for (let key of Object.keys(rpcStats)) {
          let val = rpcStats[key];
          if (key === "bytes_max") stats.setBytesMax(val);
          else if (key === "bytes_med") stats.setBytesMed(val);
          else if (key === "bytes_min") stats.setBytesMin(val);
          else if (key === "bytes_total") stats.setBytesTotal(val);
          else if (key === "histo_98pc") stats.setHisto98pc(val);
          else if (key === "num_10m") stats.setNum10m(val);
          else if (key === "num_double_spends") stats.setNumDoubleSpends(val);
          else if (key === "num_failing") stats.setNumFailing(val);
          else if (key === "num_not_relayed") stats.setNumNotRelayed(val);
          else if (key === "oldest") stats.setOldestTimestamp(val);
          else if (key === "txs_total") stats.setNumTxs(val);
          else if (key === "fee_total") stats.setFeeTotal(BigInteger.parse(val));
          else if (key === "histo") {
            stats.setHisto(new Map());
            for (let elem of val) stats.getHisto().set(elem.bytes, elem.txs);
          }
          else console.log("WARNING: ignoring unexpected field in tx pool stats: " + key + ": " + val);
        }

        // uninitialize some stats if not applicable
        if (stats.getHisto98pc() === 0) stats.setHisto98pc(undefined);
        if (stats.getNumTxs() === 0) {
          stats.setBytesMin(undefined);
          stats.setBytesMed(undefined);
          stats.setBytesMax(undefined);
          stats.setHisto98pc(undefined);
          stats.setOldestTimestamp(undefined);
        }

        return stats;
      }

      static _convertRpcAltChain(rpcChain) {
        assert(rpcChain);
        let chain = new MoneroAltChain();
        for (let key of Object.keys(rpcChain)) {
          let val = rpcChain[key];
          if (key === "block_hash") {}  // using block_hashes instead
          else if (key === "difficulty") { } // handled by wide_difficulty
          else if (key === "difficulty_top64") { }  // handled by wide_difficulty
          else if (key === "wide_difficulty") chain.setDifficulty(GenUtils.reconcile(chain.getDifficulty(), MoneroDaemonRpc._prefixedHexToBI(val)));
          else if (key === "height") chain.setHeight(val);
          else if (key === "length") chain.setLength(val);
          else if (key === "block_hashes") chain.setBlockHashes(val);
          else if (key === "main_chain_parent_block") chain.setMainChainParentBlockHash(val);
          else console.log("WARNING: ignoring unexpected field in alternative chain: " + key + ": " + val);
        }
        return chain;
      }

      static _convertRpcPeer(rpcPeer) {
        assert(rpcPeer);
        let peer = new MoneroPeer();
        for (let key of Object.keys(rpcPeer)) {
          let val = rpcPeer[key];
          if (key === "host") peer.setHost(val);
          else if (key === "id") peer.setId("" + val);  // TODO monero-wallet-rpc: peer id is BigInteger but string in `get_connections`
          else if (key === "ip") {} // host used instead which is consistently a string
          else if (key === "last_seen") peer.setLastSeenTimestamp(val);
          else if (key === "port") peer.setPort(val);
          else if (key === "rpc_port") peer.setRpcPort(val);
          else if (key === "pruning_seed") peer.setPruningSeed(val);
          else if (key === "rpc_credits_per_hash") peer.setRpcCreditsPerHash(BigInteger.parse(val));
          else console.log("WARNING: ignoring unexpected field in rpc peer: " + key + ": " + val);
        }
        return peer;
      }

      static _convertRpcConnection(rpcConnection) {
        let peer = new MoneroPeer();
        peer.setIsOnline(true);
        for (let key of Object.keys(rpcConnection)) {
          let val = rpcConnection[key];
          if (key === "address") peer.setAddress(val);
          else if (key === "avg_download") peer.setAvgDownload(val);
          else if (key === "avg_upload") peer.setAvgUpload(val);
          else if (key === "connection_id") peer.setId(val);
          else if (key === "current_download") peer.setCurrentDownload(val);
          else if (key === "current_upload") peer.setCurrentUpload(val);
          else if (key === "height") peer.setHeight(val);
          else if (key === "host") peer.setHost(val);
          else if (key === "ip") {} // host used instead which is consistently a string
          else if (key === "incoming") peer.setIsIncoming(val);
          else if (key === "live_time") peer.setLiveTime(val);
          else if (key === "local_ip") peer.setIsLocalIp(val);
          else if (key === "localhost") peer.setIsLocalHost(val);
          else if (key === "peer_id") peer.setId(val);
          else if (key === "port") peer.setPort(parseInt(val));
          else if (key === "rpc_port") peer.setRpcPort(val);
          else if (key === "recv_count") peer.setNumReceives(val);
          else if (key === "recv_idle_time") peer.setReceiveIdleTime(val);
          else if (key === "send_count") peer.setNumSends(val);
          else if (key === "send_idle_time") peer.setSendIdleTime(val);
          else if (key === "state") peer.setState(val);
          else if (key === "support_flags") peer.setNumSupportFlags(val);
          else if (key === "pruning_seed") peer.setPruningSeed(val);
          else if (key === "rpc_credits_per_hash") peer.setRpcCreditsPerHash(BigInteger.parse(val));
          else if (key === "address_type") peer.setType(val);
          else console.log("WARNING: ignoring unexpected field in peer: " + key + ": " + val);
        }
        return peer;
      }

      static _convertToRpcBan(ban) {
        let rpcBan = {};
        rpcBan.host = ban.getHost();
        rpcBan.ip = ban.getIp();
        rpcBan.ban = ban.isBanned();
        rpcBan.seconds = ban.getSeconds();
        return rpcBan;
      }

      static _convertRpcMiningStatus(rpcStatus) {
        let status = new MoneroMiningStatus();
        status.setIsActive(rpcStatus.active);
        status.setSpeed(rpcStatus.speed);
        status.setNumThreads(rpcStatus.threads_count);
        if (rpcStatus.active) {
          status.setAddress(rpcStatus.address);
          status.setIsBackground(rpcStatus.is_background_mining_enabled);
        }
        return status;
      }

      static _convertRpcUpdateCheckResult(rpcResult) {
        assert(rpcResult);
        let result = new MoneroDaemonUpdateCheckResult();
        for (let key of Object.keys(rpcResult)) {
          let val = rpcResult[key];
          if (key === "auto_uri") result.setAutoUri(val);
          else if (key === "hash") result.setHash(val);
          else if (key === "path") {} // handled elsewhere
          else if (key === "status") {} // handled elsewhere
          else if (key === "update") result.setIsUpdateAvailable(val);
          else if (key === "user_uri") result.setUserUri(val);
          else if (key === "version") result.setVersion(val);
          else if (key === "untrusted") {} // handled elsewhere
          else console.log("WARNING: ignoring unexpected field in rpc check update result: " + key + ": " + val);
        }
        if (result.getAutoUri() === "") result.setAutoUri(undefined);
        if (result.getUserUri() === "") result.setUserUri(undefined);
        if (result.getVersion() === "") result.setVersion(undefined);
        if (result.getHash() === "") result.setHash(undefined);
        return result;
      }

      static _convertRpcUpdateDownloadResult(rpcResult) {
        let result = new MoneroDaemonUpdateDownloadResult(MoneroDaemonRpc._convertRpcUpdateCheckResult(rpcResult));
        result.setDownloadPath(rpcResult["path"]);
        if (result.getDownloadPath() === "") result.setDownloadPath(undefined);
        return result;
      }

      /**
       * Converts a '0x' prefixed hexidecimal string to a BigInteger.
       *
       * @param hex is the '0x' prefixed hexidecimal string to convert
       * @return BigInteger is the hexicedimal converted to decimal
       */
      static _prefixedHexToBI(hex) {
        assert(hex.substring(0, 2) === "0x");
        return BigInteger.parse(hex, 16);
      }
    }

    // static variables
    MoneroDaemonRpc.DEFAULT_ID = "0000000000000000000000000000000000000000000000000000000000000000";  // uninitialized tx or block hash from daemon rpc
    MoneroDaemonRpc.MAX_REQ_SIZE = "3000000";  // max request size when fetching blocks from daemon
    MoneroDaemonRpc.NUM_HEADERS_PER_REQ = "750";  // number of headers to fetch and cache per request

    /**
     * Implements a MoneroDaemon by proxying requests to a worker.
     *
     * @private
     */
    class MoneroDaemonRpcProxy extends MoneroDaemon {

      // --------------------------- STATIC UTILITIES -----------------------------

      static async connect(config) {
        let daemonId = GenUtils.getUUID();
        config = Object.assign({}, config, {proxyToWorker: false});
        await LibraryUtils.invokeWorker(daemonId, "connectDaemonRpc", [config]);
        return new MoneroDaemonRpcProxy(daemonId, await LibraryUtils.getWorker());
      }

      // ---------------------------- INSTANCE METHODS ----------------------------

      constructor(daemonId, worker) {
        super();
        this.daemonId = daemonId;
        this.worker = worker;
        this.wrappedListeners = [];
      }

      async getProcess() {
        return undefined; // proxy does not have access to process
      }

      async stopProcess(force) {
        if (this.process === undefined) throw new MoneroError("MoneroDaemonRpcProxy instance not created from new process");
        let listenersCopy = GenUtils.copyArray(this.getListeners());
        for (let listener of listenersCopy) await this.removeListener(listener);
        return GenUtils.killProcess(this.process, force ? "sigkill" : undefined);
      }

      async addListener(listener) {
        let wrappedListener = new DaemonWorkerListener(listener);
        let listenerId = wrappedListener.getId();
        LibraryUtils.WORKER_OBJECTS[this.daemonId].callbacks["onBlockHeader_" + listenerId] = [wrappedListener.onBlockHeader, wrappedListener];
        this.wrappedListeners.push(wrappedListener);
        return this._invokeWorker("daemonAddListener", [listenerId]);
      }

      async removeListener(listener) {
        for (let i = 0; i < this.wrappedListeners.length; i++) {
          if (this.wrappedListeners[i].getListener() === listener) {
            let listenerId = this.wrappedListeners[i].getId();
            await this._invokeWorker("daemonRemoveListener", [listenerId]);
            delete LibraryUtils.WORKER_OBJECTS[this.daemonId].callbacks["onBlockHeader_" + listenerId];
            this.wrappedListeners.splice(i, 1);
            return;
          }
        }
        throw new MoneroError("Listener is not registered with daemon");
      }

      getListeners() {
        let listeners = [];
        for (let wrappedListener of this.wrappedListeners) listeners.push(wrappedListener.getListener());
        return listeners;
      }

      async getRpcConnection() {
        let config = await this._invokeWorker("daemonGetRpcConnection");
        return new MoneroRpcConnection(config);
      }

      async isConnected() {
        return this._invokeWorker("daemonIsConnected");
      }

      async getVersion() {
        let versionJson = await this._invokeWorker("daemonGetVersion");
        return new MoneroVersion(versionJson.number, versionJson.isRelease);
      }

      async isTrusted() {
        return this._invokeWorker("daemonIsTrusted");
      }

      async getHeight() {
        return this._invokeWorker("daemonGetHeight");
      }

      async getBlockHash(height) {
        return this._invokeWorker("daemonGetBlockHash", Array.from(arguments));
      }

      async getBlockTemplate(walletAddress, reserveSize) {
        return new MoneroBlockTemplate(await this._invokeWorker("daemonGetBlockTemplate", Array.from(arguments)));
      }

      async getLastBlockHeader() {
        return new MoneroBlockHeader(await this._invokeWorker("daemonGetLastBlockHeader"));
      }

      async getBlockHeaderByHash(blockHash) {
        return new MoneroBlockHeader(await this._invokeWorker("daemonGetBlockHeaderByHash", Array.from(arguments)));
      }

      async getBlockHeaderByHeight(height) {
        return new MoneroBlockHeader(await this._invokeWorker("daemonGetBlockHeaderByHeight", Array.from(arguments)));
      }

      async getBlockHeadersByRange(startHeight, endHeight) {
        let blockHeadersJson = await this._invokeWorker("daemonGetBlockHeadersByRange", Array.from(arguments));
        let headers = [];
        for (let blockHeaderJson of blockHeadersJson) headers.push(new MoneroBlockHeader(blockHeaderJson));
        return headers;
      }

      async getBlockByHash(blockHash) {
        return new MoneroBlock(await this._invokeWorker("daemonGetBlockByHash", Array.from(arguments)));
      }

      async getBlocksByHash(blockHashes, startHeight, prune) {
        let blocksJson = await this._invokeWorker("daemonGetBlocksByHash", Array.from(arguments));
        let blocks = [];
        for (let blockJson of blocksJson) blocks.push(new MoneroBlock(blockJson));
        return blocks;
      }

      async getBlockByHeight(height) {
        return new MoneroBlock(await this._invokeWorker("daemonGetBlockByHeight", Array.from(arguments)));
      }

      async getBlocksByHeight(heights) {
        let blocksJson = await this._invokeWorker("daemonGetBlocksByHeight", Array.from(arguments));
        let blocks = [];
        for (let blockJson of blocksJson) blocks.push(new MoneroBlock(blockJson));
        return blocks;
      }

      async getBlocksByRange(startHeight, endHeight) {
        let blocksJson = await this._invokeWorker("daemonGetBlocksByRange", Array.from(arguments));
        let blocks = [];
        for (let blockJson of blocksJson) blocks.push(new MoneroBlock(blockJson));
        return blocks;
      }

      async getBlocksByRangeChunked(startHeight, endHeight, maxChunkSize) {
        let blocksJson = await this._invokeWorker("daemonGetBlocksByRangeChunked", Array.from(arguments));
        let blocks = [];
        for (let blockJson of blocksJson) blocks.push(new MoneroBlock(blockJson));
        return blocks;
      }

      async getBlockHashes(blockHashes, startHeight) {
        return this._invokeWorker("daemonGetBlockHashes", Array.from(arguments));
      }

      async getTxs(txHashes, prune = false) {

        // deserialize txs from blocks
        let blocks = [];
        for (let blockJson of await this._invokeWorker("daemonGetTxs", Array.from(arguments))) {
          blocks.push(new MoneroBlock(blockJson));
        }

        // collect txs
        let txs = [];
        for (let block of blocks) {
          for (let tx of block.getTxs()) {
            if (!tx.isConfirmed()) tx.setBlock(undefined);
            txs.push(tx);
          }
        }
        return txs;
      }

      async getTxHexes(txHashes, prune = false) {
        return this._invokeWorker("daemonGetTxHexes", Array.from(arguments));
      }

      async getMinerTxSum(height, numBlocks) {
        return new MoneroMinerTxSum(await this._invokeWorker("daemonGetMinerTxSum", Array.from(arguments)));
      }

      async getFeeEstimate(graceBlocks) {
        return new MoneroFeeEstimate(await this._invokeWorker("daemonGetFeeEstimate", Array.from(arguments)));
      }

      async submitTxHex(txHex, doNotRelay) {
        return new MoneroSubmitTxResult(await this._invokeWorker("daemonSubmitTxHex", Array.from(arguments)));
      }

      async relayTxsByHash(txHashes) {
        return this._invokeWorker("daemonRelayTxsByHash", Array.from(arguments));
      }

      async getTxPool() {
        let blockJson = await this._invokeWorker("daemonGetTxPool");
        let txs = new MoneroBlock(blockJson).getTxs();
        for (let tx of txs) tx.setBlock(undefined);
        return txs ? txs : [];
      }

      async getTxPoolHashes() {
        return this._invokeWorker("daemonGetTxPoolHashes", Array.from(arguments));
      }

      async getTxPoolBacklog() {
        throw new MoneroError("Not implemented");
      }

      async getTxPoolStats() {
        return new MoneroTxPoolStats(await this._invokeWorker("daemonGetTxPoolStats"));
      }

      async flushTxPool(hashes) {
        return this._invokeWorker("daemonFlushTxPool", Array.from(arguments));
      }

      async getKeyImageSpentStatuses(keyImages) {
        return this._invokeWorker("daemonGetKeyImageSpentStatuses", Array.from(arguments));
      }

      async getOutputs(outputs) {
        throw new MoneroError("Not implemented");
      }

      async getOutputHistogram(amounts, minCount, maxCount, isUnlocked, recentCutoff) {
        let entries = [];
        for (let entryJson of await this._invokeWorker("daemonGetOutputHistogram", [amounts, minCount, maxCount, isUnlocked, recentCutoff])) {
          entries.push(new MoneroOutputHistogramEntry(entryJson));
        }
        return entries;
      }

      async getOutputDistribution(amounts, cumulative, startHeight, endHeight) {
        throw new MoneroError("Not implemented");
      }

      async getInfo() {
        return new MoneroDaemonInfo(await this._invokeWorker("daemonGetInfo"));
      }

      async getSyncInfo() {
        return new MoneroDaemonSyncInfo(await this._invokeWorker("daemonGetSyncInfo"));
      }

      async getHardForkInfo() {
        return new MoneroHardForkInfo(await this._invokeWorker("daemonGetHardForkInfo"));
      }

      async getAltChains() {
        let altChains = [];
        for (let altChainJson of await this._invokeWorker("daemonGetAltChains")) altChains.push(new MoneroAltChain(altChainJson));
        return altChains;
      }

      async getAltBlockHashes() {
        return this._invokeWorker("daemonGetAltBlockHashes");
      }

      async getDownloadLimit() {
        return this._invokeWorker("daemonGetDownloadLimit");
      }

      async setDownloadLimit(limit) {
        return this._invokeWorker("daemonSetDownloadLimit", Array.from(arguments));
      }

      async resetDownloadLimit() {
        return this._invokeWorker("daemonResetDownloadLimit");
      }

      async getUploadLimit() {
        return this._invokeWorker("daemonGetUploadLimit");
      }

      async setUploadLimit(limit) {
        return this._invokeWorker("daemonSetUploadLimit", Array.from(arguments));
      }

      async resetUploadLimit() {
        return this._invokeWorker("daemonResetUploadLimit");
      }

      async getPeers() {
        let peers = [];
        for (let peerJson of await this._invokeWorker("daemonGetPeers")) peers.push(new MoneroPeer(peerJson));
        return peers;
      }

      async getKnownPeers() {
        let peers = [];
        for (let peerJson of await this._invokeWorker("daemonGetKnownPeers")) peers.push(new MoneroPeer(peerJson));
        return peers;
      }

      async setOutgoingPeerLimit(limit) {
        return this._invokeWorker("daemonSetIncomingPeerLimit", Array.from(arguments));
      }

      async setIncomingPeerLimit(limit) {
        return this._invokeWorker("daemonSetIncomingPeerLimit", Array.from(arguments));
      }

      async getPeerBans() {
        let bans = [];
        for (let banJson of await this._invokeWorker("daemonGetPeerBans")) bans.push(new MoneroBan(banJson));
        return bans;
      }

      async setPeerBans(bans) {
        let bansJson = [];
        for (let ban of bans) bansJson.push(ban.toJson());
        return this._invokeWorker("daemonSetPeerBans", [bansJson]);
      }

      async startMining(address, numThreads, isBackground, ignoreBattery) {
        return this._invokeWorker("daemonStartMining", Array.from(arguments));
      }

      async stopMining() {
        await this._invokeWorker("daemonStopMining")
      }

      async getMiningStatus() {
        return new MoneroMiningStatus(await this._invokeWorker("daemonGetMiningStatus"));
      }

      async submitBlocks(blockBlobs) {
        throw new MoneroError("Not implemented");
      }

      async pruneBlockchain(check) {
        return new MoneroPruneResult(await this._invokeWorker("daemonPruneBlockchain"));
      }

      async checkForUpdate() {
        throw new MoneroError("Not implemented");
      }

      async downloadUpdate(path) {
        throw new MoneroError("Not implemented");
      }

      async stop() {
        while (this.wrappedListeners.length) await this.removeBlockListener(this.wrappedListeners[0].getListener());
        return this._invokeWorker("daemonStop");
      }

      async waitForNextBlockHeader() {
        return new MoneroBlockHeader(await this._invokeWorker("daemonWaitForNextBlockHeader"));
      }

      // --------------------------- PRIVATE HELPERS ------------------------------

      // TODO: duplicated with MoneroWalletFullProxy
      async _invokeWorker(fnName, args) {
        return LibraryUtils.invokeWorker(this.daemonId, fnName, args);
      }
    }

    /**
     * Polls a Monero daemon for updates and notifies listeners as they occur.
     *
     * @class
     * @ignore
     */
    class DaemonPoller {

      constructor(daemon) {
        let that = this;
        this._daemon = daemon;
        this._looper = new TaskLooper(async function() { await that.poll(); });
      }

      setIsPolling(isPolling) {
        this._isPolling = isPolling;
        if (isPolling) this._looper.start(this._daemon.config.pollInterval);
        else this._looper.stop();
      }

      async poll() {
        try {

          // get latest block header
          let header = await this._daemon.getLastBlockHeader();

          // save first header for comparison
          if (!this._lastHeader) {
            this._lastHeader = await this._daemon.getLastBlockHeader();
            return;
          }

          // compare header to last
          if (header.getHash() !== this._lastHeader.getHash()) {
            this._lastHeader = header;
            for (let listener of this._daemon.getListeners()) {
              await listener.onBlockHeader(header); // notify listener
            }
          }
        } catch (err) {
          console.error("Failed to background poll daemon header");
          console.error(err);
        }
      }
    }

    /**
     * Internal listener to bridge notifications to external listeners.
     *
     * @private
     */
    class DaemonWorkerListener {

      constructor(listener) {
        this._id = GenUtils.getUUID();
        this._listener = listener;
      }

      getId() {
        return this._id;
      }

      getListener() {
        return this._listener;
      }

      async onBlockHeader(headerJson) {
        return this._listener.onBlockHeader(new MoneroBlockHeader(headerJson));
      }
    }

    module.exports = MoneroDaemonRpc;