cirquityd-rpc.js

// Copyright (c) 2018-2019, Brandon Lehmann, The TurtleCoin Developers
// Copyright (c) 2019-2020, Deeterd, The Cirquity Developers
//
// Please see the included LICENSE file for more information.

'use strict'

const packageInfo = require('../package.json')
const request = require('request-promise-native')
const util = require('util')

/**
 * @module Cirquityd
 * @class
 */
class Cirquityd {
  /**
   * Initializes a new Cirquityd object
   * @constructor
   * @param {Object} [opts] - Configuration options
   * @param {string} [opts.host=127.0.0.1] - the address of the daemon
   * @param {string} [opts.port=11898] - the RPC port number of the daemon
   * @param {number} [opts.timeout=2000] - the timeout to use during RPC calls
   * @param {boolean} [opts.ssl=false] - whether the daemon uses SSL (HTTPS) or not
   * @param {string} [opts.userAgent=cirquity-rpc/version] - the user agent string to use with requests
   * @param {boolean} [opts.keepAlive=true] - whether the underlying HTTP(s) connection should be kept alive and reused
   */
  constructor (opts) {
    opts = opts || {}
    this.host = opts.host || '127.0.0.1'
    this.port = opts.port || 18128
    this.timeout = opts.timeout || 2000
    this.ssl = opts.ssl || false
    this.userAgent = opts.userAgent || util.format('%s/%s', packageInfo.name, packageInfo.version)
    this.keepAlive = (typeof opts.keepAlive !== 'undefined') ? opts.keepAlive : true
  }

  /**
   * RPC GET Request
   * @async
   * @private
   * @param {string} method - the RPC method to call
   * @returns {Object} the response
   */
  _get (method) {
    if (method.length === 0) throw new Error('no method supplied')
    const protocol = (this.ssl) ? 'https' : 'http'

    return request({
      uri: util.format('%s://%s:%s/%s', protocol, this.host, this.port, method),
      method: 'GET',
      json: true,
      timeout: this.timeout,
      forever: this.keepAlive,
      headers: {
        'User-Agent': this.userAgent
      }
    })
  }

  /**
   * RPC POST Request
   * @async
   * @private
   * @param {string} method - the RPC method to call
   * @param {Object} params - the parameters for the RPC POST request
   * @returns {Object} the response
   */
  _post (method, params) {
    if (method.length === 0) throw new Error('no method supplied')
    params = params || {}

    var body = {
      jsonrpc: '2.0',
      method: method,
      params: params
    }

    return this._rawPost('json_rpc', body)
      .then(response => {
        if (response.error) throw new Error(response.error.message)

        return response.result
      })
  }

  /**
   * RPC raw POST Request
   * @async
   * @private
   * @param {string} endpoint - the RPC endpoint to call
   * @param {Object} body - the body of the POST request
   * @returns {Object} the response
   */
  _rawPost (endpoint, body) {
    if (endpoint.length === 0) throw new new Error('no endpoint supplied')()
    if (body === undefined) throw new new Error('no body supplied')()
    const protocol = (this.ssl) ? 'https' : 'http'

    return request({
      uri: util.format('%s://%s:%s/%s', protocol, this.host, this.port, endpoint),
      method: 'POST',
      body: body,
      json: true,
      timeout: this.timeout,
      forever: this.keepAlive,
      headers: {
        'User-Agent': this.userAgent
      }
    })
  }

  /**
   * Transaction Summary
   * @memberof Cirquityd
   * @typedef {Object} TransactionSummary
   * @property {number} amount_out - the amount of the transaction
   * @property {number} fee - the fee of the transaction
   * @property {string} hash - the hash of the transaction
   * @property {number} size - the size of the transaction
   */

  /**
   * Block Summary
   * @memberof Cirquityd
   * @typedef {Object} BlockSummary
   * @property {number} alreadyGeneratedCoins - the number of already generated coins
   * @property {number} alreadyGeneratedTransactions - the number of already generated transactions
   * @property {number} baseReward - the block base reward
   * @property {number} blockSize - the block size
   * @property {number} depth - the depth of the block in the chain (aka. confirmations)
   * @property {number} difficulty - the block difficulty
   * @property {number} effectiveSizeMedian - the effective median size of the blocks
   * @property {string} hash - the block hash
   * @property {number} height - the block height
   * @property {number} major_version - the block major version
   * @property {number} minor_version - the block minor version
   * @property {number} nonce - the block nonce
   * @property {boolean} orphan_status - whether the block is an orphan
   * @property {number} penalty - the block penalty
   * @property {string} prev_hash - the previous block hash
   * @property {number} reward - the block reward
   * @property {number} sizeMedian - the median block size
   * @property {number} timestamp - the block timestamp
   * @property {number} totalFeeAmount - the total amount of fees in the block
   * @property {Cirquityd.TransactionSummary[]} transactions
   * @property {number} transactionsCumulativeSize - the total size of the transactions in the block
   */

