core/oipd-api/daemonApi.js

import axios from 'axios'
import { MPSingle } from '../../modules'
import { decodeArtifact } from '../../decoders'

/**
 * The Transaction ID on the Blockchain.
 * @typedef {string} TXID
 * @example <caption>Full TXID Reference</caption>
 * 8a83ecb7812ca2770814d996529e153b07b103424cd389b800b743baa9604c5b
 * @example <caption>Shortened TXID Reference</caption>
 * 8a83ec
 */

/**
 * Objects that are used to build an elasticsearch complex query
 * @typedef {Object} queryObject
 * @property {string} field - artifact property ex. artifact.info.title
 * @property {string} query - the query term that will be searched on the field ex. 'Some title'
 * @property {string} operator - Can be: "AND", "OR", "NOT", or "wrap" (wrap objects will also have a property called 'type' which can be either 'start', 'end', or 'all')
 * @property {string} type - Can be: 'start', 'end', or 'all'
 * @example <caption>Search a query on a specific field</caption>
 * let fieldObject = {field: "artifact.title", query: "Some Title"}
 * @example <caption>Search a query on all fields</caption>
 * let queryObject = {query: "cats"}
 * @example <caption>Add a Complex Operator (AND, OR, or NOT)</caption>
 * let complexObject = {operator: "AND"}
 * @example <caption>Wrap parenthesis around the entire search query</caption>
 * let wrapAll = {operator: "wrap", type: "all"}
 * @example <caption>Add a beginning parenthesis</caption>
 * let wrapStart = {operator: "wrap", type: "start"}
 * @example <caption> Add an ending parenthesis</caption>
 * let wrapEnd = {operator: "wrap", type: "end"}
 * @example <caption>Build a search query</caption>
 * let searchQuery = [
 *      {operator: "wrap", type: "start"},
 *      {field: "artifact.type", query: "research"},
 *      {operator: "AND"}
 *      {field: "artifact.info.year", query: "2017"}
 *      {operator: "wrap", type: "end"},
 *      {operator: "OR"},
 *      {operator: "wrap", type: "start"},
 *      {field: "artifact.info.year", query: "2016"},
 *      {operator: "AND"},
 *      {field: "artifact.type", query: "music"},
 *      {operator: "wrap", type: "end"},
 * ]
 * let query = DaemonApi.createQs(searchQuery)
 * //query === "( artifact.type:"research" AND artifact.info.year:"2017" ) OR ( artifact.info.year:"2016" AND artifact.type:"music" )"
 * let {artifacts} = await DaemonApi.searchArtifacts(query)
 * @example <caption>Make things easier on yourself with constants and functions</caption>
 * const field = (field, query) => {return {field, query}}
 * const query = query => {return {query}}
 * const WrapAll = {operator: "wrap", type: "all"}
 * const WrapStart = {operator: "wrap", type: "start"}
 * const WrapEnd = {operator: "wrap", type: "end"}
 * const AND = {operator: "AND"}
 * const OR  = {operator: "OR"}
 * const NOT = {operator: "NOT"}
 *
 * let query = [
 *      field("artifact.type", "research"),
 *      AND,
 *      field("artifact.info.year", "2017"),
 *      WrapAll,
 *      OR,
 *      WrapStart,
 *      field("artifact.type", "music",
 *      AND,
 *      field("artifact.info.year", "2016"),
 *      WrapEnd
 * ]
 * let qs = DaemonApi.createQs(query)
 */

const decodeArray = (artifacts) => {
  let tmpArray = []
  for (let art of artifacts) {
    tmpArray.push(decodeArtifact(art))
  }
  return tmpArray
}

// ToDo: change to 'https' when ready
const localhost = 'http://localhost:1606/oip'
const defaultOIPdURL = 'https://api.oip.io/oip'

/**
 * Class to make HTTP calls to an OIP Daemon
 */
class DaemonApi {
  /**
   * ##### Examples
   * Spawn an OIP Daemon API (OIPdAPI) that connects to a local running daemon
   * ```javascript
   * import {DaemonApi} from 'js-oip'
   *
   * let oipd = new DaemonApi("localhost:1606") //leave blank for default API URL
   * let latestArtifacts = await oipd.getLatestArtifacts()
   * ```
   * @param  {String} [daemonUrl="https://api.oip.io/oip"] - The URL of an OIP Daemon
   */
  constructor (daemonUrl) {
    if (daemonUrl) {
      if (daemonUrl === 'localhost') {
        this.setUrl(localhost)
      } else {
        this.setUrl(daemonUrl)
      }
    } else {
      this.setUrl(defaultOIPdURL)
    }
  }

