Source: primitives/covenant.js

/*!
 * covenant.js - covenant object for hsd
 * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
 * https://github.com/handshake-org/hsd
 */

'use strict';

const assert = require('bsert');
const bio = require('bufio');
const util = require('../utils/util');
const rules = require('../covenants/rules');
const consensus = require('../protocol/consensus');
const {encoding} = bio;
const {types, typesByVal} = rules;

/** @typedef {import('@handshake-org/bfilter').BloomFilter} BloomFilter */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('./address')} Address */

/** @typedef {ReturnType<Covenant['getJSON']>} CovenantJSON */

/**
 * Covenant
 * @alias module:primitives.Covenant
 * @property {Number} type
 * @property {Buffer[]} items
 * @property {Number} length
 */

class Covenant extends bio.Struct {
  /**
   * Create a covenant.
   * @constructor
   * @param {rules.types|Object} [type]
   * @param {Buffer[]} [items]
   */

  constructor(type, items) {
    super();

    this.type = types.NONE;
    this.items = [];

    if (type != null)
      this.fromOptions(type, items);
  }

  /**
   * Inject properties from options object.
   * @param {rules.types|Object} [type]
   * @param {Buffer[]} [items]
   * @returns {this}
   */

  fromOptions(type, items) {
    if (type && typeof type === 'object') {
      items = type.items;
      type = type.type;
    }

    if (Array.isArray(type))
      return this.fromArray(type);

    if (type != null) {
      assert((type & 0xff) === type);
      this.type = type;
      if (items)
        return this.fromArray(items);
      return this;
    }

    return this;
  }

  /**
   * Get an item.
   * @param {Number} index
   * @returns {Buffer}
   */

  get(index) {
    if (index < 0)
      index += this.items.length;

    assert((index >>> 0) === index);
    assert(index < this.items.length);

    return this.items[index];
  }

  /**
   * Set an item.
   * @param {Number} index
   * @param {Buffer} item
   * @returns {Covenant}
   */

  set(index, item) {
    if (index < 0)
      index += this.items.length;

    assert((index >>> 0) === index);
    assert(index <= this.items.length);
    assert(Buffer.isBuffer(item));

    this.items[index] = item;
    return this;
  }

  /**
   * Push an item.
   * @param {Buffer} item
   * @returns {this}
   */

  push(item) {
    assert(Buffer.isBuffer(item));
    this.items.push(item);
    return this;
  }

  /**
   * Get a uint8.
   * @param {Number} index
   * @returns {Number}
   */

  getU8(index) {
    const item = this.get(index);
    assert(item.length === 1);
    return item[0];
  }

  /**
   * Push a uint8.
   * @param {Number} num
   * @returns {Covenant}
   */

  pushU8(num) {
    assert((num & 0xff) === num);
    const item = Buffer.allocUnsafe(1);
    item[0] = num;
    this.push(item);
    return this;
  }

  /**
   * Get a uint32.
   * @param {Number} index
   * @returns {Number}
   */

  getU32(index) {
    const item = this.get(index);
    assert(item.length === 4);
    return bio.readU32(item, 0);
  }

  /**
   * Push a uint32.
   * @param {Number} num
   * @returns {Covenant}
   */

  pushU32(num) {
    assert((num >>> 0) === num);
    const item = Buffer.allocUnsafe(4);
    bio.writeU32(item, num, 0);
    this.push(item);
    return this;
  }

  /**
   * Get a hash.
   * @param {Number} index
   * @returns {Hash}
   */

  getHash(index) {
    const item = this.get(index);
    assert(item.length === 32);
    return item;
  }

  /**
   * Push a hash.
   * @param {Hash} hash
   * @returns {Covenant}
   */

  pushHash(hash) {
    assert(Buffer.isBuffer(hash));
    assert(hash.length === 32);
    this.push(hash);
    return this;
  }

  /**
   * Get a string.
   * @param {Number} index
   * @returns {String}
   */

  getString(index) {
    const item = this.get(index);
    assert(item.length >= 1 && item.length <= 63);
    return item.toString('binary');
  }

  /**
   * Push a string.
   * @param {String} str
   * @returns {Covenant}
   */

