UNPKG

xud

Version:
854 lines 62.6 kB
"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