  /**
   * Returns information on a single block by hash
   * @async
   * @param {string} hash - the hash of the block to retrieve
   * @returns {Promise<Cirquityd.BlockSummary>} resolves with block summary or rejects with error
   */
  block (hash) {
    if (!hash) throw new Error('must specify hash')

    return this._post('f_block_json', { hash })
      .then(response => { return response.block })
  }

  /**
   * Gets the current block count
   * @async
   * @returns {Promise<number>} resolves with the current block count or rejects with error
   */
  blockCount () {
    return this._post('getblockcount')
      .then(response => { return response.count })
  }

  /**
   * Block Header
   * @memberof Cirquityd
   * @typedef {Object} BlockHeader
   * @property {number} block_size - the block size
   * @property {number} depth - the depth of the block in the chain (aka. confirmations)
   * @property {number} difficulty - the block difficulty
   * @property {string} hash - the block hash
   * @property {number} height - the block height
   * @property {number} major_version - the block major version
   * @property {number} minor_version - the block minor version
   * @property {number} nonce - the block nonce
   * @property {number} num_txes - the number of transactions in the block
   * @property {boolean} orphan_status - whether the block is an orphan
   * @property {string} prev_hash - the previous block hash
   * @property {number} reward - the block reward
   * @property {number} timestamp - the block timestamp total size of the transactions in the block
   */

  /**
   * Gets the block header
   * @async
   * @param {string} hash - the hash of the block to retrieve
   * @returns {Promise<Cirquityd.BlockHeader>} resolves with block header or rejects with error
   */
  blockHeaderByHash (hash) {
    if (!hash) throw new Error('must specify hash')

    return this._post('getblockheaderbyhash', { hash })
      .then(response => { return response.block_head })
  }

  /**
   * Gets the block header
   * @async
   * @param {number} height - the height of the block to retrieve
   * @returns {Promise<Cirquityd.BlockHeader>} resolves with block header or rejects with error
   */
  blockHeaderByHeight (height) {
    if (typeof height === 'undefined') throw new Error('must specify height')

    return this._post('getblockheaderbyheight', { height })
      .then(response => { return response.block_header })
  }

  /**
   * Block Short Summary
   * @memberof Cirquityd
   * @typedef {Object} BlockShortHeader
   * @property {number} cumul_size - the total size of the block
   * @property {number} difficulty - the difficulty of the block
   * @property {string} hash - the block hash
   * @property {number} height - the height of the block
   * @property {number} timestamp - the timestamp of the block
   * @property {number} tx_count - the number of transactions in the block
   */

  /**
   * Gets the summary block information for the last 30 blocks before height (inclusive)
   * @async
   * @param {number} height - the height of the block to retrieve
   * @returns {Promise<Cirquityd.BlockShortHeader[]>} resolves with block short headers or rejects with error
   */
  blockShortHeaders (height) {
    if (typeof height === 'undefined') throw new Error('must specify height')

    return this._post('f_blocks_list_json', { height })
      .then(response => { return response.blocks })
  }

  /**
   * Transaction Extra Detail
   * @memberof Cirquityd
   * @typedef {Object} TransactionExtraDetail
   * @property {number[]} [nonce] - the nonce
   * @property {string} publicKey - the public key
   * @property {string} raw - the raw transaction extra
   */

  /**
   * @memberof Cirquityd
   * @typedef {Object} TransactionDetailInputCoinbaseInput
   * @property {number} height - the height of the block
   */

  /**
   * @memberof Cirquityd
   * @typedef {Object} TransactionDetailInputCoinbase
   * @property {number} amount - the amount of the input
   * @property {Cirquityd.TransactionDetailInputCoinbaseInput} input - the input
   */

  /**
   * @memberof Cirquityd
   * @typedef {Object} TransactionDetailInputKeyOutput
   * @property {number} number - the output index of the output used
   * @property {string} transactionHash - the transaction hash of the output

  /**
   * @memberof Cirquityd
   * @typedef {Object} TransactionDetailInputKeyInput
   * @property {number} amount - the amount of the input
   * @property {string} k_image - the key image of the input
   * @property {number[]} key_offsets - the key offsets of the input
   */

  /**
   * @memberof Cirquityd
   * @typedef {Object} TransactionDetailInputKey
   * @property {Cirquityd.TransactionDetailInputKeyInput} input - the input
   * @property {number} mixin - the ring size
   * @property {Cirquityd.TransactionDetailInputKeyOutput} output - the related output information
   */