  pushString(str) {
    assert(typeof str === 'string');
    assert(str.length >= 1 && str.length <= 63);
    this.push(Buffer.from(str, 'binary'));
    return this;
  }

  /**
   * Test whether the covenant is known.
   * @returns {Boolean}
   */

  isKnown() {
    return this.type <= types.REVOKE;
  }

  /**
   * Test whether the covenant is unknown.
   * @returns {Boolean}
   */

  isUnknown() {
    return this.type > types.REVOKE;
  }

  /**
   * Test whether the covenant is a payment.
   * @returns {Boolean}
   */

  isNone() {
    return this.type === types.NONE;
  }

  /**
   * Test whether the covenant is a claim.
   * @returns {Boolean}
   */

  isClaim() {
    return this.type === types.CLAIM;
  }

  /**
   * Test whether the covenant is an open.
   * @returns {Boolean}
   */

  isOpen() {
    return this.type === types.OPEN;
  }

  /**
   * Test whether the covenant is a bid.
   * @returns {Boolean}
   */

  isBid() {
    return this.type === types.BID;
  }

  /**
   * Test whether the covenant is a reveal.
   * @returns {Boolean}
   */

  isReveal() {
    return this.type === types.REVEAL;
  }

  /**
   * Test whether the covenant is a redeem.
   * @returns {Boolean}
   */

  isRedeem() {
    return this.type === types.REDEEM;
  }

  /**
   * Test whether the covenant is a register.
   * @returns {Boolean}
   */

  isRegister() {
    return this.type === types.REGISTER;
  }

  /**
   * Test whether the covenant is an update.
   * @returns {Boolean}
   */

  isUpdate() {
    return this.type === types.UPDATE;
  }

  /**
   * Test whether the covenant is a renewal.
   * @returns {Boolean}
   */

  isRenew() {
    return this.type === types.RENEW;
  }

  /**
   * Test whether the covenant is a transfer.
   * @returns {Boolean}
   */

  isTransfer() {
    return this.type === types.TRANSFER;
  }

  /**
   * Test whether the covenant is a finalize.
   * @returns {Boolean}
   */

  isFinalize() {
    return this.type === types.FINALIZE;
  }

  /**
   * Test whether the covenant is a revocation.
   * @returns {Boolean}
   */

  isRevoke() {
    return this.type === types.REVOKE;
  }

  /**
   * Build helpers
   */

  /**
   * Set covenant to NONE.
   * @returns {Covenant}
   */

  setNone() {
    this.type = types.NONE;
    this.items = [];
    return this;
  }

  /**
   * Set covenant to OPEN.
   * @param {Hash} nameHash
   * @param {Buffer} rawName
   * @returns {Covenant}
   */

  setOpen(nameHash, rawName) {
    this.type = types.OPEN;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(0);
    this.push(rawName);
    return this;
  }

  /**
   * Set covenant to BID.
   * @param {Hash} nameHash
   * @param {Number} start
   * @param {Buffer} rawName
   * @param {Hash} blind
   * @returns {Covenant}
   */

  setBid(nameHash, start, rawName, blind) {
    this.type = types.BID;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(start);
    this.push(rawName);
    this.pushHash(blind);

    return this;
  }

  /**
   * Set covenant to REVEAL.
   * @param {Hash} nameHash
   * @param {Number} height
   * @param {Hash} nonce
   * @returns {Covenant}
   */

  setReveal(nameHash, height, nonce) {
    this.type = types.REVEAL;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(height);
    this.pushHash(nonce);

    return this;
  }

  /**
   * Set covenant to REDEEM.
   * @param {Hash} nameHash
   * @param {Number} height
   * @returns {Covenant}
   */

  setRedeem(nameHash, height) {
    this.type = types.REDEEM;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(height);

    return this;
  }

  /**
   * Set covenant to REGISTER.
   * @param {Hash} nameHash
   * @param {Number} height
   * @param {Buffer} record
   * @param {Hash} blockHash
   * @returns {Covenant}
   */

  setRegister(nameHash, height, record, blockHash) {
    this.type = types.REGISTER;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(height);
    this.push(record);
    this.pushHash(blockHash);

    return this;
  }

