Source: covenants/namestate.js

/*!
 * namestate.js - name states 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 Network = require('../protocol/network');
const Outpoint = require('../primitives/outpoint');
const {ZERO_HASH} = require('../protocol/consensus');
const NameDelta = require('./namedelta');
const {encoding} = bio;

/*
 * Constants
 */

const states = {
  OPENING: 0,
  LOCKED: 1,
  BIDDING: 2,
  REVEAL: 3,
  CLOSED: 4,
  REVOKED: 5
};

const statesByVal = {
  [states.OPENING]: 'OPENING',
  [states.LOCKED]: 'LOCKED',
  [states.BIDDING]: 'BIDDING',
  [states.REVEAL]: 'REVEAL',
  [states.CLOSED]: 'CLOSED',
  [states.REVOKED]: 'REVOKED'
};

const EMPTY = Buffer.alloc(0);

/**
 * NameState
 * @extends {bio.Struct}
 */

class NameState extends bio.Struct {
  constructor() {
    super();

    this.name = EMPTY;
    this.nameHash = ZERO_HASH;
    this.height = 0;
    this.renewal = 0;
    this.owner = new Outpoint();
    this.value = 0;
    this.highest = 0;
    this.data = EMPTY;
    this.transfer = 0;
    this.revoked = 0;
    this.claimed = 0;
    this.renewals = 0;
    this.registered = false;
    this.expired = false;
    this.weak = false;

    // Not serialized.
    this._delta = null;
  }

  get delta() {
    if (!this._delta)
      this._delta = new NameDelta();
    return this._delta;
  }

  set delta(delta) {
    this._delta = delta;
  }

  inject(ns) {
    assert(ns instanceof this.constructor);

    this.name = ns.name;
    this.nameHash = ns.nameHash;
    this.height = ns.height;
    this.renewal = ns.renewal;
    this.owner = ns.owner;
    this.value = ns.value;
    this.highest = ns.highest;
    this.data = ns.data;
    this.transfer = ns.transfer;
    this.revoked = ns.revoked;
    this.claimed = ns.claimed;
    this.renewals = ns.renewals;
    this.registered = ns.registered;
    this.expired = ns.expired;
    this.weak = ns.weak;

    return this;
  }

  clear() {
    this._delta = null;
    return this;
  }

  isNull() {
    return this.height === 0
        && this.renewal === 0
        && this.owner.isNull()
        && this.value === 0
        && this.highest === 0
        && this.data.length === 0
        && this.transfer === 0
        && this.revoked === 0
        && this.claimed === 0
        && this.renewals === 0
        && this.registered === false
        && this.expired === false
        && this.weak === false;
  }

  hasDelta() {
    return this._delta && !this._delta.isNull();
  }

  state(height, network) {
    assert((height >>> 0) === height);
    assert(network && network.names);

    const {
      treeInterval,
      lockupPeriod,
      biddingPeriod,
      revealPeriod
    } = network.names;

    const openPeriod = treeInterval + 1;

    if (this.revoked !== 0)
      return states.REVOKED;

    if (this.claimed !== 0) {
      if (height < this.height + lockupPeriod)
        return states.LOCKED;
      return states.CLOSED;
    }

    if (height < this.height + openPeriod)
      return states.OPENING;

    if (height < this.height + openPeriod + biddingPeriod)
      return states.BIDDING;

    if (height < this.height + openPeriod + biddingPeriod + revealPeriod)
      return states.REVEAL;

    return states.CLOSED;
  }

  isOpening(height, network) {
    return this.state(height, network) === states.OPENING;
  }

  isLocked(height, network) {
    return this.state(height, network) === states.LOCKED;
  }

  isBidding(height, network) {
    return this.state(height, network) === states.BIDDING;
  }

  isReveal(height, network) {
    return this.state(height, network) === states.REVEAL;
  }

  isClosed(height, network) {
    return this.state(height, network) === states.CLOSED;
  }

