xud
Version:
Exchange Union Daemon
854 lines • 62.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
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) : adopt(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 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
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 assert_1 = __importDefault(require("assert"));
const events_1 = require("events");
const UnitConverter_1 = require("../utils/UnitConverter");
const v1_1 = __importDefault(require("uuid/v1"));
const enums_1 = require("../constants/enums");
const packets_1 = require("../p2p/packets");
const Swaps_1 = __importDefault(require("../swaps/Swaps"));
const aliasUtils_1 = require("../utils/aliasUtils");
const utils_1 = require("../utils/utils");
const errors_1 = __importStar(require("./errors"));
const OrderBookRepository_1 = __importDefault(require("./OrderBookRepository"));
const TradingPair_1 = __importDefault(require("./TradingPair"));
const types_1 = require("./types");
/**
* Represents an order book containing all orders for all active trading pairs. This encompasses
* all orders tracked locally and is the primary interface with which other modules interact with
* the order book.
*/
let OrderBook = /** @class */ (() => {
class OrderBook extends events_1.EventEmitter {
constructor({ logger, models, thresholds, pool, swaps, nosanityswaps, nobalancechecks, nomatching = false, strict = true }) {
super();
/** A map between active trading pair ids and trading pair instances. */
this.tradingPairs = new Map();
/** A map between own orders local id and their global id. */
this.localIdMap = new Map();
/** A map of supported currency tickers to currency instances. */
this.currencyInstances = new Map();
/** A map of supported trading pair tickers and pair database instances. */
this.pairInstances = new Map();
this.checkThresholdCompliance = (order) => {
const { minQuantity } = this.thresholds;
return order.quantity >= minQuantity;
};
/**
* Checks that a currency advertised by a peer is known to us, has a swap client identifier,
* and that their token identifier matches ours.
*/
this.isPeerCurrencySupported = (peer, currency) => {
const currencyInstance = this.currencyInstances.get(currency);
if (!currencyInstance) {
return false; // we don't know about this currency
}
if (!peer.getIdentifier(currencyInstance.swapClient, currency)) {
return false; // peer did not provide a swap client identifier for this currency
}
// ensure that our token identifiers match
const ourTokenIdentifier = this.pool.getTokenIdentifier(currency);
const peerTokenIdentifier = peer.getTokenIdentifier(currency);
return ourTokenIdentifier === peerTokenIdentifier;
};
this.bindPool = () => {
this.pool.on('packet.order', this.addPeerOrder);
this.pool.on('packet.orderInvalidation', this.handleOrderInvalidation);
this.pool.on('packet.getOrders', this.sendOrders);
this.pool.on('packet.swapRequest', this.handleSwapRequest);
this.pool.on('peer.close', this.removePeerOrders);
this.pool.on('peer.pairDropped', this.removePeerPair);
this.pool.on('peer.verifyPairs', this.verifyPeerPairs);
this.pool.on('peer.nodeStateUpdate', this.checkPeerCurrencies);
};
this.bindSwaps = () => {
this.swaps.on('swap.recovered', (recoveredSwap) => __awaiter(this, void 0, void 0, function* () {
// when a swap is recovered, we want to record it in the database
// as a trade since funds did eventually get swapped
yield this.persistTrade({
quantity: recoveredSwap.quantity,
makerOrderId: recoveredSwap.orderId,
rHash: recoveredSwap.rHash,
});
}));
this.swaps.on('swap.paid', (swapSuccess) => __awaiter(this, void 0, void 0, function* () {
if (swapSuccess.role === enums_1.SwapRole.Maker) {
const { orderId, pairId, quantity, peerPubKey } = swapSuccess;
// we must remove the amount that was put on hold while the swap was pending for the remaining order
this.removeOrderHold(orderId, pairId, quantity);
const ownOrder = this.removeOwnOrder({
orderId,
pairId,
takerPubKey: peerPubKey,
quantityToRemove: quantity,
});
this.emit('ownOrder.swapped', { pairId, quantity, id: orderId });
yield this.persistTrade({
quantity: swapSuccess.quantity,
makerOrder: ownOrder,
rHash: swapSuccess.rHash,
});
}
}));
this.swaps.on('swap.failed', (deal) => {
if (deal.role === enums_1.SwapRole.Maker && (deal.phase === enums_1.SwapPhase.SwapAccepted || deal.phase === enums_1.SwapPhase.SendingPayment)) {
// if our order is the maker and the swap failed after it was agreed to but before it was executed
// we must release the hold on the order that we set when we agreed to the deal
this.removeOrderHold(deal.orderId, deal.pairId, deal.quantity);
}
});
};
/** Loads the supported pairs and currencies from the database. */
this.init = () => __awaiter(this, void 0, void 0, function* () {
const [pairs, currencies] = yield Promise.all([this.repository.getPairs(), this.repository.getCurrencies()]);
currencies.forEach(currency => this.currencyInstances.set(currency.id, currency));
pairs.forEach((pair) => {
this.pairInstances.set(pair.id, pair);
this.addTradingPair(pair.id);
});
this.pool.updatePairs(this.pairIds);
});
/**
* Gets all trades or a limited number of trades from the database.
*/
this.getTrades = (limit) => __awaiter(this, void 0, void 0, function* () {
const response = yield this.repository.getTrades(limit);
return response;
});
/**
* Get lists of buy and sell orders of peers.
*/
this.getPeersOrders = (pairId) => {
const tp = this.getTradingPair(pairId);
return tp.getPeersOrders();
};
/**
* Get lists of this node's own buy and sell orders.
*/
this.getOwnOrders = (pairId) => {
const tp = this.getTradingPair(pairId);
return tp.getOwnOrders();
};
/** Get the trading pair instance for a given pairId, or throw an error if none exists. */
this.getTradingPair = (pairId) => {
const tp = this.tradingPairs.get(pairId);
if (!tp) {
throw errors_1.default.PAIR_DOES_NOT_EXIST(pairId);
}
return tp;
};
/**
* Gets an own order by order id and pair id.
* @returns The order matching parameters, or undefined if no order could be found.
*/
this.getOwnOrder = (orderId, pairId) => {
const tp = this.getTradingPair(pairId);
return tp.getOwnOrder(orderId);
};
this.tryGetOwnOrder = (orderId, pairId) => {
try {
return this.getOwnOrder(orderId, pairId);
}
catch (err) {
return;
}
};
this.getPeerOrder = (orderId, pairId, peerPubKey) => {
const tp = this.getTradingPair(pairId);
return tp.getPeerOrder(orderId, peerPubKey);
};
this.addPair = (pair) => __awaiter(this, void 0, void 0, function* () {
const pairId = utils_1.derivePairId(pair);
if (pair.baseCurrency.toLowerCase() === pair.quoteCurrency.toLowerCase()) {
throw errors_1.default.DUPLICATE_PAIR_CURRENCIES(pair.baseCurrency, pair.quoteCurrency);
}
if (this.pairInstances.has(pairId)) {
throw errors_1.default.PAIR_ALREADY_EXISTS(pairId);
}
if (!this.currencyInstances.has(pair.baseCurrency)) {
throw errors_1.default.CURRENCY_DOES_NOT_EXIST(pair.baseCurrency);
}
if (!this.currencyInstances.has(pair.quoteCurrency)) {
throw errors_1.default.CURRENCY_DOES_NOT_EXIST(pair.quoteCurrency);
}
const pairInstance = yield this.repository.addPair(pair);
this.pairInstances.set(pairInstance.id, pairInstance);
this.addTradingPair(pairInstance.id);
this.pool.rawPeers().forEach((peer) => __awaiter(this, void 0, void 0, function* () {
this.checkPeerCurrencies(peer);
yield this.verifyPeerPairs(peer);
}));
this.pool.updatePairs(this.pairIds);
return pairInstance;
});
this.addTradingPair = (pairId) => {
const tp = new TradingPair_1.default(this.logger, pairId, this.nomatching);
this.tradingPairs.set(pairId, tp);
tp.on('peerOrder.dust', (order) => {
this.removePeerOrder(order.id, order.pairId);
});
tp.on('ownOrder.dust', (order) => {
this.removeOwnOrder({
orderId: order.id,
pairId: order.pairId,
quantityToRemove: order.quantity,
});
});
};
this.addCurrency = (currency) => __awaiter(this, void 0, void 0, function* () {
if (this.currencyInstances.has(currency.id)) {
throw errors_1.default.CURRENCY_ALREADY_EXISTS(currency.id);
}
if (currency.swapClient === enums_1.SwapClientType.Connext && !currency.tokenAddress) {
throw errors_1.default.CURRENCY_MISSING_ETHEREUM_CONTRACT_ADDRESS(currency.id);
}
const currencyInstance = yield this.repository.addCurrency(Object.assign(Object.assign({}, currency), { decimalPlaces: currency.decimalPlaces || 8 }));
this.currencyInstances.set(currencyInstance.id, currencyInstance);
yield this.swaps.swapClientManager.add(currencyInstance);
});
this.removeCurrency = (currencyId) => __awaiter(this, void 0, void 0, function* () {
const currency = this.currencyInstances.get(currencyId);
if (currency) {
for (const pair of this.pairInstances.values()) {
if (currencyId === pair.baseCurrency || currencyId === pair.quoteCurrency) {
throw errors_1.default.CURRENCY_CANNOT_BE_REMOVED(currencyId, pair.id);
}
}
this.currencyInstances.delete(currencyId);
yield currency.destroy();
}
else {
throw errors_1.default.CURRENCY_DOES_NOT_EXIST(currencyId);
}
});
this.removePair = (pairId) => {
const pair = this.pairInstances.get(pairId);
if (!pair) {
throw errors_1.default.PAIR_DOES_NOT_EXIST(pairId);
}
this.pairInstances.delete(pairId);
this.tradingPairs.delete(pairId);
this.pool.rawPeers().forEach((peer) => __awaiter(this, void 0, void 0, function* () {
this.checkPeerCurrencies(peer);
yield this.verifyPeerPairs(peer);
}));
this.pool.updatePairs(this.pairIds);
return pair.destroy();
};
this.placeLimitOrder = ({ order, immediateOrCancel = false, replaceOrderId, onUpdate }) => __awaiter(this, void 0, void 0, function* () {
const stampedOrder = this.stampOwnOrder(order, replaceOrderId);
if (order.quantity < TradingPair_1.default.QUANTITY_DUST_LIMIT) {
const baseCurrency = order.pairId.split('/')[0];
throw errors_1.default.MIN_QUANTITY_VIOLATED(TradingPair_1.default.QUANTITY_DUST_LIMIT, baseCurrency);
}
if (order.quantity * order.price < TradingPair_1.default.QUANTITY_DUST_LIMIT) {
const quoteCurrency = order.pairId.split('/')[1];
throw errors_1.default.MIN_QUANTITY_VIOLATED(TradingPair_1.default.QUANTITY_DUST_LIMIT, quoteCurrency);
}
if (this.nomatching) {
this.addOwnOrder(stampedOrder);
onUpdate && onUpdate({ type: types_1.PlaceOrderEventType.RemainingOrder, order: stampedOrder });
return {
internalMatches: [],
swapSuccesses: [],
swapFailures: [],
remainingOrder: stampedOrder,
};
}
return this.placeOrder({
onUpdate,
replaceOrderId,
order: stampedOrder,
discardRemaining: immediateOrCancel,
maxTime: Date.now() + OrderBook.MAX_PLACEORDER_ITERATIONS_TIME,
});
});
this.placeMarketOrder = ({ order, onUpdate }) => __awaiter(this, void 0, void 0, function* () {
if (this.nomatching) {
throw errors_1.default.MARKET_ORDERS_NOT_ALLOWED();
}
if (order.quantity < TradingPair_1.default.QUANTITY_DUST_LIMIT) {
const baseCurrency = order.pairId.split('/')[0];
throw errors_1.default.MIN_QUANTITY_VIOLATED(TradingPair_1.default.QUANTITY_DUST_LIMIT, baseCurrency);
}
const stampedOrder = this.stampOwnOrder(Object.assign(Object.assign({}, order), { price: order.isBuy ? Number.POSITIVE_INFINITY : 0 }));
const addResult = yield this.placeOrder({
onUpdate,
order: stampedOrder,
discardRemaining: true,
maxTime: Date.now() + OrderBook.MAX_PLACEORDER_ITERATIONS_TIME,
});
delete addResult.remainingOrder;
return addResult;
});
/**
* Places an order in the order book. This method first attempts to match the order with existing
* orders by price and initiate swaps for any matches with peer orders. It can be called recursively
* for any portions of the order that fail swaps.
* @param order the order to place
* @param discardRemaining whether to discard any unmatched portion of the order, if `false` the
* unmatched portion will enter the order book.
* @param onUpdate a callback for when there are updates to the matching and order placement
* routine including internal matches, successful swaps, failed swaps, and remaining orders
* @param maxTime the deadline in epoch milliseconds for this method to end recursive calls
*/
this.placeOrder = ({ order, discardRemaining = false, retry = false, onUpdate, maxTime, replaceOrderId, }) => __awaiter(this, void 0, void 0, function* () {
// Check if order complies to thresholds
if (this.thresholds.minQuantity > 0) {
if (!this.checkThresholdCompliance(order)) {
throw errors_1.default.MIN_QUANTITY_VIOLATED(this.thresholds.minQuantity, '');
}
}
// this method can be called recursively on swap failures retries.
// if max time exceeded, don't try to match
if (maxTime && Date.now() > maxTime) {
assert_1.default(retry, 'we may only timeout placeOrder on retries');
this.logger.debug(`placeOrder max time exceeded. order (${JSON.stringify(order)}) won't be fully matched`);
// returning the remaining order to be rolled back and handled by the initial call
return {
internalMatches: [],
swapSuccesses: [],
swapFailures: [],
remainingOrder: discardRemaining ? undefined : order,
};
}
const tp = this.getTradingPair(order.pairId);
let replacedOrderIdentifier;
if (replaceOrderId) {
assert_1.default(!discardRemaining, 'can not replace order and discard remaining order');
// put the order we are replacing on hold while we place the new order
replacedOrderIdentifier = this.localIdMap.get(replaceOrderId);
if (!replacedOrderIdentifier) {
throw errors_1.default.ORDER_NOT_FOUND(replaceOrderId);
}
assert_1.default(replacedOrderIdentifier.pairId === order.pairId);
}
if (!this.nobalancechecks) {
// for limit orders, we use the price of our order to calculate inbound/outbound amounts
// for market orders, we use the price of the best matching order in the order book
const price = (order.price === 0 || order.price === Number.POSITIVE_INFINITY) ?
(order.isBuy ? tp.quoteAsk() : tp.quoteBid()) :
order.price;
const quantityBeingReplaced = replacedOrderIdentifier ?
this.getOwnOrder(replacedOrderIdentifier.id, replacedOrderIdentifier.pairId).quantity :
0;
/** The quantity that's being added to the replaced order. */
const quantityDelta = order.quantity - quantityBeingReplaced;
if (quantityDelta > 0) {
yield this.swaps.swapClientManager.checkSwapCapacities(Object.assign(Object.assign({}, order), { price, quantity: quantityDelta }));
}
}
if (replacedOrderIdentifier) {
this.addOrderHold(replacedOrderIdentifier.id, replacedOrderIdentifier.pairId);
}
// perform matching routine. maker orders that are matched will be removed from the order book.
const matchingResult = tp.match(order);
/** Any portion of the placed order that could not be swapped or matched internally. */
let { remainingOrder } = matchingResult;
/** Local orders that matched with the placed order. */
const internalMatches = [];
/** Successful swaps performed for the placed order. */
const swapSuccesses = [];
/** Failed swaps attempted for the placed order. */
const swapFailures = [];
/** Maker orders that we attempted to swap with but failed. */
const failedMakerOrders = [];
/** Maker orders that were invalidated while we were attempting swaps. */
const invalidatedMakerOrderIds = new Set();
// we add a handler here to track orders that were invalidated while we were trying to swap
// them so that we don't accidentally add them back to the order book after they fail a swap
const handlePeerOrderInvalidation = (invalidatedOrder) => {
if (invalidatedOrder.pairId === order.pairId) {
invalidatedMakerOrderIds.add(invalidatedOrder.id);
}
};
this.pool.on('packet.orderInvalidation', handlePeerOrderInvalidation);
/**
* The routine for retrying a portion of the order that failed a swap attempt.
* @param failedSwapQuantity the quantity of the failed portion to retry
*/
const retryFailedSwap = (failedSwapQuantity) => __awaiter(this, void 0, void 0, function* () {
this.logger.debug(`repeating matching routine for ${order.id} for failed quantity of ${failedSwapQuantity}`);
const orderToRetry = Object.assign(Object.assign({}, order), { quantity: failedSwapQuantity });
// invoke placeOrder recursively, append matches/swaps and any remaining order
const retryResult = yield this.placeOrder({ discardRemaining, onUpdate, maxTime, order: orderToRetry, retry: true });
internalMatches.push(...retryResult.internalMatches);
swapSuccesses.push(...retryResult.swapSuccesses);
if (retryResult.remainingOrder) {
if (remainingOrder) {
remainingOrder.quantity += retryResult.remainingOrder.quantity;
}
else {
remainingOrder = retryResult.remainingOrder;
}
}
});
/**
* The routine for handling matches found in the order book. This can be run in parallel
* so that all matches, including those which require swaps with peers, can be executed
* simultaneously.
*/
const handleMatch = (maker, taker) => __awaiter(this, void 0, void 0, function* () {
onUpdate && onUpdate({ type: types_1.PlaceOrderEventType.Match, order: maker });
if (types_1.isOwnOrder(maker)) {
// this is an internal match which is effectively executed immediately upon being found
this.logger.info(`internal match executed on taker ${taker.id} and maker ${maker.id} for ${maker.quantity}`);
internalMatches.push(maker);
this.pool.broadcastOrderInvalidation(maker);
this.emit('ownOrder.filled', maker);
yield this.persistTrade({
quantity: maker.quantity,
makerOrder: maker,
takerOrder: taker,
});
}
else {
// this is a match with a peer order which cannot be considered executed until after a
// successful swap, which is an asynchronous process that can fail for numerous reasons
const portion = { id: maker.id, pairId: maker.pairId, quantity: maker.quantity };
const alias = aliasUtils_1.pubKeyToAlias(maker.peerPubKey);
this.logger.debug(`matched with peer ${maker.peerPubKey} (${alias}), executing swap on taker ${taker.id} and maker ${maker.id} for ${maker.quantity}`);
try {
const swapResult = yield this.executeSwap(maker, taker);
if (swapResult.quantity < maker.quantity) {
// swap was only partially completed
portion.quantity = swapResult.quantity;
const rejectedQuantity = maker.quantity - swapResult.quantity;
this.logger.info(`match partially executed on taker ${taker.id} and maker ${maker.id} for ${swapResult.quantity} ` +
`with peer ${maker.peerPubKey} (${alias}), ${rejectedQuantity} quantity not accepted and will repeat matching routine`);
yield retryFailedSwap(rejectedQuantity);
}
else {
this.logger.info(`match executed on taker ${taker.id} and maker ${maker.id} for ${maker.quantity} with peer ${maker.peerPubKey} (${alias})`);
}
swapSuccesses.push(swapResult);
onUpdate && onUpdate({ type: types_1.PlaceOrderEventType.SwapSuccess, swapSuccess: swapResult });
}
catch (err) {
const failMsg = `swap for ${portion.quantity} failed during order matching`;
if (typeof err === 'number' && enums_1.SwapFailureReason[err] !== undefined) {
// treat the error as a SwapFailureReason
this.logger.warn(`${failMsg} due to ${enums_1.SwapFailureReason[err]}, will repeat matching routine for failed quantity`);
const swapFailure = {
failureReason: err,
orderId: maker.id,
pairId: maker.pairId,
quantity: portion.quantity,
peerPubKey: maker.peerPubKey,
};
swapFailures.push(swapFailure);
try {
// we remove orders from the order book when they fail a swap so that we don't immediately retry them
const removedOrder = this.removePeerOrder(maker.id, maker.pairId, maker.peerPubKey).order;
// we want to try matching with this order again at a later time so we add
// the failed quantity back to the order and preserve the order to add it
// back to the order book after matching is complete for this taker order.
removedOrder.quantity += portion.quantity;
failedMakerOrders.push(removedOrder);
}
catch (err) {
if (err.code === errors_1.errorCodes.ORDER_NOT_FOUND) {
// if the order has already been removed, either it was removed fully during
// matching or it's been invalidated by a peer or filled by a separate order
// in this case we want to add back the order removed during matching
// but only if it was not invalidated by the peer in the same time period
if (!invalidatedMakerOrderIds.has(maker.id)) {
failedMakerOrders.push(maker);
}
}
else {
// for other errors we throw
throw err;
}
}
onUpdate && onUpdate({ swapFailure, type: types_1.PlaceOrderEventType.SwapFailure });
yield retryFailedSwap(portion.quantity);
}
else {
// treat this as a critical error and abort matching, we only expect SwapFailureReasons to be thrown in the try block above
this.logger.error(`${failMsg} due to unexpected error`, err);
throw err;
}
}
}
});
// iterate over the matches to be executed in parallel
const matchPromises = [];
for (const { maker, taker } of matchingResult.matches) {
matchPromises.push(handleMatch(maker, taker));
}
// wait for all matches to complete execution, any portions that cannot be executed due to
// failed swaps will be added to the remaining order which may be added to the order book.
yield Promise.all(matchPromises);
if (replacedOrderIdentifier) {
this.removeOrderHold(replacedOrderIdentifier.id, replacedOrderIdentifier.pairId);
}
if (remainingOrder) {
if (discardRemaining) {
this.logger.verbose(`no more matches found for order ${order.id}, remaining order will be discarded`);
remainingOrder = undefined;
}
else if (!retry) {
// on recursive retries of placeOrder, we don't add remaining orders to the orderbook
// instead we preserve the remainder and return it to the parent caller, which will sum
// up any remaining orders and add them to the order book as a single order once
// matching is complete
if (remainingOrder.quantity < TradingPair_1.default.QUANTITY_DUST_LIMIT ||
remainingOrder.quantity * remainingOrder.price < TradingPair_1.default.QUANTITY_DUST_LIMIT) {
remainingOrder = undefined;
this.logger.verbose(`remainder for order ${order.id} does not meet dust limit and will be discarded`);
}
else {
this.addOwnOrder(remainingOrder, replacedOrderIdentifier === null || replacedOrderIdentifier === void 0 ? void 0 : replacedOrderIdentifier.id);
onUpdate && onUpdate({ type: types_1.PlaceOrderEventType.RemainingOrder, order: remainingOrder });
}
}
}
else if (replacedOrderIdentifier) {
// we tried to replace an order but the replacement order was fully matched, so simply remove the original order
this.removeOwnOrder({
orderId: replacedOrderIdentifier.id,
pairId: replacedOrderIdentifier.pairId,
});
}
failedMakerOrders.forEach((peerOrder) => {
var _a;
const peer = this.pool.tryGetPeer(peerOrder.peerPubKey);
if ((peer === null || peer === void 0 ? void 0 : peer.active) && peer.isPairActive(peerOrder.pairId)) {
// if this peer and its trading pair is still active then we add the order back to the book
(_a = this.tradingPairs.get(peerOrder.pairId)) === null || _a === void 0 ? void 0 : _a.addPeerOrder(peerOrder);
}
});
this.pool.removeListener('packet.orderInvalidation', handlePeerOrderInvalidation);
return {
internalMatches,
swapSuccesses,
swapFailures,
remainingOrder,
};
});
/**
* Executes a swap between maker and taker orders. Emits the `peerOrder.filled` event if the swap succeeds.
* @returns A promise that resolves to a [[SwapSuccess]] once the swap is completed, throws a [[SwapFailureReason]] if it fails
*/
this.executeSwap = (maker, taker) => __awaiter(this, void 0, void 0, function* () {
// make sure the order is in the database before we begin the swap
if (!(yield this.repository.getOrder(maker.id))) {
yield this.repository.addOrderIfNotExists(Object.assign(Object.assign({}, maker), { nodeId: this.pool.getNodeId(maker.peerPubKey) }));
}
try {
const swapResult = yield this.swaps.executeSwap(maker, taker);
this.emit('peerOrder.filled', maker);
yield this.persistTrade({
quantity: swapResult.quantity,
makerOrder: maker,
takerOrder: taker,
rHash: swapResult.rHash,
});
return swapResult;
}
catch (err) {
const failureReason = err;
this.logger.error(`swap between orders ${maker.id} & ${taker.id} failed due to ${enums_1.SwapFailureReason[failureReason]}`);
throw failureReason;
}
});
/**
* Adds an own order to the order book and broadcasts it to peers.
* Optionally removes/replaces an existing order.
* @returns false if it's a duplicated order or with an invalid pair id, otherwise true
*/
this.addOwnOrder = (order, replaceOrderId) => {
const tp = this.getTradingPair(order.pairId);
if (replaceOrderId) {
this.removeOwnOrder({
orderId: replaceOrderId,
pairId: order.pairId,
noBroadcast: true,
});
}
const result = tp.addOwnOrder(order);
assert_1.default(result, 'own order id is duplicated');
this.localIdMap.set(order.localId, { id: order.id, pairId: order.pairId });
this.emit('ownOrder.added', order);
const outgoingOrder = OrderBook.createOutgoingOrder(order, replaceOrderId);
this.pool.broadcastOrder(outgoingOrder);
return true;
};
this.persistTrade = ({ quantity, makerOrder, takerOrder, makerOrderId, rHash }) => __awaiter(this, void 0, void 0, function* () {
assert_1.default(makerOrder || makerOrderId, 'either makerOrder or makerOrderId must be specified to persist a trade');
const addOrderPromises = [];
if (makerOrder) {
addOrderPromises.push(this.repository.addOrderIfNotExists(makerOrder));
}
if (takerOrder) {
addOrderPromises.push(this.repository.addOrderIfNotExists(takerOrder));
}
yield Promise.all(addOrderPromises);
yield this.repository.addTrade({
quantity,
rHash,
makerOrderId: makerOrder ? makerOrder.id : makerOrderId,
takerOrderId: takerOrder ? takerOrder.id : undefined,
});
});
/**
* Adds an incoming peer order to the local order book. It timestamps the order based on when it
* enters the order book and also records its initial quantity upon being received.
* @returns `false` if it's a duplicated order or with an invalid pair id, otherwise true
*/
this.addPeerOrder = (order) => {
if (this.thresholds.minQuantity > 0) {
if (!this.checkThresholdCompliance(order)) {
this.logger.debug('incoming peer order does not comply with configured threshold');
return false;
}
}
// TODO: penalize peers for sending ordes too small to swap?
if (order.quantity * order.price < TradingPair_1.default.QUANTITY_DUST_LIMIT ||
order.quantity < TradingPair_1.default.QUANTITY_DUST_LIMIT) {
this.logger.warn('incoming peer order is too small to swap');
return false;
}
const tp = this.tradingPairs.get(order.pairId);
if (!tp) {
// TODO: penalize peer for sending an order for an unsupported pair
return false;
}
const stampedOrder = Object.assign(Object.assign({}, order), { createdAt: utils_1.ms(), initialQuantity: order.quantity });
if (!tp.addPeerOrder(stampedOrder)) {
this.logger.debug(`incoming peer order is duplicated: ${order.id}`);
// TODO: penalize peer
return false;
}
this.emit('peerOrder.incoming', stampedOrder);
return true;
};
this.getOwnOrderByLocalId = (localId) => {
const orderIdentifier = this.localIdMap.get(localId);
if (!orderIdentifier) {
throw errors_1.default.LOCAL_ID_DOES_NOT_EXIST(localId);
}
const order = this.getOwnOrder(orderIdentifier.id, orderIdentifier.pairId);
return order;
};
this.removeOwnOrders = () => {
const removedOrderLocalIds = [];
const onHoldOrderLocalIds = [];
for (const localId of this.localIdMap.keys()) {
const { onHoldQuantity } = this.removeOwnOrderByLocalId(localId, true);
if (onHoldQuantity === 0) {
removedOrderLocalIds.push(localId);
}
else {
onHoldOrderLocalIds.push(localId);
}
}
return { removedOrderLocalIds, onHoldOrderLocalIds };
};
/**
* Removes all or part of an order from the order book by its local id. Throws an error if the
* specified pairId is not supported or if the order to cancel could not be found.
* @param allowAsyncRemoval whether to allow an eventual async removal of the order in case
* some quantity of the order is on hold and cannot be immediately removed. If false, while some quantity of
* the order is on hold, an error will be thrown.
* @param quantityToRemove the quantity to remove from the order, if undefined then the entire
* order is removed.
* @returns an object summarizing the result of the order removal, including any quantity that
* was on hold and could not be immediately removed, the total quantity removed, and the quantity
* remaining on the order.
*/
this.removeOwnOrderByLocalId = (localId, allowAsyncRemoval, quantityToRemove) => {
const order = this.getOwnOrderByLocalId(localId);
let remainingQuantityToRemove = quantityToRemove || order.quantity;
let onHoldQuantity = order.hold;
let removedQuantity = 0;
if (remainingQuantityToRemove > order.quantity) {
// quantity to be removed can't be higher than order's quantity.
throw errors_1.default.QUANTITY_DOES_NOT_MATCH(remainingQuantityToRemove, order.quantity);
}
const removableQuantity = order.quantity - order.hold;
if (remainingQuantityToRemove <= removableQuantity) {
this.removeOwnOrder({
orderId: order.id,
pairId: order.pairId,
quantityToRemove: remainingQuantityToRemove,
});
removedQuantity += remainingQuantityToRemove;
remainingQuantityToRemove = 0;
}
else {
// we can't immediately remove the entire quantity because of a hold on the order.
if (!allowAsyncRemoval) {
throw errors_1.default.QUANTITY_ON_HOLD(localId, order.hold);
}
if (removableQuantity > 0) {
// we can remove any portion of the order that's not on hold up front
this.removeOwnOrder({
orderId: order.id,
pairId: order.pairId,
quantityToRemove: removableQuantity,
});
removedQuantity += removableQuantity;
remainingQuantityToRemove -= removableQuantity;
}
const failedHandler = (deal) => {
if (deal.orderId === order.id) {
// remove the portion that failed now that it's not on hold
const quantityToRemove = Math.min(deal.quantity, remainingQuantityToRemove);
this.removeOwnOrder({
quantityToRemove,
orderId: order.id,
pairId: order.pairId,
});
cleanup(quantityToRemove);
}
};
const paidHandler = (result) => {
if (result.orderId === order.id) {
const quantityToRemove = Math.min(result.quantity, remainingQuantityToRemove);
cleanup(quantityToRemove);
}
};
const cleanup = (quantity) => {
remainingQuantityToRemove -= quantity;
removedQuantity += quantity;
onHoldQuantity -= quantity;
this.logger.debug(`removed hold of ${quantity} on local order ${localId}, ${remainingQuantityToRemove} remaining`);
if (remainingQuantityToRemove === 0) {
// we can stop listening for swaps once all holds are cleared
this.swaps.removeListener('swap.failed', failedHandler);
this.swaps.removeListener('swap.paid', paidHandler);
}
};
this.swaps.on('swap.failed', failedHandler);
this.swaps.on('swap.paid', paidHandler);
}
return {
removedQuantity,
onHoldQuantity,
pairId: order.pairId,
remainingQuantity: order.quantity - remainingQuantityToRemove,
};
};
this.addOrderHold = (orderId, pairId, holdAmount) => {
const tp = this.getTradingPair(pairId);
tp.addOrderHold(orderId, holdAmount);
};
this.removeOrderHold = (orderId, pairId, holdAmount) => {
const tp = this.getTradingPair(pairId);
tp.removeOrderHold(orderId, holdAmount);
};
/**
* Removes all or part of an own order from the order book and broadcasts an order invalidation packet.
* @param quantityToRemove the quantity to remove from the order, if undefined then the full order is removed
* @param takerPubKey the node pub key of the taker who filled this order, if applicable
* @returns the removed portion of the order
*/
this.removeOwnOrder = ({ orderId, pairId, quantityToRemove, takerPubKey, noBroadcast }) => {
const tp = this.getTradingPair(pairId);
try {
const removeResult = tp.removeOwnOrder(orderId, quantityToRemove);
this.emit('ownOrder.removed', removeResult.order);
if (removeResult.fullyRemoved) {
this.localIdMap.delete(removeResult.order.localId);
}
if (!noBroadcast) {
this.pool.broadcastOrderInvalidation(removeResult.order, takerPubKey);
}
return removeResult.order;
}
catch (err) {
if (quantityToRemove !== undefined) {
this.logger.error(`error while removing ${quantityToRemove} of order (${orderId})`, err);
}
else {
this.logger.error(`error while removing order (${orderId})`, err);
}
throw err;
}
};
/**
* Removes all or part of a peer order from the order book and emits the `peerOrder.invalidation` event.
* @param quantityToRemove the quantity to remove from the order, if undefined then the full order is removed
*/
this.removePeerOrder = (orderId, pairId, peerPubKey, quantityToRemove) => {
const tp = this.getTradingPair(pairId);
return tp.removePeerOrder(orderId, peerPubKey, quantityToRemove);
};
this.removePeerOrders = (peerPubKey) => {
if (!peerPubKey) {
return;
}
for (const pairId of this.pairInstances.keys()) {
this.removePeerPair(peerPubKey, pairId);
}
this.logger.debug(`removed all orders for peer ${peerPubKey} (${aliasUtils_1.pubKeyToAlias(peerPubKey)})`);
};
this.removePeerPair = (peerPubKey, pairId) => {
const tp = this.tradingPairs.get(pairId);
if (!tp) {
return;
}
const orders = tp.removePeerOrders(peerPubKey);
orders.forEach((order) => {
this.emit('peerOrder.invalidation', order);
});
};
this.checkPeerCurrencies = (peer) => {
const advertisedCurrencies = peer.getAdvertisedCurrencies();
advertisedCurrencies.forEach((advertisedCurrency) => {
if (!this.isPeerCurrencySupported(peer, advertisedCurrency)) {
peer.disableCurrency(advertisedCurrency);
}
else {
peer.enableCurrency(advertisedCurrency);
}
});
};
/**
* Verifies the advertised trading pairs of a peer. Checks that the peer has advertised
* lnd pub keys for both the base and quote currencies for each pair, and optionally attempts a
* "sanity swap" for each currency which is a 1 satoshi for 1 sat