@sangaman/xud
Version:
Exchange Union Daemon
350 lines • 15.8 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0)
t[p[i]] = s[p[i]];
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const v1_1 = __importDefault(require("uuid/v1"));
const events_1 = require("events");
const OrderBookRepository_1 = __importDefault(require("./OrderBookRepository"));
const MatchingEngine_1 = __importDefault(require("./MatchingEngine"));
const MatchesProcessor_1 = __importDefault(require("./MatchesProcessor"));
const errors_1 = __importDefault(require("./errors"));
const types_1 = require("../types");
const utils_1 = require("../utils/utils");
/** A class representing an orderbook containing all orders for all active trading pairs. */
class OrderBook extends events_1.EventEmitter {
constructor(logger, models, pool, lndClient, raidenClient) {
super();
this.logger = logger;
this.pool = pool;
this.lndClient = lndClient;
this.raidenClient = raidenClient;
/** An array of supported pair instances for the orderbook. */
this.pairs = [];
/** An array of supported pair ids for the orderbook. */
this.pairIds = [];
/** A map between active trading pair ids and matching engines. */
this.matchingEngines = new Map();
/** A map between active trading pair ids and local buy and sell orders. */
this.ownOrders = new Map();
/** A map between active trading pair ids and peer buy and sell orders. */
this.peerOrders = new Map();
/** A map between an order's local id and its global id. */
this.localIdMap = new Map();
this.swapHandler = (order) => {
if (order.quantity === 0) {
// full order execution
if (types_1.orders.isPeerOrder(order)) {
this.removeOrder(this.peerOrders, order.id, order.pairId);
}
else {
this.removeOwnOrder(order.pairId, order.id);
}
}
else {
// TODO: partial order execution, update existing order
}
};
this.init = () => __awaiter(this, void 0, void 0, function* () {
this.pairs = yield this.repository.getPairs();
this.pairs.forEach((pair) => {
this.pairIds.push(pair.id);
this.matchingEngines.set(pair.id, new MatchingEngine_1.default(this.logger, pair.id));
this.ownOrders.set(pair.id, this.initOrders());
this.peerOrders.set(pair.id, this.initOrders());
});
});
this.initOrders = () => {
return {
buyOrders: new Map(),
sellOrders: new Map(),
};
};
/**
* Get lists of buy and sell orders of peers.
*/
this.getPeerOrders = (pairId, maxResults) => {
return this.getOrders(pairId, maxResults, this.peerOrders);
};
/*
* Get lists of this node's own buy and sell orders.
*/
this.getOwnOrders = (pairId, maxResults) => {
return this.getOrders(pairId, maxResults, this.ownOrders);
};
this.getOrders = (pairId, maxResults, ordersMap) => {
const orders = ordersMap.get(pairId);
if (!orders) {
throw errors_1.default.INVALID_PAIR_ID(pairId);
}
if (maxResults > 0) {
return {
buyOrders: Array.from(orders.buyOrders.values()).slice(0, maxResults),
sellOrders: Array.from(orders.sellOrders.values()).slice(0, maxResults),
};
}
else {
return {
buyOrders: Array.from(orders.buyOrders.values()),
sellOrders: Array.from(orders.sellOrders.values()),
};
}
};
this.addLimitOrder = (order) => {
return this.addOwnOrder(order);
};
this.addMarketOrder = (order) => {
const price = order.quantity > 0 ? Number.MAX_VALUE : 0;
return this.addOwnOrder(Object.assign({}, order, { price }), true);
};
this.removeOwnOrderByLocalId = (pairId, localId) => {
const id = this.localIdMap.get(localId);
if (id === undefined) {
return { removed: false, globalId: id };
}
else {
this.localIdMap.delete(localId);
return {
removed: this.removeOwnOrder(pairId, id),
globalId: id,
};
}
};
this.removeOwnOrder = (pairId, orderId) => {
const matchingEngine = this.matchingEngines.get(pairId);
if (!matchingEngine) {
this.logger.warn(`Invalid pairId: ${pairId}`);
return false;
}
if (matchingEngine.removeOwnOrder(orderId)) {
this.logger.debug(`order removed: ${JSON.stringify(orderId)}`);
return this.removeOrder(this.ownOrders, orderId, pairId);
}
else {
return false;
}
};
this.removePeerOrder = (orderId, pairId, quantityToDecrease) => {
const matchingEngine = this.matchingEngines.get(pairId);
const ordersMap = this.peerOrders.get(pairId);
if (!matchingEngine || !ordersMap) {
this.logger.warn(`Invalid pairId: ${pairId}`);
return false;
}
const order = matchingEngine.removePeerOrder(orderId, quantityToDecrease);
if (order) {
let result;
if (!quantityToDecrease || quantityToDecrease === 0) {
result = this.removeOrder(this.peerOrders, orderId, pairId);
}
else {
result = this.updateOrderQuantity(order, quantityToDecrease);
}
if (result) {
this.emit('peerOrder.invalidation', { orderId, pairId, quantity: quantityToDecrease });
return true;
}
}
this.logger.warn(`Invalid orderId: ${orderId}`);
return false;
};
this.addOwnOrder = (order, discardRemaining = false) => {
if (this.localIdMap.has(order.localId)) {
throw errors_1.default.DUPLICATE_ORDER(order.localId);
}
const matchingEngine = this.matchingEngines.get(order.pairId);
if (!matchingEngine) {
throw errors_1.default.INVALID_PAIR_ID(order.pairId);
}
const stampedOrder = Object.assign({}, order, { id: v1_1.default(), createdAt: utils_1.ms() });
const matchingResult = matchingEngine.matchOrAddOwnOrder(stampedOrder, discardRemaining);
const { matches, remainingOrder } = matchingResult;
if (matches.length > 0) {
matches.forEach(({ maker, taker }) => {
this.handleMatch({ maker, taker });
this.updateOrderQuantity(maker, maker.quantity);
});
}
if (remainingOrder && !discardRemaining) {
this.broadcastOrder(remainingOrder);
this.addOrder(this.ownOrders, remainingOrder);
this.logger.debug(`order added: ${JSON.stringify(remainingOrder)}`);
}
return matchingResult;
};
/**
* Add peer order
* @returns false if it's a duplicated order or with an invalid pair id, otherwise true
*/
this.addPeerOrder = (order) => {
const matchingEngine = this.matchingEngines.get(order.pairId);
if (!matchingEngine) {
this.logger.debug(`incoming peer order invalid pairId: ${order.pairId}`);
// TODO: penalize peer
return false;
}
const stampedOrder = Object.assign({}, order, { createdAt: utils_1.ms() });
if (!this.addOrder(this.peerOrders, stampedOrder)) {
this.logger.debug(`incoming peer order is duplicated: ${order.id}`);
// TODO: penalize peer
return false;
}
matchingEngine.addPeerOrder(stampedOrder);
this.logger.debug(`order added: ${JSON.stringify(stampedOrder)}`);
this.emit('peerOrder.incoming', stampedOrder);
return true;
};
this.removePeerOrders = (peer) => __awaiter(this, void 0, void 0, function* () {
this.matchingEngines.forEach((matchingEngine) => {
const orders = matchingEngine.removePeerOrders(peer.nodePubKey);
orders.forEach((order) => {
this.removeOrder(this.peerOrders, order.id, order.pairId);
this.emit('peerOrder.invalidation', {
orderId: order.id,
pairId: order.pairId,
});
});
});
});
this.updateOrderQuantity = (order, quantityToDecrease) => {
const isOwnOrder = types_1.orders.isOwnOrder(order);
const orderMap = this.getOrderMap(isOwnOrder ? this.ownOrders : this.peerOrders, order);
const orderToUpdate = orderMap.get(order.id);
if (!orderToUpdate) {
return false;
}
orderToUpdate.quantity -= quantityToDecrease;
if (orderToUpdate.quantity === 0) {
if (isOwnOrder) {
const { localId } = order;
this.localIdMap.delete(localId);
}
orderMap.delete(order.id);
}
return true;
};
/**
* Add an order to an order map
* @returns false if an order with the same id already exist, otherwise true
*/
this.addOrder = (ordersMap, order) => {
if (this.isOwnOrdersMap(ordersMap)) {
const { localId } = order;
this.localIdMap.set(localId, order.id);
}
const orderMap = this.getOrderMap(ordersMap, order);
if (orderMap.has(order.id)) {
return false;
}
else {
orderMap.set(order.id, order);
return true;
}
};
this.removeOrder = (ordersMap, orderId, pairId) => {
const orders = ordersMap.get(pairId);
if (!orders) {
throw errors_1.default.INVALID_PAIR_ID(pairId);
}
if (orders.buyOrders.has(orderId)) {
orders.buyOrders.delete(orderId);
return true;
}
else if (orders.sellOrders.has(orderId)) {
orders.sellOrders.delete(orderId);
return true;
}
return false;
};
this.getOrderMap = (ordersMap, order) => {
const orders = ordersMap.get(order.pairId);
if (!orders) {
throw errors_1.default.INVALID_PAIR_ID(order.pairId);
}
if (order.quantity > 0) {
return orders.buyOrders;
}
else {
return orders.sellOrders;
}
};
this.isOwnOrdersMap = (ordersMap) => {
return ordersMap === this.ownOrders;
};
/**
* Send all local orders to a given peer in an [[OrdersPacket].
* @param reqId the request id of a [[GetOrdersPacket]] packet that this method is responding to
*/
this.sendOrders = (peer, reqId) => __awaiter(this, void 0, void 0, function* () {
// TODO: just send supported pairs
const outgoingOrders = [];
this.pairIds.forEach((pairId) => {
const orders = this.getOwnOrders(pairId, 0);
orders['buyOrders'].forEach(order => outgoingOrders.push(this.createOutgoingOrder(order)));
orders['sellOrders'].forEach(order => outgoingOrders.push(this.createOutgoingOrder(order)));
});
peer.sendOrders(outgoingOrders, reqId);
});
/**
* Create an outgoing order and broadcast it to all peers.
*/
this.broadcastOrder = (order) => {
if (this.pool) {
const outgoingOrder = this.createOutgoingOrder(order);
if (outgoingOrder) {
this.pool.broadcastOrder(outgoingOrder);
}
}
};
this.createOutgoingOrder = (order) => {
// TODO: Remove functionality of attaching invoices to orders per new swap approach.
const invoice = 'dummyInvoice'; // temporarily testing invoices while lnd is not available
const _a = Object.assign({}, order, { invoice }), { createdAt, localId } = _a, outgoingOrder = __rest(_a, ["createdAt", "localId"]);
return outgoingOrder;
};
this.handleMatch = (match) => {
this.logger.debug(`order match: ${JSON.stringify(match)}`);
if (this.pool) {
const { maker } = match;
if (types_1.orders.isOwnOrder(maker)) {
this.pool.broadcastOrderInvalidation({
orderId: maker.id,
pairId: maker.pairId,
quantity: maker.quantity,
});
}
}
this.matchesProcessor.add(match);
};
this.matchesProcessor = new MatchesProcessor_1.default(logger, pool, raidenClient);
this.repository = new OrderBookRepository_1.default(logger, models);
if (pool) {
pool.on('packet.order', this.addPeerOrder);
pool.on('packet.orderInvalidation', order => this.removePeerOrder(order.orderId, order.pairId, order.quantity));
pool.on('packet.getOrders', this.sendOrders);
pool.on('peer.close', this.removePeerOrders);
}
if (raidenClient) {
raidenClient.on('swap', this.swapHandler);
}
}
}
exports.default = OrderBook;
//# sourceMappingURL=OrderBook.js.map