  /**
   * Transaction Detail Input
   * @memberof Cirquityd
   * @typedef {Object} TransactionDetailInput
   * @property {Cirquityd.TransactionDetailInputCoinbase|Cirquityd.TransactionDetailInputKey} data - the input data
   * @property {string} type - the input type
   */

  /**
   * Transaction Output Target Data
   * @memberof Cirquityd
   * @typedef {Object} TransactionOutputTargetData
   * @property {string} key - the output key
   */

  /**
   * Transaction Output Target
   * @memberof Cirquityd
   * @typedef {Object} TransactionOutputTarget
   * @property {Cirquityd.TransactionOutputTargetData} data - output data structure
   * @property {string} type - the output type
   */

  /**
   * Transaction Output
   * @memberof Cirquityd
   * @typedef {Object} TransactionOutput
   * @property {number} amount - the amount of the output
   * @property {Cirquityd.TransactionOutputTarget} target - the output target
   */

  /**
   * Transaction Detail Output
   * @memberof Cirquityd
   * @typedef {Object} TransactionDetailOutput
   * @property {number} globalIndex - the global index of the output
   * @property {Cirquityd.TransactionOutput} output - The transaction output
   */

  /**
   * Transaction Details
   * @memberof Cirquityd
   * @typedef {Object} TransactionDetail
   * @property {string} blockHash - the block hash
   * @property {number} blockIndex - the block index (aka. height)
   * @property {Cirquityd.TransactionExtraDetail} extra - the transaction extra
   * @property {number} fee - the transaction fee
   * @property {string} hash - the transaction hash
   * @property {boolean} inBlockchain - whether the transaction is in the blockchain
   * @property {Cirquityd.TransactionDetailInput[]} inputs - the inputs of the transaction
   * @property {number} mixin - the number of transaction mixins
   * @property {Cirquityd.TransactionDetailOutput[]} outputs - the outputs of the transaction
   * @property {string} paymentId - the payment ID of the transaction
   * @property {string[]} signatures - the signatures of the transaction
   * @property {number} signaturesSize - the size of the signatures
   * @property {number} size - the transaction size
   * @property {number} timestamp - the transaction timestamp
   * @property {number} totalInputsAmount - the total amount of the transaction's inputs
   * @property {number} totalOutputsAmount - the total amount of the transaction's outputs
   * @property {number} unlockTime - the unlock time/block of the transaction
   */

  /**
   * Block Details
   * @memberof Cirquityd
   * @typedef {Object} BlockDetails
   * @property {number} alreadyGeneratedCoins - the number of already generated coins
   * @property {number} alreadyGeneratedTransactions - the number of already generated transactions
   * @property {number} baseReward - the block base reward
   * @property {number} blockSize - the block size
   * @property {number} depth - the depth of the block in the chain (aka. confirmations)
   * @property {number} difficulty - the block difficulty
   * @property {string} hash - the block hash
   * @property {number} index - the block index (aka. height)
   * @property {number} majorVersion - the block major version
   * @property {number} minorVersion - the block minor version
   * @property {number} nonce - the block nonce
   * @property {string} prevBlockHash - the previous block hash
   * @property {number} reward - the block reward
   * @property {number} sizeMedian - the median block size
   * @property {number} timestamp - the block timestamp
   * @property {number} totalFeeAmount - the total amount of fees in the block
   * @property {Cirquityd.TransactionDetail[]} transactions
   * @property {number} transactionsCumulativeSize - the total size of the transactions in the block
   */

  /**
   * Query Blocks Detailed Response
   * @memberof Cirquityd
   * @typedef {Object} BlocksDetailedResponse
   * @property {Cirquityd.BlockDetails} blocks - the blocks
   * @property {number} currentHeight - the current height of the blockchain
   * @property {number} fullOffset
   * @property {number} startHeight - the height at which this response starts
   * @property {string} status - the response status
   */

  /**
   * Returns up to 100 blocks. If blockHashes are given, it will return beginning from the height of the first hash it finds, plus one.
   * However, if timestamp is given, and this value is higher than the blockHashes, it will start returning from that height instead.
   * The blockHashes should be given with the highest block height hashes first.
   * First 10 blocks hashes go sequential, next in pow(2,n) offset, like 2, 4, 8, 16, 32, 64 and so on, and the last one is always genesis block
   * Typical usage: specify a start timestamp initially, and from then on, also provide the returned block hashes.
   * @async
   * @param {Object} [opts] - the options to use when syncing
   * @param {number} [opts.timestamp=0] - the timestamp to start from
   * @param {string[]} [opts.blockHashes] - the block hashes
   * @param {number} [opts.blockCount=0] - the number of blocks to return
   * @returns {Promise<Cirquityd.BlocksDetailedResponse>} resolves with blocks detail information or rejects with error
   */
  blocksDetailed (opts) {
    opts = opts || {}
    if (!Array.isArray(opts.blockHashes)) throw new Error('must supply an array of block hashes')
    if (opts.timestamp === undefined) opts.timestamp = 0
    if (opts.blockCount === undefined) opts.blockCount = 100

    var body = {
      blockIds: opts.blockHashes,
      timestamp: opts.timestamp,
      blockCount: opts.blockCount
    }

    return this._rawPost('queryblocksdetailed', body)
  }