  isRevoked(height, network) {
    return this.state(height, network) === states.REVOKED;
  }

  isRedeemable(height, network) {
    return this.state(height, network) >= states.CLOSED;
  }

  isClaimable(height, network) {
    assert((height >>> 0) === height);
    assert(network && network.names);

    return this.claimed !== 0
        && !network.names.noReserved
        && height < network.names.claimPeriod;
  }

  isExpired(height, network) {
    assert((height >>> 0) === height);
    assert(network && network.names);

    if (this.revoked !== 0) {
      if (height < this.revoked + network.names.auctionMaturity)
        return false;
      return true;
    }

    // Can only renew once we reach the closed state.
    if (!this.isClosed(height, network))
      return false;

    // Claimed names can only expire once the claim period is over.
    if (this.isClaimable(height, network))
      return false;

    // If we haven't been renewed in a year, start over.
    if (height >= this.renewal + network.names.renewalWindow)
      return true;

    // If nobody revealed their bids, start over.
    if (this.owner.isNull())
      return true;

    return false;
  }

  maybeExpire(height, network) {
    if (this.isExpired(height, network)) {
      const {data} = this;

      // Note: we keep the name data even
      // after expiration (but not revocation).
      this.reset(height);
      this.setExpired(true);
      this.setData(data);

      return true;
    }

    return false;
  }

  reset(height) {
    assert((height >>> 0) === height);

    this.setHeight(height);
    this.setRenewal(height);
    this.setOwner(new Outpoint());
    this.setValue(0);
    this.setHighest(0);
    this.setData(null);
    this.setTransfer(0);
    this.setRevoked(0);
    this.setClaimed(0);
    this.setRenewals(0);
    this.setRegistered(false);
    this.setExpired(false);
    this.setWeak(false);

    return this;
  }

  set(name, height) {
    assert(Buffer.isBuffer(name));

    this.name = name;
    this.reset(height);

    return this;
  }

  setHeight(height) {
    assert((height >>> 0) === height);

    if (height === this.height)
      return this;

    if (this.delta.height === null)
      this.delta.height = this.height;

    this.height = height;

    return this;
  }

  setRenewal(renewal) {
    assert((renewal >>> 0) === renewal);

    if (renewal === this.renewal)
      return this;

    if (this.delta.renewal === null)
      this.delta.renewal = this.renewal;

    this.renewal = renewal;

    return this;
  }

  setOwner(owner) {
    assert(owner instanceof Outpoint);

    if (owner.equals(this.owner))
      return this;

    if (this.delta.owner === null)
      this.delta.owner = this.owner;

    this.owner = owner;

    return this;
  }

  setValue(value) {
    assert(Number.isSafeInteger(value) && value >= 0);

    if (value === this.value)
      return this;

    if (this.delta.value === null)
      this.delta.value = this.value;

    this.value = value;

    return this;
  }

  setHighest(highest) {
    assert(Number.isSafeInteger(highest) && highest >= 0);

    if (highest === this.highest)
      return this;

    if (this.delta.highest === null)
      this.delta.highest = this.highest;

    this.highest = highest;

    return this;
  }

  setData(data) {
    if (data === null)
      data = EMPTY;

    assert(Buffer.isBuffer(data));

    if (this.data.equals(data))
      return this;

    if (this.delta.data === null)
      this.delta.data = this.data;

    this.data = data;

    return this;
  }

  setTransfer(transfer) {
    assert((transfer >>> 0) === transfer);

    if (transfer === this.transfer)
      return this;

    if (this.delta.transfer === null)
      this.delta.transfer = this.transfer;

    this.transfer = transfer;

    return this;
  }

  setRevoked(revoked) {
    assert((revoked >>> 0) === revoked);

    if (revoked === this.revoked)
      return this;

    if (this.delta.revoked === null)
      this.delta.revoked = this.revoked;

    this.revoked = revoked;

    return this;
  }

