lib/fulfillment.js

'use strict'

/**
 * @module types
 */

const TypeRegistry = require('./type-registry')
const Condition = require('./condition')
const base64url = require('../util/base64url')
const Asn1Fulfillment = require('../schemas/fulfillment').Fulfillment

/**
 * Base class for fulfillment types.
 */
class Fulfillment {
  /**
   * Create a Fulfillment object from a URI.
   *
   * This method will parse a fulfillment URI and construct a corresponding
   * Fulfillment object.
   *
   * @param {String} serializedFulfillment URI representing the fulfillment
   * @return {Fulfillment} Resulting object
   */
  static fromUri (serializedFulfillment) {
    if (serializedFulfillment instanceof Fulfillment) {
      return serializedFulfillment
    } else if (typeof serializedFulfillment !== 'string') {
      throw new TypeError('Serialized fulfillment must be a string')
    }
    const fulfillment = Fulfillment.fromBinary(Buffer.from(serializedFulfillment, 'base64'))

    return fulfillment
  }

  /**
   * Create a Fulfillment object from a binary blob.
   *
   * This method will parse a stream of binary data and construct a
   * corresponding Fulfillment object.
   *
   * @param {Buffer} data Binary buffer
   * @return {Fulfillment} Resulting object
   */
  static fromBinary (data) {
    const fulfillmentJson = Asn1Fulfillment.decode(data)
    return Fulfillment.fromAsn1Json(fulfillmentJson)
  }

  static fromAsn1Json (json) {
    const FulfillmentClass = TypeRegistry.findByAsn1FulfillmentType(json.type).Class

    const condition = new FulfillmentClass()
    condition.parseAsn1JsonPayload(json.value)

    return condition
  }

  static fromJson (json) {
    const ConditionClass = TypeRegistry.findByName(json.type).Class

    const condition = new ConditionClass()
    condition.parseJson(json)

    return condition
  }

  /**
   * Return the type ID of this fulfillment.
   *
   * @return {Number} Type ID as an integer.
   */
  getTypeId () {
    return this.constructor.TYPE_ID
  }

  getTypeName () {
    return this.constructor.TYPE_NAME
  }

  /**
   * Return the bitmask of this fulfillment.
   *
   * For simple fulfillment types this is simply the empty set.
   *
   * For compound fulfillments, this returns the set of names of all
   * subfulfillment types, recursively.
   *
   * @return {Set<String>} Set of subtype names.
   */
  getSubtypes () {
    return new Set()
  }

  /**
   * Generate condition corresponding to this fulfillment.
   *
   * An important property of crypto-conditions is that the condition can always
   * be derived from the fulfillment. This makes it very easy to post
   * fulfillments to a system without having to specify which condition the
   * relate to. The system can keep an index of conditions and look up any
   * matching events related to that condition.
   *
   * @return {Condition} Condition corresponding to this fulfillment.
   */
  getCondition () {
    const condition = new Condition()
    condition.setHash(this.generateHash())
    condition.setTypeId(this.getTypeId())
    condition.setCost(this.calculateCost())
    condition.setSubtypes(this.getSubtypes())
    return condition
  }

  /**
   * Shorthand for getting condition URI.
   *
   * Stands for getCondition().serializeUri().
   *
   * @return {String} Condition URI.
   */
  getConditionUri () {
    return this.getCondition().serializeUri()
  }

  /**
   * Shorthand for getting condition encoded as binary.
   *
   * Stands for getCondition().serializeBinary().
   *
   * @return {Buffer} Binary encoded condition.
   */
  getConditionBinary () {
    return this.getCondition().serializeBinary()
  }

  /**
   * Generate the hash of the fulfillment.
   *
   * This method is a stub and will be overridden by subclasses.
   *
   * @return {Buffer} Fingerprint of the condition.
   *
   * @private
   */
  generateHash () {
    throw new Error('This method should be implemented by a subclass')
  }

  /**
   * Calculate the cost of the fulfillment payload.
   *
   * Each condition type has a standard deterministic formula for estimating the
   * cost of validating the fulfillment. This is an abstract function which will
   * be overridden by each of the types with the actual formula.
   *
   * @return {Number} Cost
   *
   * @private
   */
  calculateCost () {
    throw new Error('Condition types must implement calculateCost')
  }

  parseAsn1JsonPayload (json) {
    this.parseJson(json)
  }

  /**
   * Generate the URI form encoding of this fulfillment.
   *
   * Turns the fulfillment into a URI containing only URL-safe characters. This
   * format is convenient for passing around fulfillments in URLs, JSON and
   * other text-based formats.
   *
   * @return {String} Fulfillment as a URI
   */
  serializeUri () {
    return base64url.encode(this.serializeBinary())
  }

  getAsn1Json () {
    return {
      type: this.constructor.TYPE_ASN1_FULFILLMENT,
      value: this.getAsn1JsonPayload()
    }
  }

  /**
   * Serialize fulfillment to a buffer.
   *
   * Encodes the fulfillment as a string of bytes. This is used internally for
   * encoding subfulfillments, but can also be used to passing around
   * fulfillments in a binary protocol for instance.
   *
   * @return {Buffer} Serialized fulfillment
   */
  serializeBinary () {
    const asn1Json = this.getAsn1Json()
    return Asn1Fulfillment.encode(asn1Json)
  }

  serializeBase64Url () {
    return base64url.encode(this.serializeBinary())
  }

  /**
   * Validate this fulfillment.
   *
   * This implementation is a stub and will be overridden by the subclasses.
   *
   * @return {Boolean} Validation result
   */
  validate () {
    throw new Error('Not implemented')
  }
}

module.exports = Fulfillment