  /**
   * @memberof Cirquityd
   * @typedef BlockLite
   * @property {string} block - the hexadecimcal representation of the block
   * @property {string} hash - the block hash
   * @property {Cirquityd.TransactionPrefix[]} transactions - the transactions in the block
   */

  /**
   * @memberof Cirquityd
   * @typedef BlocksLiteResponse
   * @property {number} currentHeight - the current height
   * @property {number} fullOffset - the full offset height
   * @property {Cirquityd.BlockLite[]} items - the block data
   * @property {number} startHeight - the height the response starts from
   * @property {string} status - the status of the request
   */

  /**
   * Returns up to 100 blocks. If blockHashes are given, it will return beginning from the height of the first hash it finds, plus one.
   * However, if timestamp is given, and this value is higher than the blockHashes, it will start returning from that height instead.
   * The blockHashes should be given with the highest block height hashes first.
   * First 10 blocks hashes go sequential, next in pow(2,n) offset, like 2, 4, 8, 16, 32, 64 and so on, and the last one is always genesis block
   * Typical usage: specify a start timestamp initially, and from then on, also provide the returned block hashes.
   * @async
   * @param {Object} [opts] - the options to use when syncing
   * @param {number} [opts.timestamp=0] - the timestamp to start from
   * @param {string[]} [opts.blockHashes] - the block hashes
   * @returns {Promise<Cirquityd.BlocksLiteResponse>} resolves with block information or rejects with error
   */
  blocksLite (opts) {
    opts = opts || {}
    if (!Array.isArray(opts.blockHashes)) throw new Error('must supply an array of block hashes')
    if (opts.timestamp === undefined) opts.timestamp = 0

    var body = {
      blockIds: opts.blockHashes,
      timestamp: opts.timestamp
    }

    return this._rawPost('queryblockslite', body)
      .then(response => {
        /* The response returned by the daemon is a nightmare but we're going to clean it up a bit */
        const tmp = []
        response.items
          .forEach(item => {
            const transactions = []

            item['blockShortInfo.txPrefixes']
              .forEach(txn => {
                transactions.push({
                  hash: txn['transactionPrefixInfo.txHash'],
                  prefix: txn['transactionPrefixInfo.txPrefix']
                })
              })

            tmp.push({
              block: Buffer.from(item['blockShortInfo.block']).toString('hex'),
              hash: item['blockShortInfo.blockId'],
              transactions: transactions
            })
          })
        response.items = tmp

        return response
      })
  }

  /**
   * Block Template Response
   * @memberof Cirquityd
   * @typedef {Object} BlockTemplateResponse
   * @property {string} blocktemplate_blob - the raw block template
   * @property {number} difficulty - the target difficulty
   * @property {number} height - the block height
   * @property {number} reserved_offset - the reserved offset location in the raw block template
   * @property {string} status - the response status
   */

  /**
   * Gets the block template using the supplied parameters
   * @async
   * @param {string} walletAddress - the wallet address for the block template
   * @param {number} reserveSize - the amount of block template reserve space to generate
   * @returns {Promise<Cirquityd.BlockTemplateResponse>} resolves with block template response or rejects with error
   */
  blockTemplate (walletAddress, reserveSize) {
    if (typeof reserveSize === 'undefined') throw new Error('must specify reserveSize')
    if (!walletAddress) throw new Error('must specify walletAddress')

    return this._post('getblocktemplate', {
      reserve_size: reserveSize,
      wallet_address: walletAddress
    })
  }

  /**
   * Node Fee Response
   * @memberof Cirquityd
   * @typedef {Object} NodeFee
   * @property {string} address - the node fee address
   * @property {number} amount - the node fee amount
   * @property {string} status - the response status
   */

  /**
   * Retrieves the node fee in atomic units
   * @async
   * @returns {Promise<Cirquityd.NodeFee>} resolves with node fee information or rejects with error
   */
  fee () {
    return this._get('fee')
  }