  setClaimed(claimed) {
    assert((claimed >>> 0) === claimed);

    if (claimed === this.claimed)
      return this;

    if (this.delta.claimed === null)
      this.delta.claimed = this.claimed;

    this.claimed = claimed;

    return this;
  }

  setRenewals(renewals) {
    assert((renewals >>> 0) === renewals);

    if (renewals === this.renewals)
      return this;

    if (this.delta.renewals === null)
      this.delta.renewals = this.renewals;

    this.renewals = renewals;

    return this;
  }

  setRegistered(registered) {
    assert(typeof registered === 'boolean');

    if (registered === this.registered)
      return this;

    if (this.delta.registered === null)
      this.delta.registered = this.registered;

    this.registered = registered;

    return this;
  }

  setExpired(expired) {
    assert(typeof expired === 'boolean');

    if (expired === this.expired)
      return this;

    if (this.delta.expired === null)
      this.delta.expired = this.expired;

    this.expired = expired;

    return this;
  }

  setWeak(weak) {
    assert(typeof weak === 'boolean');

    if (weak === this.weak)
      return this;

    if (this.delta.weak === null)
      this.delta.weak = this.weak;

    this.weak = weak;

    return this;
  }

  applyState(delta) {
    assert(delta instanceof NameDelta);

    if (delta.height !== null)
      this.height = delta.height;

    if (delta.renewal !== null)
      this.renewal = delta.renewal;

    if (delta.owner !== null)
      this.owner = delta.owner;

    if (delta.value !== null)
      this.value = delta.value;

    if (delta.highest !== null)
      this.highest = delta.highest;

    if (delta.data !== null)
      this.data = delta.data;

    if (delta.transfer !== null)
      this.transfer = delta.transfer;

    if (delta.revoked !== null)
      this.revoked = delta.revoked;

    if (delta.claimed !== null)
      this.claimed = delta.claimed;

    if (delta.renewals !== null)
      this.renewals = delta.renewals;

    if (delta.registered !== null)
      this.registered = delta.registered;

    if (delta.expired !== null)
      this.expired = delta.expired;

    if (delta.weak !== null)
      this.weak = delta.weak;

    return this;
  }

  getSize() {
    let size = 0;

    size += 1;
    size += this.name.length;
    size += 2;
    size += this.data.length;

    size += 4;
    size += 4;

    size += 2;

    if (!this.owner.isNull())
      size += 32 + encoding.sizeVarint(this.owner.index);

    if (this.value !== 0)
      size += encoding.sizeVarint(this.value);

    if (this.highest !== 0)
      size += encoding.sizeVarint(this.highest);

    if (this.transfer !== 0)
      size += 4;

    if (this.revoked !== 0)
      size += 4;

    if (this.claimed !== 0)
      size += 4;

    if (this.renewals !== 0)
      size += encoding.sizeVarint(this.renewals);

    return size;
  }

  getField() {
    let field = 0;

    if (!this.owner.isNull())
      field |= 1 << 0;

    if (this.value !== 0)
      field |= 1 << 1;

    if (this.highest !== 0)
      field |= 1 << 2;

    if (this.transfer !== 0)
      field |= 1 << 3;

    if (this.revoked !== 0)
      field |= 1 << 4;

    if (this.claimed !== 0)
      field |= 1 << 5;

    if (this.renewals !== 0)
      field |= 1 << 6;

    if (this.registered)
      field |= 1 << 7;

    if (this.expired)
      field |= 1 << 8;

    if (this.weak)
      field |= 1 << 9;

    return field;
  }

