Source: client/etherscan.js

/**
 * @file Etherscan API client.
 * @module client/etherscan
 */

'use strict'

// Imports
const { URL } = require('url')
const config = require('config')
const request = require('../net/request')

/**
 * Private members store.
 * @private
 */
const privs = new WeakMap()

/**
 * Etherscan API URLs.
 * @private
 */
const url = {}

/** Base URL. */
url.base = new URL('https://api.etherscan.io/api')

/** Account module URLs. */
url.account = {}

/** Account base URL. */
url.account.base = new URL(url.base)
url.account.base
  .searchParams.set('module', 'account')

/** Account -> List Transactions URL. */
url.account.listTransactions = new URL(url.account.base)
url.account.listTransactions
  .searchParams.set('action', 'txlist')

/**
 * Etherscan API client.
 * @static
 */
class Client {
  /**
   * No parameters.
   */
  constructor () {
    const priv = {}
    privs.set(this, priv)
    priv.apiKey = config.get('Etherscan.apiKey')
  }

  /**
   * An Ethereum transaction.
   * @todo Determine timeStamp meaning.
   * @todo Determine timeStamp encoding.
   * @todo Can only contract transactions fail with an error?
   * @todo Determine cumulativeGasUsed meaning.
   * @typedef {Object} transaction
   * @prop {string} blockNumber
   *     Containing block number. Integer string.
   * @prop {string} timeStamp
   *     Integer string.
   * @prop {string} hash
   *     Transaction hash.
   * @prop {string} nonce
   *     Transaction nonce. Integer string.
   * @prop {string} blockHash
   *     Containing block hash.
   * @prop {string} transactionIndex
   *     Index of transaction in containing block. Integer string.
   * @prop {string} from
   *     Hex representation of source address.
   * @prop {string} to
   *     Hex representation of target address.
   *     Empty string for contract creation transactions.
   * @prop {string} value
   *     Amount transferred in wei. Integer string.
   * @prop {string} gas
   *     Gas limit in gas. Integer string.
   * @prop {string} gasPrice
   *     Gas price in wei. Integer string.
   * @prop {string} isError
   *     Whether transaction failed with an error.
   *     Integer string "1" for error.
   *     Integer string "0" for no error.
   * @prop {string} input
   *     Transaction data.
   *     Hexadecimal string prepended with "0x".
   *     For empty data value is "0x".
   * @prop {string} contractAddress
   *     Hex representation of created contract address.
   *     Empty string for no contract created.
   * @prop {string} cumulativeGasUsed
   *     Integer string.
   * @prop {string} gasUsed
   *     Gas used by transaction in gas. Integer string.
   * @prop {string} confirmations
   *     Current number of containing block confirmations.
   *     Integer string.
   */

  /**
   * Get list of account transactions.
   * @param {string} address - Account address.
   * @param {object} options - Request options.
   * @param {number} [options.startBlock=null]
   *     Block number of query range low end inclusive.
   * @param {number} [options.endBlock=null]
   *     Block number of query range high end inclusive.
   * @param {number} [options.page=null]
   *     Page number to request.
   * @param {number} [options.pageSize=null]
   *     Page size to request.
   * @return {module:client/etherscan~transaction[]}
   *     List of account transactions.
   */
  async listAccountTransactions (
    address, {
      startBlock = null,
      endBlock = null,
      page = null,
      pageSize = null
    } = {}
  ) {
    // Validate arguments
    arg.addressHex(address)
    if (startBlock !== null) {
      arg.integer(startBlock)
    }
    if (endBlock !== null) {
      arg.integer(endBlock)
    }
    if (page !== null) {
      arg.integer(page)
    }
    if (pageSize !== null) {
      arg.integer(pageSize)
    }

    // Private members
    const priv = privs.get(this)

    // Construct request address
    const requestUrl = new URL(url.account.listTransactions)
    requestUrl.searchParams.set('address', address)
    if (startBlock !== null) {
      requestUrl.searchParams.set('startBlock', startBlock)
    }
    if (endBlock !== null) {
      requestUrl.searchParams.set('endBlock', endBlock)
    }
    if (page !== null) {
      requestUrl.searchParams.set('page', page)
    }
    if (pageSize !== null) {
      requestUrl.searchParams.set('offset', pageSize)
    }
    if (priv.apiKey !== null) {
      requestUrl.searchParams.set('apikey', priv.apiKey)
    }
    const requestAddress = requestUrl.toString()

    // Acquire RPC response
    const rpcResponseJson = await request.get(requestAddress)
    const rpcResponse = JSON.parse(rpcResponseJson)

    // Extract result
    const result = rpcResponse.result

    // Return result
    return result
  }
}

// Expose
module.exports = Client

// Circular imports
const arg = require('../util/arg')