  /**
   * Returns the global output indexes of the transaction
   * @async
   * @param {string} transactionHash - the hash of the transaction to retrieve
   * @returns {Promise<number[]>} resolves with indexes global output indexes or rejects with error
   */
  globalIndexes (transactionHash) {
    if (typeof transactionHash === 'undefined') throw new Error('must supply a transaction hash')

    var body = {
      txid: transactionHash
    }

    return this._rawPost('get_o_indexes', body)
      .then(response => {
        if (response.status.toLowerCase() !== 'ok') throw new Error('Transaction not found')

        return response.o_indexes
      })
  }

  /**
   * Global Indexes Range Response
   * @memberof Cirquityd
   * @typedef {Object} GlobalIndexesResponse
   * @property {string} key - the transaction hash
   * @property {number[]} value - the global output indexes
   */

  /**
   * Returns the global indexes for any transactions in the range [startHeight .. endHeight]. Generally, you only want the global index for a specific transaction, however, this reveals that you probably are the recipient of this transaction. By supplying a range of blocks, you can obfusticate which transaction you are enquiring about.
   * @async
   * @param {number} startHeight - The height to begin returning indices from
   * @param {number} endHeight - The height to end returning indices from
   * @returns {Promise<Cirquityd.GlobalIndexesResponse[]>} resolves with global indexes information or rejects with error
   */
  globalIndexesForRange (startHeight, endHeight) {
    if (typeof startHeight === 'undefined') throw new Error('Must specify start height')
    if (typeof endHeight === 'undefined') throw new Error('Must specify end height')

    return this._rawPost('get_global_indexes_for_range', { startHeight, endHeight })
      .then(response => {
        if (!response.status || !response.indexes) throw new Error('Missing indexes or status key')
        if (response.status.toLowerCase() !== 'ok') throw new Error('Status is not OK')

        return response.indexes
      })
  }

  /**
   * Node Height Response
   * @memberof Cirquityd
   * @typedef {Object} NodeHeight
   * @property {number} height - the current height of the node
   * @property {number} network_height - the observed network height
   * @property {string} status - the response status
   */

  /**
   * Returns the current daemon height statistics
   * @async
   * @returns {Promise<Cirquityd.NodeHeight>} resolves with node height information or rejects with error
   */
  height () {
    return this._get('height')
  }

  /**
   * Node Info Response
   * @memberof Cirquityd
   * @typedef {Object} NodeInfo
   * @property {number} alt_blocks_count - the number of alternate blocks the node knows about
   * @property {number} difficulty - the current network difficulty
   * @property {number} grey_peerlist_size - the number of currently gray listed peers
   * @property {number} hashrate - the network hash rate
   * @property {number} height - the current height of the node
   * @property {number} incoming_connections_count - the number of incoming connections to the node
   * @property {number} last_known_block_index - the last known block index
   * @property {number} major_version - the current block major version
   * @property {number} minor_version - the current block minor version
   * @property {number} network_height - the observed network height
   * @property {number} outgoing_connections_count - the number of outgoing connections from the node
   * @property {number} start_time - the timestamp of when the node was started
   * @property {string} status - the response status
   * @property {number} supported_height - the height as which the code the node is running is supported until
   * @property {boolean} synced - whether the node is fully synced with the network or not
   * @property {number} tx_count - the number of transactions the node knows of
   * @property {number} tx_pool_size - the number of transactions in the node's mempool
   * @property {number[]} upgrade_heights - the list of upgrade heights the node is aware of
   * @property {string} version - the version number of the node software
   * @property {number} white_peerlist_size - the number of currently whitelisted peers
   */

  /**
   * Returns the current daemon information
   * @async
   * @returns {Promise<Cirquityd.NodeInfo>} resolves with node information or rejects with error
   */
  info () {
    return this._get('info')
  }

  /**
   * Retrieves the last block header
   * @async
   * @returns {Promise<Cirquityd.BlockHeader>} resolves with block header or rejects with error
   */
  lastBlockHeader () {
    return this._post('getlastblockheader')
      .then(response => { return response.block_header })
  }

  /**
   * Node Peers Response
   * @memberof Cirquityd
   * @typedef {Object} NodePeers
   * @property {string[]} gray_peers - graylisted peers
   * @property {string[]} peers - peers
   * @property {string} status - the response status
   */

  /**
   * Returns the current daemon peers
   * @async
   * @returns {Promise<Cirquityd.NodePeers>} resolves with node peer information or rejects with error
   */
  peers () {
    return this._get('peers')
  }

  /**
   * @memberof Cirquityd
   * @typedef PoolChangesAdded
   * @property {string} hash - the transaction hash
   * @property {Cirquityd.TransactionPrefix} prefix - the transaction prefix
   */