  write(bw) {
    bw.writeU8(this.name.length);
    bw.writeBytes(this.name);
    bw.writeU16(this.data.length);
    bw.writeBytes(this.data);

    bw.writeU32(this.height);
    bw.writeU32(this.renewal);

    bw.writeU16(this.getField());

    if (!this.owner.isNull()) {
      bw.writeHash(this.owner.hash);
      bw.writeVarint(this.owner.index);
    }

    if (this.value !== 0)
      bw.writeVarint(this.value);

    if (this.highest !== 0)
      bw.writeVarint(this.highest);

    if (this.transfer !== 0)
      bw.writeU32(this.transfer);

    if (this.revoked !== 0)
      bw.writeU32(this.revoked);

    if (this.claimed !== 0)
      bw.writeU32(this.claimed);

    if (this.renewals !== 0)
      bw.writeVarint(this.renewals);

    return bw;
  }

  read(br) {
    this.name = br.readBytes(br.readU8());
    this.data = br.readBytes(br.readU16());
    this.height = br.readU32();
    this.renewal = br.readU32();

    const field = br.readU16();

    if (field & (1 << 0)) {
      this.owner.hash = br.readHash();
      this.owner.index = br.readVarint();
    }

    if (field & (1 << 1))
      this.value = br.readVarint();

    if (field & (1 << 2))
      this.highest = br.readVarint();

    if (field & (1 << 3))
      this.transfer = br.readU32();

    if (field & (1 << 4))
      this.revoked = br.readU32();

    if (field & (1 << 5))
      this.claimed = br.readU32();

    if (field & (1 << 6))
      this.renewals = br.readVarint();

    if (field & (1 << 7))
      this.registered = true;

    if (field & (1 << 8))
      this.expired = true;

    if (field & (1 << 9))
      this.weak = true;

    return this;
  }

  getJSON(height, network) {
    let state = undefined;
    let stats = undefined;

    if (height != null) {
      network = Network.get(network);
      state = this.state(height, network);
      state = statesByVal[state];
      stats = this.toStats(height, network);
    }

    return {
      name: this.name.toString('binary'),
      nameHash: this.nameHash.toString('hex'),
      state: state,
      height: this.height,
      renewal: this.renewal,
      owner: this.owner.toJSON(),
      value: this.value,
      highest: this.highest,
      data: this.data.toString('hex'),
      transfer: this.transfer,
      revoked: this.revoked,
      claimed: this.claimed,
      renewals: this.renewals,
      registered: this.registered,
      expired: this.expired,
      weak: this.weak,
      stats: stats
    };
  }

  fromJSON(json) {
    assert(json && typeof json === 'object');
    assert(typeof json.name === 'string');
    assert(json.name.length >= 0 && json.name.length <= 63);
    assert(typeof json.nameHash === 'string');
    assert(json.nameHash.length === 64);
    assert((json.height >>> 0) === json.height);
    assert((json.renewal >>> 0) === json.renewal);
    assert(json.owner && typeof json.owner === 'object');
    assert(Number.isSafeInteger(json.value) && json.value >= 0);
    assert(Number.isSafeInteger(json.highest) && json.highest >= 0);
    assert(typeof json.data === 'string');
    assert((json.data.length & 1) === 0);
    assert((json.transfer >>> 0) === json.transfer);
    assert((json.revoked >>> 0) === json.revoked);
    assert((json.claimed >>> 0) === json.claimed);
    assert((json.renewals >>> 0) === json.renewals);
    assert(typeof json.registered === 'boolean');
    assert(typeof json.expired === 'boolean');
    assert(typeof json.weak === 'boolean');

    this.name = Buffer.from(json.name, 'binary');
    this.nameHash = Buffer.from(json.nameHash, 'hex');
    this.height = json.height;
    this.renewal = json.renewal;
    this.owner = Outpoint.fromJSON(json.owner);
    this.value = json.value;
    this.highest = json.highest;
    this.data = Buffer.from(json.data, 'hex');
    this.transfer = json.transfer;
    this.revoked = json.revoked;
    this.claimed = json.claimed;
    this.renewals = json.renewals;
    this.registered = json.registered;
    this.expired = json.expired;
    this.weak = json.weak;

    return this;
  }