  /**
   * Set covenant to UPDATE.
   * @param {Hash} nameHash
   * @param {Number} height
   * @param {Buffer} resource
   * @returns {Covenant}
   */

  setUpdate(nameHash, height, resource) {
    this.type = types.UPDATE;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(height);
    this.push(resource);

    return this;
  }

  /**
   * Set covenant to RENEW.
   * @param {Hash} nameHash
   * @param {Number} height
   * @param {Hash} blockHash
   * @returns {Covenant}
   */

  setRenew(nameHash, height, blockHash) {
    this.type = types.RENEW;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(height);
    this.pushHash(blockHash);

    return this;
  }

  /**
   * Set covenant to TRANSFER.
   * @param {Hash} nameHash
   * @param {Number} height
   * @param {Address} address
   * @returns {Covenant}
   */

  setTransfer(nameHash, height, address) {
    this.type = types.TRANSFER;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(height);
    this.pushU8(address.version);
    this.push(address.hash);

    return this;
  }

  /**
   * Set covenant to REVOKE.
   * @param {Hash} nameHash
   * @param {Number} height
   * @param {Buffer} rawName
   * @param {Number} flags
   * @param {Number} claimed
   * @param {Number} renewals
   * @param {Hash} blockHash
   * @returns {Covenant}
   */

  setFinalize(nameHash, height, rawName, flags, claimed, renewals, blockHash) {
    this.type = types.FINALIZE;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(height);
    this.push(rawName);
    this.pushU8(flags);
    this.pushU32(claimed);
    this.pushU32(renewals);
    this.pushHash(blockHash);

    return this;
  }

  /**
   * Set covenant to REVOKE.
   * @param {Hash} nameHash
   * @param {Number} height
   * @returns {Covenant}
   */

  setRevoke(nameHash, height) {
    this.type = types.REVOKE;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(height);

    return this;
  }

  /**
   * Set covenant to CLAIM.
   * @param {Hash} nameHash
   * @param {Number} height
   * @param {Buffer} rawName
   * @param {Number} flags
   * @param {Hash} commitHash
   * @param {Number} commitHeight
   * @returns {Covenant}
   */

  setClaim(nameHash, height, rawName, flags, commitHash, commitHeight) {
    this.type = types.CLAIM;
    this.items = [];
    this.pushHash(nameHash);
    this.pushU32(height);
    this.push(rawName);
    this.pushU8(flags);
    this.pushHash(commitHash);
    this.pushU32(commitHeight);

    return this;
  }

  /**
   * Test whether the covenant is name-related.
   * @returns {Boolean}
   */

  isName() {
    if (this.type < types.CLAIM)
      return false;

    if (this.type > types.REVOKE)
      return false;

    return true;
  }

  /**
   * Test whether a covenant type should be
   * considered subject to the dust policy rule.
   * @returns {Boolean}
   */

  isDustworthy() {
    switch (this.type) {
      case types.NONE:
      case types.BID:
        return true;
      default:
        return this.type > types.REVOKE;
    }
  }

  /**
   * Test whether a coin should be considered
   * unspendable in the coin selector.
   * @returns {Boolean}
   */

  isNonspendable() {
    switch (this.type) {
      case types.NONE:
      case types.OPEN:
      case types.REDEEM:
        return false;
      default:
        return true;
    }
  }

  /**
   * Test whether a covenant should be considered "linked".
   * @returns {Boolean}
   */

  isLinked() {
    return this.type >= types.REVEAL && this.type <= types.REVOKE;
  }

  /**
   * Convert covenant to an array of buffers.
   * @returns {Buffer[]}
   */

  toArray() {
    return this.items.slice();
  }

  /**
   * Inject properties from an array of buffers.
   * @private
   * @param {Buffer[]} items
   */

  fromArray(items) {
    assert(Array.isArray(items));
    this.items = items;
    return this;
  }

  /**
   * Test whether the covenant is unspendable.
   * @returns {Boolean}
   */

  isUnspendable() {
    return this.type === types.REVOKE;
  }