  /**
   * @memberof Cirquityd
   * @typedef PoolChanges
   * @property {Cirquityd.PoolChangesAdded} addedTxs - the recently added pool transactions
   * @property {string[]} deletedTxsIds - the transaction hashes of transactions removed from the pool
   * @property {boolean} isTailBlockActual - whether the tail block hash supplied is really the top
   * @property {string} status - the status of the request
   */

  /**
   * Returns updates regarding the transaction mempool
   * @async
   * @param {string} tailBlockHash - the last known block hash
   * @param {string[]} knownTransactionHashes - the transaction hashes that we know of
   * @returns {Promise<Cirquityd.PoolChanges>} resolves with pool change information or rejects with error
   */
  poolChanges (tailBlockHash, knownTransactionHashes) {
    if (tailBlockHash === undefined) throw new Error('must supply a tail block hash')
    if (!Array.isArray(knownTransactionHashes)) throw new Error('must supply an array of known transaction hashes')

    var body = {
      tailBlockId: tailBlockHash,
      knownTxsIds: knownTransactionHashes
    }

    return this._rawPost('get_pool_changes_lite', body)
      .then(response => {
        /* We need to clean up the response a bit */
        const tmp = []

        response.addedTxs
          .forEach(tx => {
            tmp.push({
              hash: tx['transactionPrefixInfo.txHash'],
              prefix: tx['transactionPrefixInfo.txPrefix']
            })
          })

        response.addedTxs = tmp

        return response
      })
  }

  /**
   * A Random Output
   * @memberof Cirquityd
   * @typedef {Object} RandomOutput
   * @property {number} global_amount_index - the output global index
   * @property {string} out_key - the output key
   */

  /**
   * A Random Outs object
   * @memberof Cirquityd
   * @typedef {Object} RandomOuts
   * @property {number} amount - the amount of the output
   * @property {Cirquityd.RandomOutput[]} outs - a list of random outputs
   */

  /**
   * Random Outputs Response
   * @memberof Cirquityd
   * @typedef {Object} RandomOutputsResponse
   * @property {Cirquityd.RandomOuts[]} outs - a list of random outs
   * @property {string} status - the response status
   */

  /**
   * Retrieves random outputs for mixing
   * @async
   * @param {number[]} amounts - the amounts that we need mixins for
   * @param {number} mixin - the number of mixins we need
   * @returns {Promise<Cirquityd.RandomOutputsResponse>} resolves with random outputs information or rejects with error
   */
  randomOutputs (amounts, mixin) {
    if (!Array.isArray(amounts)) throw new Error('must supply an array of amounts')
    if (typeof mixin === 'undefined') throw new Error('must supply a mixin value')

    mixin = parseInt(mixin)
    if (isNaN(mixin)) throw new Error('must supply a valid mixin value')

    var body = {
      amounts: amounts,
      outs_count: mixin
    }

    return this._rawPost('getrandom_outs', body)
  }

  /**
   * A Raw Transaction
   * @memberof Cirquityd
   * @typedef {Object} RawTransaction
   * @property {string} transaction - the raw transaction hex
   * @property {number} tx_size - the size of the transaction
   */

  /**
   * A Raw Block
   * @memberof Cirquityd
   * @typedef {Object} RawBlock
   * @property {string} block - the raw block hex
   * @property {number} block_size - the size of the block
   * @property {Cirquityd.RawTransaction[]} transactions - a list of raw transactions
   * @property {number} tx_count - the number of transactions in the block
   */

  /**
   * Get Blocks Fast Response
   * @memberof Cirquityd
   * @typedef {Object} RawBlocksResponse
   * @property {Cirquityd.RawBlock[]} blocks - the raw blocks
   * @property {number} current_height - the current height of the blockchain
   * @property {number} start_height - the starting height of the blocks in the response
   * @property {string} status - the response status
   */

  /**
   * Get raw blocks
   * @async
   * @param {string[]} blockHashes - first 10 blocks id goes sequential, next goes in pow(2,n) offset, like 2, 4, 8, 16, 32, 64 and so on, and the last one is always genesis block
   * @param {number} [blockCount] - the number of blocks to retrieve
   * @returns {Promise<Cirquityd.RawBlocksResponse>} resolves with raw blocks information or rejects with error
   */
  rawBlocks (blockHashes, blockCount) {
    if (!Array.isArray(blockHashes)) throw new Error('must supply an array of block hashes')
    if (blockCount && isNaN(blockCount)) throw new Error('block count must be a number')

    const body = {
      block_ids: blockHashes
    }

    if (blockCount) {
      body.blockCount = Math.abs(blockCount)
    }

    return this._rawPost('getblocks', body)
      .then(response => {
        /* We need to do a little bit of massaging here on this
           response because the daemon returns some funny
           business that we don't care for in JS */
        return {
          blocks: response['response.blocks'],
          current_height: response['response.current_height'],
          start_height: response['response.start_height'],
          status: response['response.status']
        }
      })
  }

