core/oip/oip.js

import { ECPair, payments } from 'bitcoinjs-lib'

import { DaemonApi } from '../oipd-api'
import { MultipartX } from '../../modules'
import { OIPRecord } from '../../modules/records'
import { EditRecord } from '../../modules/records/edit'
import { ExplorerWallet, RPCWallet } from '../../modules/wallets'
import { FLODATA_MAX_LEN } from '../../modules/flo/FLOTransaction'
import { floMainnet, floTestnet, floRegtest } from '../../config'

/**
 * Class to publish, register, edit, transfer, and deactivate OIP Records
 */
class OIP {
  /**
   * ##### Example
   * ```javascript
   * import {OIP} from 'js-oip'
   *
   * let wif = "cRVa9rNx5N1YKBw8PhavegJPFCiYCfC4n8cYmdc3X1Y6TyFZGG4B"
   * let oip = new OIP(wif, "testnet")
   * ```
   * @param {String} wif - private key in Wallet Import Format (WIF) see: {@link https://en.bitcoin.it/wiki/Wallet_import_format}
   * @param {String} [network="mainnet"] - Use "testnet" for mainnet
   * @param {Object} [options] - Options to for the OIP class
   * @param {Object} [options.publicAddress] - Explicitly define a public address for the passed WIF
   * @param {Object} [options.oipdURL] - The OIP daemon API url to use when looking up the Latest Record in oip.edit()
   * @param {Object} [options.rpc] - By default, OIP uses a connection to a web explorer to publish Records, you can however use a connection to an RPC wallet instead by passing an object into this option
   * @param {Object} [options.rpc.host] - The Hostname for the RPC wallet connection
   * @param {Object} [options.rpc.port] - The Port for the RPC wallet connection
   * @param {Object} [options.rpc.username] - The Username for the RPC wallet connection
   * @param {Object} [options.rpc.password] - The Password for the RPC wallet connection
   */
  constructor (wif, network, options) {
    this.options = options || {}

    this.options.wif = wif
    this.options.network = network

    // If public address is not defined, calculate it using bitcoin-js (used by RPC-Wallet)
    if (!this.options.publicAddress) {
      let tmpNetwork = floMainnet
      if (network === 'testnet') { tmpNetwork = floTestnet }
      if (network === 'regtest') { tmpNetwork = floRegtest }

      let myECPair = ECPair.fromWIF(this.options.wif, tmpNetwork.network)
      this.options.publicAddress = payments.p2pkh({ pubkey: myECPair.publicKey, network: tmpNetwork.network }).address
    }

    if (this.options.rpc) {
      this.wallet = new RPCWallet(this.options)
      this.walletInitialized = false
    } else {
      this.wallet = new ExplorerWallet(this.options)
      this.walletInitialized = true
    }

    this.oipdAPI = new DaemonApi(this.options.oipdURL)
  }

  /**
   * Sign an OIP Record (if unsigned), and verify it's signature
   * @param  {OIPRecord} record - The record you want to make sure is signed
   */
  async signRecord (record) {
    // Check if a signature exists
    if (!record.getSignature() || record.getSignature() === '') {
      // Set the publisher address
      record.setPubAddress(this.options.publicAddress)
      // Check if a timestamp is set, and if not, set it to the current date (in ms time)
      if (!record.getTimestamp()) { record.setTimestamp(Date.now()) }

      // Attempt the signing of the Record
      let { success, error } = await record.signSelf(this.wallet.signMessage.bind(this.wallet))
      if (!success) {
        // If there was an error, log the stack, and then return the error.
        console.log(error.stack)
        throw new Error(`Failed to sign record: ${error}`)
      }
    }

    // Check if the record has a valid signature
    if (!record.hasValidSignature()) {
      throw new Error(`Invalid signature`)
    }
  }

  /**
   * Broadcast an OIP Record
   * @param {OIPRecord} record - Any Object whos class extends OIPRecord (Artifact, Publisher, Platform, Retailer, Influencer, EditRecord, etc)
   * @param {String} methodType - The method you are wanting to perform, i.e. `publish`, `edit`, `deactivate`, `transfer` etc
   * @return {Promise<Object>} response - An object that contains a var for `success`, the `record` that was published, and the `editRecord` if it is an edit
   * let oip = new OIP(wif, "testnet")
   * let artifact = new Artifact()
   * let result = await oip.broadcastRecord(artifact, 'publish')
   */
  async broadcastRecord (record, methodType) {
    // Verify that we are generally an OIPRecord (aka, we have the required signature and serialization functions)
    if (!(record instanceof OIPRecord)) {
      throw new Error(`Record must be an instanceof OIPRecord`)
    }

    // Make sure the wallet has initialized and is ready for use
    if (!this.walletInitialized) {
      await this.wallet.initialize()
      this.walletInitialized = true
    }

    // Make sure the record is signed, and if not, sign it.
    try {
      await this.signRecord(record)
    } catch (error) {
      return { success: false, error: `Error while Signing Record: ${error}` }
    }

    // Make sure the Record is valid
    let { success, error } = record.isValid()

    if (!success) {
      return { success: false, error: `Invalid record: ${error}` }
    }

    // Create the data we are broadcasting to the chain
    let broadcastString = record.serialize(methodType)

    // Array to store txids
    let txids = []

    // Check if we need to publish it using Multiparts, or if it will fit into a single transaction
    if (broadcastString.length > FLODATA_MAX_LEN) {
      try {
        // Split the broadcast string up and publish the multiparts for it
        txids = await this.publishMultiparts(broadcastString)
      } catch (err) {
        return { success: false, error: `Failed to publish multiparts: ${err}` }
      }
    } else {
      try {
        // Broadcast it in a single transaction :)
        let txid = await this.wallet.sendDataToChain(broadcastString)
        txids = [txid]
      } catch (err) {
        return { success: false, error: `Failed to broadcast message: ${err}` }
      }
    }

    // Set the txid to the Record
    record.setTXID(txids[0])

    // Grab the data we need and bundle it for returning
    let response = { success: true, txids, record }

    // If we are an edit record, also return the edit record :)
    if (record instanceof EditRecord) {
      // Grab the patched record to return
      let patchedRecord = record.getPatchedRecord()
      // Set the edit version to the EditRecord txid
      patchedRecord.setEditVersion(txids[0])

      // Move EditRecord and set patchedRecord
      response.record = patchedRecord
      response.editRecord = record
    }

    // Return our built response
    return response
  }