  toStats(height, network) {
    assert((height >>> 0) === height);
    assert(network && network.names);

    const spacing = network.pow.targetSpacing;

    const {
      treeInterval,
      lockupPeriod,
      biddingPeriod,
      revealPeriod,
      renewalWindow,
      auctionMaturity,
      transferLockup
    } = network.names;

    const openPeriod = treeInterval + 1;

    const stats = {};

    if (this.isOpening(height, network)) {
      const start = this.height;
      const end = this.height + openPeriod;
      const blocks = end - height;
      const hours = ((blocks * spacing) / 60 / 60);

      stats.openPeriodStart = start;
      stats.openPeriodEnd = end;

      stats.blocksUntilBidding = blocks;
      stats.hoursUntilBidding = Number(hours.toFixed(2));
    }

    if (this.isLocked(height, network)) {
      const start = this.height;
      const end = this.height + lockupPeriod;
      const blocks = end - height;
      const hours = ((blocks * spacing) / 60 / 60);

      stats.lockupPeriodStart = start;
      stats.lockupPeriodEnd = end;

      stats.blocksUntilClosed = blocks;
      stats.hoursUntilClosed = Number(hours.toFixed(2));
    }

    if (this.isBidding(height, network)) {
      const start = this.height + openPeriod;
      const end = start + biddingPeriod;
      const blocks = end - height;
      const hours = ((blocks * spacing) / 60 / 60);

      stats.bidPeriodStart = start;
      stats.bidPeriodEnd = end;

      stats.blocksUntilReveal = blocks;
      stats.hoursUntilReveal = Number(hours.toFixed(2));
    }

    if (this.isReveal(height, network)) {
      const start = this.height + openPeriod + biddingPeriod;
      const end = start + revealPeriod;
      const blocks = end - height;
      const hours = ((blocks * spacing) / 60 / 60);

      stats.revealPeriodStart = start;
      stats.revealPeriodEnd = end;

      stats.blocksUntilClose = blocks;
      stats.hoursUntilClose = Number(hours.toFixed(2));
    }

    if (this.isClosed(height, network)) {
      const start = this.renewal;
      const end = start + renewalWindow;
      const blocks = end - height;
      const days = ((blocks * spacing) / 60 / 60 / 24);

      stats.renewalPeriodStart = start;
      stats.renewalPeriodEnd = end;

      stats.blocksUntilExpire = blocks;
      stats.daysUntilExpire = Number(days.toFixed(2));
    }

    if (this.isRevoked(height, network)) {
      const start = this.revoked;
      const end = start + auctionMaturity;
      const blocks = end - height;
      const hours = ((blocks * spacing) / 60 / 60);

      stats.revokePeriodStart = start;
      stats.revokePeriodEnd = end;

      stats.blocksUntilReopen = blocks;
      stats.hoursUntilReopen = Number(hours.toFixed(2));
    }

    // Add these details if name is in mid-transfer
    if (this.transfer !== 0) {
      const start = this.transfer;
      const end = start + transferLockup;
      const blocks = end - height;
      const hours = ((blocks * spacing) / 60 / 60);

      stats.transferLockupStart = start;
      stats.transferLockupEnd = end;

      stats.blocksUntilValidFinalize = blocks;
      stats.hoursUntilValidFinalize = Number(hours.toFixed(2));
    }

    return stats;
  }

  format(height, network) {
    return this.getJSON(height, network);
  }
}

/*
 * Static
 */

NameState.states = states;
NameState.statesByVal = statesByVal;

// Max size: 668
NameState.MAX_SIZE = (0
  + 1 + 63
  + 2 + 512
  + 4
  + 4
  + 2
  + 32
  + 9
  + 9
  + 9
  + 4
  + 4
  + 4
  + 9);

/*
 * Expose
 */

module.exports = NameState;