  /**
   * Send Raw Transaction Response
   * @memberof Cirquityd
   * @typedef {Object} SendRawTransactionResponse
   * @property {string} status - the response status
   * @property {string} [error] - the error message if failed
   */

  /**
   * Sends a raw transaction to the daemon
   * @async
   * @param {string} transaction - the raw transaction
   * @returns {Promise<Cirquityd.SendRawTransactionResponse>} resolves with send raw transaction information or rejects with error
   */
  sendRawTransaction (transaction) {
    if (!transaction) throw new Error('must specify raw serialized transaction')

    return this._rawPost('sendrawtransaction', { tx_as_hex: transaction })
  }

  /**
   * Submit Block Response
   * @memberof Cirquityd
   * @typedef {Object} SubmitBlockResponse
   * @property {string} status - the response status
   */

  /**
   * Sends a new block for the chain to the daemon
   * @async
   * @param {string} blockBlob - the raw block blob
   * @returns {Promise<Cirquityd.SubmitBlockResponse>} resolves with submit block response or rejects with error
   */
  submitBlock (blockBlob) {
    if (!blockBlob) throw new Error('must specify blockBlob')

    return this._post('submitblock', [blockBlob])
  }

  /**
   * @memberof Cirquityd
   * @typedef VOUTTargetData
   * @property {string} key - the output target key
   */

  /**
   * @memberof Cirquityd
   * @typedef VOUTTarget
   * @property {Cirquityd.VOUTTargetData} data - output target data
   * @property {string} type - the output target type in hexadecimcal
   */

  /**
   * @memberof Cirquityd
   * @typedef VOUT
   * @property {number} amount - the amount of the output
   * @property {Cirquityd.VOUTTarget} target - the output target
   */

  /**
   * @memberof Cirquityd
   * @typedef VIN
   * @property {string} type - the type of the input in hexadecimcal
   * @property {Object} value - the input data
   * @property {number} value.amount - the input amount
   * @property {number} value.k_image - the input key image
   * @property {number[]} value.key_offsets - the input key offsets
   */

  /**
   * @memberof Cirquityd
   * @typedef VINCoinbase
   * @property {string} type - the input type in hexadecimcal
   * @property {Object} value - the input value
   * @property {number} value.height - the input height
   */

  /**
   * @memberof Cirquityd
   * @typedef TransactionPrefix
   * @property {string} extra - the transaction extra information as hexadecimcal
   * @property {number} unlock_time - the transaction unlock time
   * @property {number} version - the transaction version number
   * @property {Cirquityd.VINCoinbase|Cirquityd.VIN[]} vin - the transaction inputs
   * @property {Cirquityd.VOUT[]} vout - the transaction outputs
   */

  /**
   * @memberof Cirquityd
   * @typedef TransactionMetadata
   * @property {number} amount_out - the sum of the transaction outputs
   * @property {number} fee - the network fee of the transaction
   * @property {string} hash - the transaction hash
   * @property {number} mixin - the transaction ring size
   * @property {string} paymentId - the payment ID of the transaction if any
   * @property {number} size - the size of the transaction in bytes
   */

  /**
   * @memberof Cirquityd
   * @typedef TransactionResponse
   * @property {BlockShortHeader} block - the header of the block containing the transaction
   * @property {string} status - the status of the request
   * @property {Cirquityd.TransactionPrefix} tx - the transaction structured information
   * @property {Cirquityd.TransactionMetadata} txDetails - the transaction meta information
   */

  /**
   * Retrieves a single transaction's information
   * @async
   * @param {string} hash - the transaction hash
   * @returns {Promise<Cirquityd.TransactionResponse>} resolves with transaction response or rejects with error
   */
  transaction (hash) {
    if (!hash) throw new Error('must specify hash')

    return this._post('f_transaction_json', { hash })
      .then(response => {
        if (response.tx && response.tx['']) delete response.tx['']

        return response
      })
  }

  /**
   * Retrieves the summary information of the transactions in the mempool
   * @async
   * @returns {Promise<Cirquityd.TransactionSummary[]>} resolves with the transaction summaries or rejects with error
   */
  transactionPool () {
    return this._post('f_on_transactions_pool_json')
      .then(response => { return response.transactions })
  }

  /**
   * Transactions Status Response
   * @memberof Cirquityd
   * @typedef {Object} TransactionsStatusResponse
   * @property {string[]} transactionsInBlock - transaction hashes that are in blocks
   * @property {string[]} transactionsInPool - transaction hashes that are in the mempool
   * @property {string[]} transactionsUnknown - unknown transaction hashes
   */