  /**
   * Publish OIP Records
   * @param {OIPRecord} record - an Artifact, Publisher, Platform, Retailer, or Influencer
   * @return {Promise<Object>} response - An object that contains a var for `success`, the `record` that was published
   * let oip = new OIP(wif, "testnet")
   * let artifact = new Artifact()
   * let result = await oip.publish(artifact)
   */
  async publish (record) {
    // Forward the publish directly on to the broadcastRecord method
    let res = await this.broadcastRecord(record, 'publish')
    return res
  }

  // async register(record) {
  // } //ToDo

  /**
   * Publish an Edit for a Record
   * @param  {OIPRecord} editedRecord - The new version of the Record
   * @return {Promise<Object>} response - An object that contains a var for `success`, the `record` that was published, and the `editRecord`
   * @Example
   * let oip = new OIP(wif, "testnet")
   * let record = new Artifact(previousArtifactJSON)
   * record.setTitle('new title')
   * let result = await oip.edit(record)
   */
  async edit (editedRecord) {
    // Lookup the currently latest version of the Record
    let original
    try {
      let { success, record, error } = await this.oipdAPI.getRecord(editedRecord.getOriginalTXID())
      // If OIPd reported an error, then throw the error
      if (success) {
        original = record
      } else {
        return { success: false, error: `Unable to load Original Record from OIP daemon: ${error}` }
      }
    } catch (e) {
      // Throw an error if the API request failed
      return { success: false, error: `Error while requesting Original Record from OIP daemon: ${e}` }
    }
    // Throw an Error if record does not exist
    if (!original) {
      return { success: false, error: `A Record with the txid ${editedRecord.getOriginalTXID()} was not found in OIP daemon! Please make sure you have set 'options.oipdURL' to your OIP daemon server!` }
    }

    // Set the Publisher Address before we sign
    editedRecord.setPubAddress(this.options.publicAddress)
    // Check if a timestamp is set, and if not, set it to the current date (in ms time)
    if (!editedRecord.getTimestamp()) { editedRecord.setTimestamp(Date.now()) }

    // Create an Edit Record from the Original and Edited
    let edit = new EditRecord(undefined, original, editedRecord)

    // Publish to chain
    let res = await this.broadcastRecord(edit, 'edit')

    return res
  }

  // async transfer(record) {
  // } //ToDO
  // async deactivate(record) {
  // } //ToDo

  /**
   * Publish data that exceeds the maximum floData length in multiple parts
   * @param {string} data - The data you wish to publish
   * @return {Promise<Array.<String>>} txids - An array of transaction IDs
   * @example
   * let oip = new OIP(wif, "testnet")
   * let txArray = await oip.publishMultiparts(superLongStringData)
   * //For multipart publishing, use oip.publish() instead. Will auto redirect to this function
   */
  async publishMultiparts (data) {
    if (typeof data !== 'string') {
      throw new Error(`Data must be of type string. Got: ${typeof data}`)
    }

    // Make sure the wallet has had time to initialize
    if (!this.walletInitialized) {
      await this.wallet.initialize()
      this.walletInitialized = true
    }

    let mpx = new MultipartX(data)
    let mps = mpx.getMultiparts()

    let txids = []

    for (let mp of mps) {
      // set reference, addr, and sign
      mp.setAddress(this.options.publicAddress)
      if (txids.length > 0) {
        mp.setReference(txids[0])
      }
      let { error } = await mp.signSelf(this.wallet.signMessage.bind(this.wallet))
      if (error) {
        throw new Error(`Failed to sign multipart: ${error}`)
      }

      // not going to be valid yet or will it
      if (!mp.isValid().success) {
        console.log(mp)
        throw new Error(`Invalid multipart: ${mp.isValid().error}`)
      }

      let txid
      try {
        // console.log(mp.toString())
        // console.log(mp.toString().length)
        // throw new Error('STOP')
        txid = await this.wallet.sendDataToChain(mp.toString())
      } catch (err) {
        console.log(err.stack)
        throw new Error(`Failed to broadcast multipart: ${err}`)
      }
      // console.log(txid)
      txids.push(txid)
    }
    return txids
  }
}

export default OIP