  /**
   * Set the DaemonUrl
   * @param {String} daemonUrl - the URL of an OIP Daemon (OIPd)
   * @example
   * DaemonApi.setUrl('https://api.oip.io/oip')
   * let url = DaemonUrl.getUrl()
   * url === 'https://api.oip.io/oip' //true
   */
  setUrl (daemonUrl) {
    this.url = daemonUrl

    this.index = new axios.create({ // eslint-disable-line
      baseURL: this.url
    })
  }

  /**
   * Get current DaemonUrl
   * @return {String} daemonUrl
   * @example
   * let url = DaemonUrl.getUrl()
   */
  getUrl () {
    return this.url
  }

  /**
   * Get Axios Instance
   * @return {axios}
   */
  getNetwork () {
    return this.index
  }

  /**
   * Search the Index for artifacts by query
   * @param {string} query - your search query
   * @param {number} [limit=100] - max num of results
   * @return {Promise<Object>}
   * @example
   * let {success, artifacts, error} = await DaemonApi.search('myQuery')
   */
  async searchArtifacts (query, limit) {
    if (typeof query !== 'string') {
      return { success: false, error: `'query' must be of type string` }
    }

    let res
    try {
      res = await this.index.get(`/artifact/search`, {
        params: {
          q: query,
          limit
        }
      })
    } catch (err) {
      throw new Error(`Failed to search artifacts for: ${query} -- ${err}`)
    }
    if (res && res.data) {
      let { count, total, results } = res.data
      return { success: true, artifacts: decodeArray(results), count, total }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Search Artifacts by type and subtype
   * @param {string} type - options: video, music, audio, image, text, research, and property (as of 12/15/18)
   * @param {string} [subtype] - options: tomogram (as of (12/15/18)
   * @return {Promise<Object>}
   * @example
   * let {success, artifacts, error} = await DaemonApi.searchArtifactsByType('research', 'tomogram')
   */
  async searchArtifactsByType (type, subtype) {
    if (type.split('-').length === 2) {
      let split = type.split('-')[0]
      type = split[0]
      subtype = split[1]
    }

    const typeQs = `artifact.type:`
    const subtypeQs = `artifact.subtype:`

    type = type.toLowerCase()
    subtype = subtype ? subtype.toLowerCase() : null

    let typeArr = []
    switch (type) {
      // 40 and 41s
      case 'video':
        typeArr.push('video', 'Video', 'Video-Basic', 'Video-Series')
        break
      case 'music':
        typeArr.push('music')
        break
      case 'image':
        typeArr.push('Image-Basic', 'Image-Gallery')
        break
      case 'audio':
        typeArr.push('Audio-Basic', 'music')
        break
      case 'text':
        typeArr.push('Text-Basic')
        break
        // 42s
      case 'research':
        typeArr.push('research')
        break
      case 'property':
        typeArr.push('Property')
        break
      default:
        throw new Error(`invalid type: ${type}`)
    }

    let typeQuery = ``
    let subtypeQuery = ``
    const OR = 'OR'
    const AND = 'AND'
    for (let i = 0; i < typeArr.length; i++) {
      typeQuery += `${typeQs}${typeArr[i]}`
      if (i !== typeArr.length - 1) {
        typeQuery += ` ${OR} `
      }
    }

    let query = `(${typeQuery}) `
    if (subtype) {
      subtypeQuery += `${AND} `
      subtypeQuery += `${subtypeQs}${subtype}`
      query += subtypeQuery
    }

    try {
      return await this.searchArtifacts(query)
    } catch (err) {
      throw err
    }
  }

  /**
   * Generate a complex querystring for elasticsearch
   * @param {Array.<queryObject>} args - An array of objects that follow given example
   * @return {string}
   * @example <caption>Search for both Research and Music artifact types that were created in 2017</caption>
   * let args = [
   *      {operator: "wrap", type: 'start'},
   *      {field: "artifact.details.defocus", query: "-10"},
   *      {operator: "AND"},
   *      {field: "artifact.details.microscopist", query: "Yiwei Chang"},
   *      {operator: "wrap", type: "end"},
   *      {operator: "OR"},
   *      {operator: "wrap", type: "start"},
   *      {field: "artifact.details.defocus", query: "-8"},
   *      {operator: "AND"},
   *      {field: "artifact.details.microscopist", query: "Ariane Briegel"},
   *      {operator: "wrap", type: "end"},
   * ]
   * //the query would end up looking something like:
   * let query = "(artifact.details.defocus:"-10" AND artifact.details.microscopist:"Yiwei Chang") OR (artifact.details.defocus:"-8" AND artifact.details.microscopist:"Ariane Briegel")"
   * @example
   * let querystring = this.generateQs(args)
   * querystring === query //true
   * let {artifacts} = await this.searchArtifacts(querystring)
   */
  createQs (args) {
    let query = ``
    const AND = 'AND'
    const OR = 'OR'
    const NOT = 'NOT'
    for (let i = 0; i < args.length; i++) {
      if (i !== 0) {
        query += ' '
      }
      if (args[i].operator) {
        if (args[i].operator.toUpperCase() === AND || args[i].operator.toUpperCase() === OR || args[i].operator.toUpperCase() === NOT) {
          args[i] = args[i].operator.toUpperCase()
          query += `${args[i]}`
        } else if (args[i].operator.toLowerCase() === 'wrap') {
          if (args[i]['type'] === 'start') {
            query += '('
          } else if (args[i]['type'] === 'end') {
            query += ')'
          } else if (args[i]['type'] === 'all') {
            query = query.slice(0, -1) // this is arbitrary--just to standardize return vals
            query = `(${query})`
          }
        } else {
          throw new Error(`Provided invalid operator: Options: "AND", "OR", "NOT", "wrap"`)
        }
      } else {
        if (args[i].field) {
          query += `${args[i].field}:"${args[i].query}"`
        } else {
          query += `${args[i].query}` // do these need to be wrapped in quotes?
        }
      }
    }
    // console.log(query)
    return query
  }

  /**
   * Get an Artifact from the Index by TXID
   * @param {TXID} txid  - transaction id of the artifact you wish to retrieve
   * @return {Promise<Object>}
   * @example
   * let txid = 'cc9a11050acdc4401aec3f40c4cce123d99c0f2c27d4403ae4a2536ee38a4716'
   * let {success, artifact, error} = await DaemonApi.getArtifact(txid)
   */
  async getArtifact (txid) {
    let res
    try {
      res = await this.index.get(`/artifact/get/${txid}`)
    } catch (err) {
      return new Error(`Failed to get artifact: ${txid} -- ${err}`)
    }
    if (!res || !res.data) {
      return { success: false, error: `Missing response data: ${res.data} for txid ${txid}` }
    }

    if (!res.data.results || !Array.isArray(res.data.results) || res.data.results.length < 1) {
      return { success: false, error: `No results found for txid ${txid}` }
    }

    let [artifact] = res.data.results

    return { success: true, artifact: decodeArtifact(artifact) } // ToDO: OIPDecoder
  }

  /**
   * Get a Record from the Index by TXID
   * @param {TXID} txid  - transaction id of the artifact you wish to retrieve
   * @return {Promise<Object>}
   * @example
   * let txid = 'cc9a11050acdc4401aec3f40c4cce123d99c0f2c27d4403ae4a2536ee38a4716'
   * let {success, record, error} = await DaemonApi.getRecord(txid)
   */
  async getRecord (txid) {
    let res
    try {
      res = await this.index.get(`/oip042/record/get/${txid}`)
    } catch (err) {
      return new Error(`Failed to get Record: ${txid} -- ${err}`)
    }
    if (!res || !res.data) {
      return { success: false, error: `Missing response data: ${res.data} for txid ${txid}` }
    }

    if (!res.data.results || !Array.isArray(res.data.results) || res.data.results.length < 1) {
      return { success: false, error: `No results found for txid ${txid}` }
    }

    let [record] = res.data.results

    return { success: true, record: decodeArtifact(record) } // ToDO: OIPDecoder
  }

  /**
   * Get multiple artifacts by TXID
   * @param {Array.<TXID>} txids - an array of transaction IDs
   * @return {Promise<Object>}
   * @example
   * const txid1 = '6ffbffd475c7eabe0acc664087ac56c13ac7c2084746619182b360c2f19e430e'
   * const txid2 = 'f72c314d257d8062581788ab56bbe4ab1dc09dafb7961866903d1144575a3b48'
   * const txid3 = '0be3e260a9ff71464383e328d05d9e85984dd6636626bc0356eae8440de150aa'
   * let txArray = [txid1, txid2, txid3]
   * let {success, artifacts, error} = await DaemonApi.getArtifacts(txArray)
   */
  async getArtifacts (txids) {
    if (!Array.isArray(txids)) {
      return { success: false, error: `'txids' must be an Array of transaction IDs` }
    }
    let artifacts = []
    for (let txid of txids) {
      let res
      try {
        res = await this.getArtifact(txid)
      } catch (err) {
        throw new Error(`Failed to get artifacts: ${txids} -- ${err}`)
      }
      if (res.success) artifacts.push(res.artifact)
    }
    return { success: true, artifacts: artifacts }
  }

  /**
   * Get the latest artifacts published to the Index
   * @param {number} [limit=100] - The amount of artifact you want returns ( max: 1000 )
   * @param {boolean} [nsfw=false] - not safe for work
   * @return {Promise<Object>}
   * @example
   * let limit = 50
   * let {success, artifacts, error} = await DaemonApi.getLatestArtifacts(limit)
   */
  async getLatestArtifacts (limit = 100, nsfw = false) {
    let res
    try {
      res = await this.index.get(`/artifact/get/latest`, {
        params: {
          nsfw,
          limit
        }
      })
    } catch (err) {
      throw new Error(`Failed to get latest artifacts: ${err}`)
    }
    if (res && res.data) {
      let artifacts = res.data.results
      return { success: true, artifacts: decodeArray(artifacts) }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get the latest version 41 artifacts published to the Index
   * @param {number} [limit=100] - The amount of artifact you want returns ( max: 1000 )
   * @param {boolean} [nsfw=false] - not safe for work
   * @return {Promise<Object>}
   * @example
   * const limit = 50
   * let {success, artifacts, error} = await DaemonApi.getLatest041Artifacts(limit)
   */
  async getLatest041Artifacts (limit = 100, nsfw = false) {
    let res
    try {
      res = await this.index.get(`/oip041/artifact/get/latest`, {
        params: {
          nsfw,
          limit
        }
      })
    } catch (err) {
      throw new Error(`Failed to get latest 041 artifacts -- ${err}`)
    }

    if (res && res.data) {
      let artifacts = res.data.results
      return { success: true, artifacts: decodeArray(artifacts) }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get a version 41 Artifact from the Index by TXID
   * @param {TXID} txid  - transaction id of the artifact you wish to retrieve
   * @return {Promise<Object>}
   * @example
   * let txid = '8c204c5f39b67431c59c7703378b2cd3b746a64743e130de0f5cfb2118b5136b'
   * let {success, artifact, error} = await DaemonApi.get041Artifact(txid)
   */
  async get041Artifact (txid) {
    let res
    try {
      res = await this.index.get(`/oip041/artifact/get/${txid}`)
    } catch (err) {
      throw new Error(`Failed to get 041 artifact: ${txid} -- ${err}`)
    }
    if (res && res.data) {
      let [artifact] = res.data.results
      return { success: true, artifact: decodeArtifact(artifact) }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get multiple OIP041 artifact by their TXID
   * @param {Array.<TXID>} txids - an array of transaction IDs
   * @return {Promise<Object>}
   * @example
   * const txid1 = '8c204c5f39b67431c59c7703378b2cd3b746a64743e130de0f5cfb2118b5136b'
   * const txid2 = 'a690609a2a8198fbf4ed3fd7e4987637a93b7e1cad96a5aeac2197b7a7bf8fb9'
   * const txid3 = 'b4e6c9e86d14ca3565e57fed8b482d742a7a1cff0dd4cabfe9e3ea29efb3211c'
   * let txArray = [txid1, txid2, txid3]
   * let {success, artifacts, error} = await DaemonApi.get041Artifacts(txArray)
   */
  async get041Artifacts (txids) {
    if (!Array.isArray(txids)) {
      return { success: false, error: `'txids' must be an Array of transaction IDs` }
    }
    let artifacts = []
    for (let txid of txids) {
      let res
      try {
        res = await this.get041Artifact(txid)
      } catch (err) {
        throw new Error(`Failed to get 041 artifacts: ${txids} -- ${err}`)
      }
      if (res.success) artifacts.push(res.artifact)
    }
    return { success: true, artifacts: artifacts }
  }

  /**
   * Get the version 42 artifacts published to the Index
   * @param {number} [limit=100] - The amount of artifact you want returns ( max: 1000 )
   * @param {boolean} [nsfw=false] - not safe for work artifact
   * @return {Promise<Object>}
   * @example
   * const limit = 50
   * let {success, artifacts, error} = await DaemonApi.getLatest042Artifacts(limit)
   */
  async getLatest042Artifacts (limit = 100, nsfw = false) {
    let res
    try {
      res = await this.index.get(`/oip042/artifact/get/latest`, {
        params: {
          nsfw,
          limit
        }
      })
    } catch (err) {
      throw new Error(`Failed to get latest 042 artifacts: ${err}`)
    }
    if (res && res.data) {
      let artifacts = res.data.results
      return { success: true, artifacts: decodeArray(artifacts) }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get the latest Alexandria Media artifacts (version "40") published to the Index
   * @param {number} [limit=100] - The amount of artifact you want returns ( max: 1000 )
   * @param {boolean} [nsfw=false] - not safe for work
   * @return {Promise<Object>}
   * @example
   * const limit = 50
   * let {success, artifacts, error} = await DaemonApi.getLatestAlexandriaMediaArtifacts(limit)
   */
  async getLatestAlexandriaMediaArtifacts (limit = 100, nsfw = false) {
    let res
    try {
      res = await this.index.get(`/alexandria/artifact/get/latest`, {
        params: {
          nsfw,
          limit
        }
      })
    } catch (err) {
      throw new Error(`Failed to get latest alexandria media artifacts -- ${err}`)
    }
    if (res && res.data) {
      let artifacts = res.data.results
      return { success: true, artifacts: decodeArray(artifacts) }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get an Alexandria Media Artifact (version "40") from the Index by TXID
   * @param {TXID} txid  - transaction id of the artifact you wish to retrieve
   * @return {Promise<Object>}
   * @example
   * let txid = '756f9199c8992cd42c750cbd73d1fa717b31feafc3b4ab5871feadae9848acac'
   * let {success, artifact, error} = await DaemonApi.getAlexandriaMediaArtifact(txid)
   */
  async getAlexandriaMediaArtifact (txid) {
    let res
    try {
      res = await this.index.get(`/alexandria/artifact/get/${txid}`)
    } catch (err) {
      throw new Error(`Failed to get alexandria media artifact: ${txid} -- ${err}`)
    }
    if (res && res.data) {
      let [artifact] = res.data.results
      return { success: true, artifact: decodeArtifact(artifact) } // ToDo: OIPDecoder
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get one or more Alexandria Media (version "40") artifacts by their TXID
   * @param {Array.<TXID>} txids - an array of transaction IDs
   * @return {Promise<Object>}
   * @example
   * const txid1 = '33e04cb2dcf7004a460d0719eea36129ebaf48fb10cffff19653bfeeca9bc7ad'
   * const txid2 = 'a2110a1058b620d91bc78ad71e466d736f6b8b078025d19c23ddac6a3c0355ee'
   * const txid3 = 'b6f89f3c6410276f7d4cf9c3c58c4f0577495650e742e71dddc669c9e912217c'
   * let txArray = [txid1, txid2, txid3]
   * let {success, artifacts, error} = await DaemonApi.getAlexandriaMediaArtifacts(txArray)
   */
  async getAlexandriaMediaArtifacts (txids) {
    if (!Array.isArray(txids)) {
      return { success: false, error: `'txids' must be an Array of transaction IDs` }
    }
    let artifacts = []
    for (let txid of txids) {
      let res
      try {
        res = await this.getAlexandriaMediaArtifact(txid)
      } catch (err) {
        throw new Error(`Failed to get alexandria media artifacts: ${txids} -- ${err}`)
      }
      if (res.success) artifacts.push(res.artifact)
    }
    return { success: true, artifacts: artifacts }
  }

  /**
   * Search the floData from FLO provided by the Daemon's Index
   * @param {string} query - your search query
   * @param {number} [limit] - max num of results
   * @return {Promise<Object>} Returns FLO transactions that contain your query in their respective floData
   * @example
   * let query = 'myQuery'
   * let {success, txs, error} = await DaemonApi.searchFloData(query)
   * for (let i of txs) {
   *     let floData = i.tx.floData
   * }
   */
  async searchFloData (query, limit) {
    if (typeof query !== 'string') {
      return { success: false, error: `'query' must be of type string` }
    }
    let res
    try {
      res = await this.index.get(`/floData/search`, {
        params: {
          q: query,
          limit
        }
      })
    } catch (err) {
      throw new Error(`Failed to search flo data for: ${query} -- ${err}`)
    }
    if (res && res.data) {
      let txs = res.data.results
      return { success: true, txs }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get floData by TXID
   * @param {TXID} txid - the transaction id you wish to grab the floData from
   * @return {Promise.<Object>}
   * @example
   * let txid = '83452d60230d3c2c69000c2a79da79fe60cdf63012f946ac46e6df3409fb1fa7'
   * let {success, tx, error} = await DaemonApi.getFloData(txid)
   * let floData = tx.floData
   */
  async getFloData (txid) {
    let res
    try {
      res = await this.index.get(`/floData/get/${txid}`)
    } catch (err) {
      throw new Error(`Failed to get flo tx: ${txid} -- ${err}`)
    }
    if (res && res.data) {
      let [tx] = res.data.results
      return { success: true, tx }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get a Multipart Single by its TXID
   * @param {TXID} txid - transaction id of the single multipart
   * @return {Promise<Object>}
   * @example
   * let txid = 'f550b9739e7453224075630d44cba24c31959af913aeb7cb364a563f96f54548'
   * let {success, multipart, error} = await DaemonApi.getMultipart(txid)
   */
  async getMultipart (txid) {
    let res
    try {
      res = await this.index.get(`/multipart/get/id/${txid}`)
    } catch (err) {
      throw new Error(`Failed to get multipart by txid: ${txid} -- ${err}`)
    }
    if (res && res.data) {
      let [mp] = res.data.results
      return { success: true, multipart: new MPSingle(mp) }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get OIP Multiparts by the First TXID Reference
   * @param {string} ref - the TXID reference of the first multipart
   * @param {number} [limit] - max num of results
   * @return {Promise<Object>}
   * @example
   * let ref = '8c204c5f39'
   * let {success, multiparts, error} = await DaemonApi.getMultiparts(ref)
   */
  async getMultiparts (ref, limit) {
    let res
    let querystring = `/multipart/get/ref/${ref}`
    try {
      res = await this.index.get(querystring, {
        params: {
          limit
        }
      })
    } catch (err) {
      throw new Error(`Failed to get multiparts by ref: ${ref} -- ${err}`)
    }
    if (res && res.data) {
      let results = res.data.results
      let multiparts = []
      for (let mp of results) {
        multiparts.push(new MPSingle(mp))
      }
      multiparts.sort((a, b) => a.getPart() - b.getPart())
      return { success: true, multiparts }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get a historian data point by its txid
   * @param {TXID} txid
   * @return {Promise<Object>}
   * @example
   * let id = '83452d60230d3c2c69000c2a79da79fe60cdf63012f946ac46e6df3409fb1fa7'
   * let {success, hdata, error} = await DaemonApi.getHistorianData(id)
   */
  async getHistorianData (txid) {
    let res
    try {
      res = await this.index.get(`historian/get/${txid}`)
    } catch (err) {
      throw new Error(`Failed to get historian data by txid: ${txid} -- ${err}`)
    }
    if (res && res.data) {
      let [hdata] = res.data.results
      return { success: true, hdata }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get the latest historian data points
   * @param {number} [limit=100]
   * @return {Promise<Object>}
   * @example
   * let {success, hdata, error} = await DaemonApi.getLastestHistorianData()
   */
  async getLastestHistorianData (limit = 100) {
    let res
    try {
      res = await this.index.get(`historian/get/latest`, {
        params: {
          limit
        }
      })
    } catch (err) {
      throw new Error(`Failed to get latest historian data: ${err}`)
    }
    if (res && res.data) {
      let hdata = res.data.results
      return { success: true, hdata }
    } else {
      return { success: false, error: `Missing response data: ${res.data}` }
    }
  }

  /**
   * Get OIP Daemon specs
   * @return {Promise<Object>}
   * @example
   * let versionData = await DaemonApi.getVersion()
   */
  async getVersion () {
    let res
    try {
      res = await this.index.get('/daemon/version')
    } catch (err) {
      throw new Error(`Failed to get daemon version: ${err}`)
    }
    return res.data
  }

  /**
   * Get the Daemon's sync status
   * @return {Promise<Object>}
   */
  async getSyncStatus () {
    let res
    try {
      res = await this.index.get('/sync/status')
    } catch (err) {
      throw new Error(`Failed to get sync status: ${err}`)
    }
    return res.data
  }

  /**
   * Get latest oip5 records
   * @param {Object} [options]
   * @param {number} [options.limit=10] - max num of results
   * @param {string} [options.after] - the string ID returned on response to get the next set of data
   * @param {number} [options.pages] - page number
   * @param {string} [options.sort] - sort field ascending or descending. Format must match: ([0-9a-zA-Z._-]+:[ad]$?)+
   * @return {Promise<Object>}
   */
  async getLatestOip5Records (options) {
    let res
    try {
      res = await this.index.get('o5/record/get/latest', {
        params: {
          ...options
        }
      })
    } catch (err) {
      throw new Error(`Failed to get latest oip5 records: ${err}`)
    }
    if (res && res.data) {
      res = { success: true, payload: decodeResponseData(res.data) }
      return res
    } else {
      return { success: false, error: `No data returned from axios response on getLatestOip5Records` }
    }
  }

  /**
   * Get latest oip5 templates
   * @param {Object} [options]
   * @param {number} [options.limit=10] - max num of results
   * @param {string} [options.after] - the string ID returned on response to get the next set of data
   * @param {number} [options.pages] - page number
   * @param {string} [options.sort] - sort field ascending or descending. Format must match: ([0-9a-zA-Z._-]+:[ad]$?)+
   * @return {Promise<Object>}
   */
  async getLatestOip5Templates (options) {
    let res
    try {
      res = await this.index.get('o5/template/get/latest', {
        params: {
          ...options
        }
      })
    } catch (err) {
      throw new Error(`failed to get latest oip5 templates: ${err}`)
    }
    if (res && res.data) {
      res = { success: true, payload: decodeResponseData(res.data) }
      return res
    } else {
      return { success: false, error: `Failed to get data back from axios response` }
    }
  }

  /**
   * Get oip5 record
   * @param {string} [txid] - transaction id of record
   * @return {Promise<Object>}
   */
  async getOip5Record (txid) {
    let res
    try {
      res = await this.index.get(`o5/record/get/${txid}`)
    } catch (err) {
      return { success: false, error: err }
    }
    if (res && res.data) {
      res = { success: true, payload: decodeResponseData(res.data) }
      return res
    } else {
      return { success: false, error: `no data returned from axios response getOip5Record: ${txid}` }
    }
  }

  /**
   * Get oip5 records
   * @param {Array.<string>|string} txids - transaction id of record
   * @return {Promise<Array.<Object>>}
   */
  async getOip5Records (txids) {
    if (typeof txids === 'string') {
      return this.getOip5Record(txids)
    }
    let promiseArray = []
    for (let txid of txids) {
      promiseArray.push(this.getOip5Record(txid))
    }
    let responseArray = []
    for (let promise of promiseArray) {
      let resolvedPromise
      try {
        resolvedPromise = await promise
      } catch (err) {
        throw new Error(`Failed to get oip5 records: ${err}`)
      }
      responseArray.push(resolvedPromise)
    }
    return responseArray
  }

  /**
   * Get oip5 template
   * @param {string} [txid] - transaction id of record
   * @return {Promise<Object>}
   */
  async getOip5Template (txid) {
    let res
    try {
      res = await this.index.get(`o5/template/get/${txid}`)
    } catch (err) {
      throw new Error(`Failed to get oip5 template: ${err}`)
    }
    if (res && res.data) {
      res = { success: true, payload: decodeResponseData(res.data) }
      return res
    } else {
      return { success: false, error: `No data returned from axios request getOip5Template: ${txid}` }
    }
  }

  /**
   * Get oip5 templates
   * @param {string|Array.<string>} txids - transaction ids of record
   * @return {Promise<Array.<Object>>}
   */
  async getOip5Templates (txids) {
    if (typeof txids === 'string') {
      return this.getOip5Template(txids)
    }
    if (!Array.isArray(txids)) {
      throw new Error(`The param "txids" must be an array of txids or a single txid string`)
    }
    let promiseArray = []
    for (let txid of txids) {
      promiseArray.push(this.getOip5Template(txid))
    }
    let responseArray = []
    for (let promise of promiseArray) {
      let resolvedPromise
      try {
        resolvedPromise = await promise
      } catch (err) {
        throw new Error(`Could not get oip5 templates: ${err}`)
      }

      responseArray.push(resolvedPromise)
    }
    return responseArray
  }

  /**
   * Get oip5 template
   * @param {string|Array<string>} [tmplIdentifiers] - 'template identifiers' transaction IDs'
   * @return {Promise<Object>}
   */
  async getOip5Mapping (tmplIdentifiers) {
    if (typeof tmplIdentifiers === 'string') {
      tmplIdentifiers = [tmplIdentifiers]
    }
    let res
    try {
      res = await this.index.get(`o5/record/mapping/${tmplIdentifiers}`)
    } catch (err) {
      throw new Error(`Failed to get oip5 mappings: ${err}`)
    }

    if (res && res.data) {
      res = { success: true, payload: res.data }
      return res
    } else {
      return { success: false, error: `Failed to get data back from axios request trying to get oip5 mappings` }
    }
  }

  /**
   * Search oip5 templates
   * @param {Object} options
   * @param {string} options.q - query string query
   * @param {number} [options.limit=10] - max num of results (limited to 10 on backend)
   * @param {string} [options.after] - the string ID returned on response to get the next set of data
   * @param {number} [options.pages] - page number
   * @param {string} [options.sort] - sort field ascending or descending. Format must match: ([0-9a-zA-Z._-]+:[ad]$?)+
   * @return {Promise<Object>}
   */
  async searchOip5Records (options) {
    let res
    try {
      res = await this.index.get(`o5/record/search`, {
        params: {
          ...options
        }
      })
    } catch (err) {
      throw new Error(`Failed to search oip5 records: ${err}`)
    }

    if (res && res.data) {
      res = { success: true, payload: decodeResponseData(res.data) }
      return res
    } else {
      return { success: false, error: 'Did not receive data back from axios request trying to search oip5 records' }
    }
  }

  /**
   * Search oip5 templates
   * @param {Object} options
   * @param {string} options.q - query string query
   * @param {number} [options.limit=10] - max num of results (limited to 10 on backend)
   * @param {string} [options.after] - the string ID returned on response to get the next set of data
   * @param {number} [options.pages] - page number
   * @param {string} [options.sort] - sort field ascending or descending. Format must match: ([0-9a-zA-Z._-]+:[ad]$?)+
   * @return {Promise<Object>}
   */
  async searchOip5Templates (options) {
    let res
    try {
      res = await this.index.get(`o5/template/search`, {
        params: {
          ...options
        }
      })
    } catch (err) {
      throw new Error(`Failed to search oip5 templates: ${err}`)
    }

    if (res && res.data) {
      res = { success: true, payload: decodeResponseData(res.data) }
      return res
    } else {
      return { success: false, error: 'Did not receive data back from axios request trying to search oip5 templates' }
    }
  }
  async isVerifiedPublisher ({ pubAddr, templateName, apiUrl, localhost = false }) {
    const VERIFIED_PUBLISHER_TEMPLATE = 'tmpl_F471DFF9'
    const verifyApiEndpoint = 'https://api.oip.io/verified/publisher/check/'
    const localhostVerifyEndpoint = 'http://localhost:1607/verified/publisher/check/'

    let tmplName = templateName || VERIFIED_PUBLISHER_TEMPLATE
    let verifyEndpoint = apiUrl || verifyApiEndpoint
    if (localhost) verifyEndpoint = localhostVerifyEndpoint

    const q = `_exists_:record.details.${tmplName} AND meta.signed_by:${pubAddr}`
    let results
    try {
      results = await this.searchOip5Records({ q })
    } catch (err) {
      throw Error(`Failed to search oip5 record for verified publisher: \n ${err}`)
    }

    const { success, payload } = results
    if (success) {
      const { results } = payload
      if (results.length === 0) {
        return { success: false, error: `No verified publisher found` }
      }
      const { meta, record } = results[0]
      const { txid } = meta

      const { details } = record
      const twitterId = details[tmplName].twitterId
      const gabId = details[tmplName].gabId

      let registeredPublisherTxid = details[tmplName].registeredPublisher
      if (registeredPublisherTxid.raw) {
        let buf = Buffer.from(registeredPublisherTxid.raw, 'base64')
        registeredPublisherTxid = buf.toString('hex')
      }
      let regPub
      try {
        regPub = await this.getOip5Record(registeredPublisherTxid)
      } catch (err) {
        throw Error(`Failed to search oip5 record for verified publisher: \n ${err}`)
      }

      // todo: support testnet templates ?
      let name
      if (regPub.success) {
        let res = regPub.payload.results[0]
        if (res) {
          let tmpl = res.record.details['tmpl_433C2783']
          if (tmpl) {
            name = res.record.details['tmpl_433C2783'].name
          }
        }
      }

      let res
      try {
        res = await axios.get(`${verifyEndpoint}${txid}`)
      } catch (err) {
        throw Error(`Failed to hit verify api endpoint url: ${verifyEndpoint} \n ${err}`)
      }
      return { success: true,
        payload: {
          ...res.data,
          twitterId,
          gabId,
          name
        } }
    } else {
      return { success: false, error: `Did not receive data back from axios request trying to search oip5 verified publisher: ${pubAddr}` }
    }
  }
}

function decodeResponseData (payload) {
  const { next, ...rest } = payload
  return {
    next: decodeURI(next),
    ...rest
  }
}

export default DaemonApi