  /**
   * Returns the status of the transaction hashes provided
   * @async
   * @param {string[]} transactionHashes - the transaction hashes to checked
   * @returns {Promise<Cirquityd.TransactionsStatusResponse>} resolves with the transactions statuses or rejects with error
   */
  transactionsStatus (transactionHashes) {
    if (!Array.isArray(transactionHashes)) throw new Error('Must specify transaction hashes')

    return this._rawPost('get_transactions_status', { transactionHashes })
      .then(response => {
        if (!response.status || !response.transactionsInPool || !response.transactionsInBlock || !response.transactionsUnknown) throw new Error('Missing status of transactions key')
        if (response.status.toLowerCase() !== 'ok') throw new Error('Status is not OK')

        return {
          transactionsInPool: response.transactionsInPool,
          transactionsInBlock: response.transactionsInBlock,
          transactionsUnknown: response.transactionsUnknown
        }
      })
  }

  /**
   * @memberof Cirquityd
   * @typedef WalletSyncTransactionOutput
   * @property {number} amount - the output amount
   * @property {string} key - the output key
   */

  /**
   * @memberof Cirquityd
   * @typedef WalletSyncTransaction
   * @property {string} hash - the transaction hash
   * @property {Cirquityd.TransactionDetailInputKeyInput} [inputs] - the transaction inputs
   * @property {Cirquityd.WalletSyncTransactionOutput[]} outputs - the transaction outputs
   * @property {string} txPublicKey - the one-time public key of the transaction
   * @property {number} unlockTime - the unlock time (or block) of the transaction
   */

  /**
   * @memberof Cirquityd
   * @typedef WalletSyncDataBlock
   * @property {string} blockHash - the block hash
   * @property {number} blockHeight - the block height
   * @property {number} blockTimestamp - the block timestamp
   * @property {Cirquityd.WalletSyncTransaction} [coinbaseTX] - the block coinbase transaction
   * @property {Cirquityd.WalletSyncTransaction[]} transactions - the transactions in the block
   */

  /**
   * @memberof Cirquityd
   * @typedef WalletSyncDataTopBlock
   * @property {string} hash - the top block hash
   * @property {number} height - the top block height
   */

  /**
   * @memberof Cirquityd
   * @typedef WalletSyncDataResponse
   * @property {Cirquityd.WalletSyncDataBlock[]} items - block data array
   * @property {string} status - the reponse status message
   * @property {boolean} synced - whether the request is fully synched
   * @property {Cirquityd.WalletSyncDataTopBlock} [topBlock] - the top block information
   */

  /**
   * Returns up to 100 blocks. If block hash checkpoints are given, it will return beginning from the height of the first hash it finds, plus one.
   * However, if startHeight or startTimestamp is given, and this value is higher than the block hash checkpoints, it will start returning from that height instead.
   * The block hash checkpoints should be given with the highest block height hashes first.
   * Typical usage: specify a start height/timestamp initially, and from then on, also provide the returned block hashes.
   * @async
   * @param {Object} [opts] - the options to use when syncing
   * @param {number} [opts.startHeight=0] - the height to start from
   * @param {number} [opts.startTimestamp=0] - the timestamp to start from
   * @param {string[]} [opts.blockHashCheckpoints] - the block hash checkpoints
   * @param {boolean} [opts.skipCoinbaseTransactions=false] - whether to skip returning blocks with just coinbase transactions
   * @returns {Promise<Cirquityd.WalletSyncDataResponse>} resolves with sync data response or rejects with error
   */
  walletSyncData (opts) {
    opts = opts || {}

    if (typeof opts.startHeight === 'undefined') {
      opts.startHeight = 0
    }

    if (typeof opts.startTimestamp === 'undefined') {
      opts.startTimestamp = 0
    }

    if (!opts.blockHashCheckpoints) {
      opts.blockHashCheckpoints = []
    }

    if (typeof opts.skipCoinbaseTransactions === 'undefined') {
      opts.skipCoinbaseTransactions = false
    }

    return this._rawPost('getwalletsyncdata', {
      startHeight: opts.startHeight,
      startTimestamp: opts.startTimestamp,
      blockHashCheckpoints: opts.blockHashCheckpoints,
      skipCoinbaseTransactions: opts.skipCoinbaseTransactions
    })
      .then(response => {
        if (!response.status || !response.items) throw new Error('Missing items or status key')
        if (response.status.toLowerCase() !== 'ok') throw new Error('Status is not OK')

        return response
      })
  }
}

module.exports = Cirquityd