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;