  /**
   * Convert the covenant to a string.
   * @returns {String}
   */

  toString() {
    return this.encode().toString('hex', 1);
  }

  /**
   * Inject properties from covenant.
   * Used for cloning.
   * @param {this} covenant
   * @returns {this}
   */

  inject(covenant) {
    assert(covenant instanceof this.constructor);
    this.type = covenant.type;
    this.items = covenant.items.slice();
    return this;
  }

  /**
   * Test the covenant against a bloom filter.
   * @param {BloomFilter} filter
   * @returns {Boolean}
   */

  test(filter) {
    for (const item of this.items) {
      if (item.length === 0)
        continue;

      if (filter.test(item))
        return true;
    }

    return false;
  }

  /**
   * Find a data element in a covenant.
   * @param {Buffer} data - Data element to match against.
   * @returns {Number} Index (`-1` if not present).
   */

  indexOf(data) {
    for (let i = 0; i < this.items.length; i++) {
      const item = this.items[i];
      if (item.equals(data))
        return i;
    }
    return -1;
  }

  /**
   * Calculate size of the covenant
   * excluding the varint size bytes.
   * @returns {Number}
   */

  getSize() {
    let size = 0;

    for (const item of this.items)
      size += encoding.sizeVarBytes(item);

    return size;
  }

  /**
   * Calculate size of the covenant
   * including the varint size bytes.
   * @returns {Number}
   */

  getVarSize() {
    return 1 + encoding.sizeVarint(this.items.length) + this.getSize();
  }

  /**
   * Write covenant to a buffer writer.
   * @param {BufioWriter} bw
   * @returns {BufioWriter}
   */

  write(bw) {
    bw.writeU8(this.type);
    bw.writeVarint(this.items.length);

    for (const item of this.items)
      bw.writeVarBytes(item);

    return bw;
  }

  /**
   * Encode covenant.
   * @returns {Buffer}
   */

  encode() {
    const bw = bio.write(this.getVarSize());
    this.write(bw);
    return bw.render();
  }

  /**
   * Convert covenant to a hex string.
   */

  getJSON() {
    const items = [];

    for (const item of this.items)
      items.push(item.toString('hex'));

    return {
      type: this.type,
      action: typesByVal[this.type],
      items
    };
  }

  /**
   * Inject properties from json object.
   * @param {CovenantJSON} json
   * @returns {this}
   */

  fromJSON(json) {
    assert(json && typeof json === 'object', 'Covenant must be an object.');
    assert((json.type & 0xff) === json.type);
    assert(Array.isArray(json.items));

    this.type = json.type;

    for (const str of json.items) {
      const item = util.parseHex(str, -1);
      this.items.push(item);
    }

    return this;
  }

  /**
   * Inject properties from buffer reader.
   * @param {bio.BufferReader} br
   * @returns {this}
   */

  read(br) {
    this.type = br.readU8();

    const count = br.readVarint();

    if (count > consensus.MAX_SCRIPT_STACK)
      throw new Error('Too many covenant items.');

    for (let i = 0; i < count; i++)
      this.items.push(br.readVarBytes());

    return this;
  }

  /**
   * Inject items from string.
   * @param {String|String[]} items
   * @returns {this}
   */

  fromString(items) {
    if (!Array.isArray(items)) {
      assert(typeof items === 'string');

      items = items.trim();

      if (items.length === 0)
        return this;

      items = items.split(/\s+/);
    }

    for (const item of items)
      this.items.push(util.parseHex(item, -1));

    return this;
  }

  /**
   * Inspect a covenant object.
   * @returns {String}
   */

  format() {
    return `<Covenant: ${typesByVal[this.type]}:${this.toString()}>`;
  }

  /**
   * Insantiate covenant from an array of buffers.
   * @param {Buffer[]} items
   * @returns {Covenant}
   */

  static fromArray(items) {
    return new this().fromArray(items);
  }

  /**
   * Test an object to see if it is a covenant.
   * @param {Object} obj
   * @returns {Boolean}
   */

  static isCovenant(obj) {
    return obj instanceof Covenant;
  }
}

Covenant.types = types;

/*
 * Expose
 */

module.exports = Covenant;