Source: services/exchange.js

'use strict';

// Constants
const BTC = require('../currencies/btc');
const BTCA = require('../currencies/btca');
const BTCB = require('../currencies/btcb');

// Dependencies
const merge = require('lodash.merge');

// Fabric Types
const Actor = require('../types/actor');
const Entity = require('../types/entity');
const Collection = require('../types/collection');
const Message = require('../types/message');
const Service = require('../types/service');

/**
 * Implements a basic Exchange.
 */
class Exchange extends Service {
  /**
   * Create an instance of the Exchange.  You may run two instances at
   * once to simulate two-party contracts, or use the Fabric Market to
   * find and trade with real peers.
   * @param {Object} settings Map of settings to values.
   * @param {Object} settings.fees Map of fee settings (all values in BTC).
   * @param {Object} settings.fees.minimum Minimum fee (satoshis).
   * @returns Exchnge
   */
  constructor (settings = {}) {
    super(settings);

    // Configures Defaults
    this.settings = merge({
      anchor: 'BTC', // Symbol of Primary Timestamping Asset (PTA)
      path: './stores/exchange-playnet',
      debug: false,
      orders: [], // Pre-define a list of Orders
      premium: {
        type: 'bips',
        value: 2000 // 2000 bips === 20%
      },
      currencies: [
        BTC,
        BTCA,
        BTCB
      ],
      fees: {
        minimum: 20000 // satoshis
      }
    }, settings, this.settings);

    // TODO: finalize Collection API in #docs-update
    this.orders = new Collection(this.settings.orders);
    this.currencies = new Collection(this.settings.currencies);

    // Internal State
    this._state = {
      actors: {}, // Fabric Actors
      blocks: {}, // Fabric Blocks
      chains: {}, // Fabric Chains
      channels: {}, // Fabric Channels
      oracles: {}, // Fabric Oracles
      pairs: {}, // Portal Pairs
      transactions: {}, // Fabric Transactions
      witnesses: {}, // Fabric Witnesses
      orders: {} // Portal Orders
    };

    // Chainable
    return this;
  }

  async bootstrap () {
    if (!this.settings.debug) return;
    for (let i = 0; i < this.settings.orders.length; i++) {
      const order = this.settings.orders[i];
      order.signature = Buffer.alloc(64);
      const posted = await this._postOrder(order);
      this.emit('message', `Posted Order: ${posted}`);
    }
    return this;
  }

  async start () {
    // Set a heartbeat
    this.heartbeat = setInterval(this._heartbeat.bind(this), this.settings.interval);
    await this.bootstrap();
    this.emit('message', `[FABRIC:EXCHANGE] Started!`);
    this.emit('ready');
  }

  async _heartbeat () {
    await super._heartbeat();
    await this._matchOrders(this._state.orders);
  }

  async _postOrder (order) {
    if (!order) return new Error('Order must be provided.');
    if (!order.signature) return new Error('Order must be signed.');

    const entity = new Entity(order);
    this.emit('message', `Posting order [${entity.id}] ...`);

    const state = await this.orders.create(entity);
    this.emit('message', `Order [${entity.id}] posted: ${state}`);

    if (!this._state.orders[entity.id]) this._state.orders[entity.id] = entity;

    await this.commit();
    this.emit('message', Message.fromVector(['PostedExchangeOrder', state]));

    return state;
  }

  async _matchOrders (orders) {
    const exchange = this;
    const incomplete = Object.values(orders).filter(x => (x.status !== 'completed'));
    const haves = incomplete.filter(x => (x.have === exchange.settings.anchor));
    const wants = incomplete.filter(x => (x.want === exchange.settings.anchor));
    return {
      type: 'ExchangeOrderExecution',
      data: {
        haves,
        wants
      }
    };
  }
}

module.exports = Exchange;