UNPKG

@nexex/orderbook

Version:
539 lines 29.3 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; 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 __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var common_1 = require("@nestjs/common"); var api_1 = require("@nexex/api"); var types_1 = require("@nexex/types"); var bignumber_js_1 = __importDefault(require("bignumber.js")); var ethers_1 = require("ethers"); var utils_1 = require("ethers/utils"); var ramda_1 = __importDefault(require("ramda")); var rxjs_1 = require("rxjs"); var sorted_array_1 = __importDefault(require("sorted-array")); var events_module_1 = require("../events/events.module"); var global_model_1 = require("../global/global.model"); var logger_1 = __importDefault(require("../logger")); var order_service_1 = require("../order/order.service"); var bignumber_1 = require("../utils/bignumber"); var decorators_1 = require("../utils/decorators"); var defer_1 = require("../utils/defer"); var orderUtil_1 = require("../utils/orderUtil"); var orderbook_error_1 = require("./orderbook.error"); var OrderbookService = /** @class */ (function () { function OrderbookService(orderService, dex, config, events$) { this.orderService = orderService; this.dex = dex; this.config = config; this.events$ = events$; this.orderbookMap = {}; this.ready = defer_1.defer(); this.init().catch(this.ready.reject); } OrderbookService.prototype.whenReady = function () { return this.ready.promise; }; /** * * @param baseTokenAddr * @param quoteTokenAddr * @throws error */ OrderbookService.prototype.getOrderbook = function (baseTokenAddr, quoteTokenAddr) { var key = baseTokenAddr + "-" + quoteTokenAddr; return this.getOrderbookById(key); }; /** * * @param marketId * @throws error */ OrderbookService.prototype.getOrderbookById = function (marketId) { return this.orderbookMap[marketId.toLowerCase()]; }; OrderbookService.prototype.getOrderbooks = function () { return Object.values(this.orderbookMap); }; OrderbookService.prototype.getMarkets = function () { return __awaiter(this, void 0, void 0, function () { var ret, _i, _a, marketId, _b, baseAddr, quoteAddr, _c, base, quote, _d, baseInRegistry, _e, quoteInRegistry, _f; return __generator(this, function (_g) { switch (_g.label) { case 0: return [4 /*yield*/, this.whenReady()]; case 1: _g.sent(); ret = []; _i = 0, _a = Object.keys(this.orderbookMap); _g.label = 2; case 2: if (!(_i < _a.length)) return [3 /*break*/, 8]; marketId = _a[_i]; _b = marketId.split('-'), baseAddr = _b[0], quoteAddr = _b[1]; return [4 /*yield*/, this.dex.token.getToken(baseAddr)]; case 3: _f = [ _g.sent() ]; return [4 /*yield*/, this.dex.token.getToken(quoteAddr)]; case 4: _f = _f.concat([ _g.sent() ]); return [4 /*yield*/, this.dex.tokenRegistry.getTokenMetaData(baseAddr)]; case 5: _f = _f.concat([ _g.sent() ]); return [4 /*yield*/, this.dex.tokenRegistry.getTokenMetaData(quoteAddr)]; case 6: _c = _f.concat([ _g.sent() ]), base = _c[0], quote = _c[1], _d = _c[2], baseInRegistry = _d === void 0 ? { symbol: undefined } : _d, _e = _c[3], quoteInRegistry = _e === void 0 ? { symbol: undefined } : _e; ret.push({ base: base.token, quote: quote.token, marketName: (baseInRegistry.symbol || baseAddr) + "-" + (quoteInRegistry.symbol || quoteAddr), marketId: marketId }); _g.label = 7; case 7: _i++; return [3 /*break*/, 2]; case 8: return [2 /*return*/, ret]; } }); }); }; /** * * @param order * @throws OrderbookNotExist */ OrderbookService.prototype.addOrder = function (order) { var market = this.findOrderMarket(order.signedOrder.makerTokenAddress, order.signedOrder.takerTokenAddress); if (order.side === types_1.OrderSide.BID) { market.bids.insert(order); } else { market.asks.insert(order); } return market.baseToken.addr.toLowerCase() + "-" + market.quoteToken.addr.toLowerCase(); }; OrderbookService.prototype.findOrderMarket = function (makerTokenAddress, takerTokenAddress) { for (var _i = 0, _a = Object.values(this.orderbookMap); _i < _a.length; _i++) { var market = _a[_i]; if ((market.baseToken.addr.toLowerCase() === makerTokenAddress.toLowerCase() && market.quoteToken.addr.toLowerCase() === takerTokenAddress.toLowerCase()) || (market.baseToken.addr.toLowerCase() === takerTokenAddress.toLowerCase() && market.quoteToken.addr.toLowerCase() === makerTokenAddress.toLowerCase())) { return market; } } throw new orderbook_error_1.OrderbookNotExist(); }; OrderbookService.prototype.updateBalance = function (marketId, orderHash, side, baseAmount, quoteAmount, lastUpdate) { var orderbook = this.getOrderbookById(marketId); var orders = side === types_1.OrderSide.ASK ? orderbook.asks : orderbook.bids; var match = orders.array.find(function (order) { return order.orderHash === orderHash; }); if (match) { match.remainingBaseTokenAmount = baseAmount; match.remainingQuoteTokenAmount = quoteAmount; match.lastUpdate = lastUpdate; } }; OrderbookService.prototype.delistOrder = function (marketId, orderHash, side) { var orderbook = this.getOrderbookById(marketId); var orders = side === types_1.OrderSide.ASK ? orderbook.asks : orderbook.bids; var match = orders.array.find(function (order) { return order.orderHash === orderHash; }); if (match) { orders.remove(match); } }; OrderbookService.prototype.validateOrder = function (plainOrder) { return __awaiter(this, void 0, void 0, function () { var minMakerFeeRate, market, order, availableVolume, _a, _b, makerTokenAmount, takerTokenAmount, availableMakerVolume, e_1, _c, minOrderBase, minOrderQuote, _d; return __generator(this, function (_e) { switch (_e.label) { case 0: if (!api_1.orderUtil.isValidOrder(plainOrder)) { throw new Error('Order Validation failed'); } if (this.dex.exchange.getContractAddress().toLowerCase() !== plainOrder.exchangeContractAddress.toLowerCase()) { throw new Error('Order Validation failed'); } if (plainOrder.makerFeeRecipient.toLowerCase() !== this.config.marketDefault.makerFeeRecipient.toLowerCase()) { throw new Error('Order Validation failed, bad makerFeeRecipient'); } minMakerFeeRate = api_1.FeeRate.from(this.config.marketDefault.minMakerFeeRate); if (minMakerFeeRate.lt(plainOrder.makerFeeRate)) { throw new Error('require more maker fee rate'); } market = this.findOrderMarket(plainOrder.makerTokenAddress, plainOrder.takerTokenAddress); order = orderUtil_1.fromPlainDexOrder(market.baseToken, market.quoteToken, plainOrder); _e.label = 1; case 1: _e.trys.push([1, 3, , 4]); _a = bignumber_1.bignumberToBignumber; return [4 /*yield*/, this.dex.exchange.availableVolume(order.signedOrder)]; case 2: availableVolume = _a.apply(void 0, [_e.sent()]); _b = order.signedOrder, makerTokenAmount = _b.makerTokenAmount, takerTokenAmount = _b.takerTokenAmount; availableMakerVolume = availableVolume .times(makerTokenAmount) .div(takerTokenAmount) .decimalPlaces(0, bignumber_js_1.default.ROUND_DOWN); if (order.side === types_1.OrderSide.ASK) { order.remainingBaseTokenAmount = availableMakerVolume; order.remainingQuoteTokenAmount = availableVolume; } else { order.remainingBaseTokenAmount = availableVolume; order.remainingQuoteTokenAmount = availableMakerVolume; } order.lastUpdate = new Date(); return [3 /*break*/, 4]; case 3: e_1 = _e.sent(); logger_1.default.error("failed to fetch availableVolume for incomming order: " + order.orderHash); logger_1.default.error(e_1); throw new orderbook_error_1.FailToQueryAvailableVolume(); case 4: return [4 /*yield*/, this.dex.token.parseAmount(order.baseTokenAddress, this.config.marketDefault.minOrderBaseVolume)]; case 5: _d = [ _e.sent() ]; return [4 /*yield*/, this.dex.token.parseAmount(order.quoteTokenAddress, this.config.marketDefault.minOrderQuoteVolume)]; case 6: _c = _d.concat([ _e.sent() ]), minOrderBase = _c[0], minOrderQuote = _c[1]; if (minOrderBase.gt(order.remainingBaseTokenAmount.toString(10))) { throw new orderbook_error_1.OrderAmountTooSmall(minOrderBase.toString(), order.remainingBaseTokenAmount.toString(10)); } if (minOrderQuote.gt(order.remainingQuoteTokenAmount.toString(10))) { throw new orderbook_error_1.OrderAmountTooSmall(minOrderQuote.toString(), order.remainingQuoteTokenAmount.toString(10)); } return [2 /*return*/, order]; } }); }); }; OrderbookService.prototype.getSnapshot = function (marketId, limit, minimal) { return __awaiter(this, void 0, void 0, function () { var _a, baseAddress, quoteAddress, ob, fn, slicedOb; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, this.whenReady()]; case 1: _b.sent(); _a = marketId.split('-'), baseAddress = _a[0], quoteAddress = _a[1]; ob = this.getOrderbook(baseAddress, quoteAddress); fn = minimal ? ramda_1.default.compose(ramda_1.default.project(['orderHash', 'price', 'remainingBaseTokenAmount', 'remainingQuoteTokenAmount']), ramda_1.default.slice(0, limit)) : ramda_1.default.slice(0, limit); if (ob) { slicedOb = { bids: fn(ob.bids.array), asks: fn(ob.asks.array) }; return [2 /*return*/, slicedOb]; } else { throw new Error('Orderbook not found'); } return [2 /*return*/]; } }); }); }; OrderbookService.prototype.queryOrderAggregateByPrice = function (marketId, side, price, decimals) { return __awaiter(this, void 0, void 0, function () { var _a, baseAddress, quoteAddress, ob, orders, priceFilterFn; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, this.whenReady()]; case 1: _b.sent(); _a = marketId.split('-'), baseAddress = _a[0], quoteAddress = _a[1]; ob = this.getOrderbook(baseAddress, quoteAddress); orders = side === types_1.OrderSide.ASK ? ob.asks.array : ob.bids.array; if (new bignumber_js_1.default(price).decimalPlaces() > decimals) { throw new Error('decimals of price does not match decimals passed in'); } priceFilterFn = side === types_1.OrderSide.ASK ? function (order) { return order.price.decimalPlaces(decimals, bignumber_js_1.default.ROUND_UP).eq(price); } : function (order) { return order.price.decimalPlaces(decimals).eq(price); }; return [2 /*return*/, ramda_1.default.compose(function (orders) { return ({ price: new bignumber_js_1.default(price), orders: ramda_1.default.project(['orderHash', 'remainingBaseTokenAmount', 'remainingQuoteTokenAmount'], orders) }); }, ramda_1.default.project(['orderHash', 'price', 'remainingBaseTokenAmount', 'remainingQuoteTokenAmount']), ramda_1.default.filter(priceFilterFn))(orders)]; } }); }); }; OrderbookService.prototype.buildFillUpToTx = function (marketId, side, orderHashs) { return __awaiter(this, void 0, void 0, function () { var ob, orders, ret, _loop_1, _i, orderHashs_1, orderHash; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.whenReady()]; case 1: _a.sent(); ob = this.getOrderbookById(marketId); orders = side === types_1.OrderSide.ASK ? ob.asks.array : ob.bids.array; ret = []; _loop_1 = function (orderHash) { var match = orders.find(function (order) { return order.orderHash === orderHash; }); if (match) { ret.push(match.signedOrder); } }; for (_i = 0, orderHashs_1 = orderHashs; _i < orderHashs_1.length; _i++) { orderHash = orderHashs_1[_i]; _loop_1(orderHash); } return [2 /*return*/, ret]; } }); }); }; OrderbookService.prototype.topOrders = function (marketId, limit, decimals) { if (decimals === void 0) { decimals = 5; } return __awaiter(this, void 0, void 0, function () { var _a, baseAddress, quoteAddress, ob; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, this.whenReady()]; case 1: _b.sent(); _a = marketId.split('-'), baseAddress = _a[0], quoteAddress = _a[1]; ob = this.getOrderbook(baseAddress, quoteAddress); return [2 /*return*/, { bids: this.topBidOrders(ob.bids.array, limit, decimals), asks: this.topAskOrders(ob.asks.array, limit, decimals), baseToken: baseAddress, quoteToken: quoteAddress }]; } }); }); }; /** * 1) get market list from config * 2) load orders of each market from db * 3) register listener of ipfs for each market */ OrderbookService.prototype.init = function () { return __awaiter(this, void 0, void 0, function () { var length, idx, marketSymbol, _a, baseName, quoteName, _b, baseAddress, quoteAddress, baseTokenAddrNormalized, quoteTokenAddrNormalized, marketId, ob, e_2; return __generator(this, function (_c) { switch (_c.label) { case 0: logger_1.default.info('OrderbookService#init: start'); length = this.config.markets.length; idx = 0; _c.label = 1; case 1: if (!(idx < length)) return [3 /*break*/, 7]; marketSymbol = this.config.markets[idx]; logger_1.default.info('OrderbookService#init: %d/%d %s', idx + 1, length, marketSymbol); _a = marketSymbol.split('-'), baseName = _a[0], quoteName = _a[1]; _c.label = 2; case 2: _c.trys.push([2, 5, , 6]); return [4 /*yield*/, Promise.all([ this.getTokenAddress(baseName), this.getTokenAddress(quoteName) ])]; case 3: _b = _c.sent(), baseAddress = _b[0], quoteAddress = _b[1]; baseTokenAddrNormalized = baseAddress.toLowerCase(); quoteTokenAddrNormalized = quoteAddress.toLowerCase(); marketId = baseTokenAddrNormalized + "-" + quoteTokenAddrNormalized; return [4 /*yield*/, this.loadOrderbook(baseTokenAddrNormalized, quoteTokenAddrNormalized)]; case 4: ob = _c.sent(); this.orderbookMap[marketId] = ob; // step 3) this.events$.next({ type: types_1.ObEventTypes.IPFS_SUBSCRIPTION, payload: { marketId: marketId } }); return [3 /*break*/, 6]; case 5: e_2 = _c.sent(); logger_1.default.error('OrderbookService#init: %s failed', marketSymbol); logger_1.default.error(e_2.stack); return [3 /*break*/, 6]; case 6: idx++; return [3 /*break*/, 1]; case 7: this.ready.resolve(); logger_1.default.info('OrderbookService#init: complete'); return [2 /*return*/]; } }); }); }; // price round down for bids, round up for asks OrderbookService.prototype.topBidOrders = function (orders, limit, decimals) { var state = { count: 0, price: undefined }; var takeLimitOrderFn = ramda_1.default.takeWhile(function (order) { var price = order.price.decimalPlaces(decimals); if (!state.price || !price.eq(state.price)) { state.count++; state.price = price; } return state.count <= limit; }); return ramda_1.default.compose(ramda_1.default.map(function (orders) { return { orders: ramda_1.default.project(['orderHash', 'remainingBaseTokenAmount', 'remainingQuoteTokenAmount'], orders), price: orders[0].price.decimalPlaces(decimals) }; }), ramda_1.default.groupWith(function (left, right) { return left.price.decimalPlaces(decimals).eq(right.price.decimalPlaces(decimals)); }), ramda_1.default.project(['orderHash', 'price', 'remainingBaseTokenAmount', 'remainingQuoteTokenAmount']), takeLimitOrderFn)(orders); }; // price round down for bids, round up for asks OrderbookService.prototype.topAskOrders = function (orders, limit, decimals) { var state = { count: 0, price: undefined }; var takeLimitOrderFn = ramda_1.default.takeWhile(function (order) { var price = order.price.decimalPlaces(decimals, bignumber_js_1.default.ROUND_UP); if (!state.price || !price.eq(state.price)) { state.count++; state.price = price; } return state.count <= limit; }); return ramda_1.default.compose(ramda_1.default.map(function (orders) { // const [aggregateBaseTokenAmount, aggregateQuoteTokenAmount] = // orders.reduce(([acc1, acc2], order) => // [acc1.plus(order.remainingBaseTokenAmount), acc2.plus(order.remainingQuoteTokenAmount)] // , [new BigNumber(0), new BigNumber(0)]); return { orders: ramda_1.default.project(['orderHash', 'remainingBaseTokenAmount', 'remainingQuoteTokenAmount'], orders), price: orders[0].price.decimalPlaces(decimals) }; }), ramda_1.default.groupWith(function (left, right) { return left.price.decimalPlaces(decimals).eq(right.price.decimalPlaces(decimals)); }), ramda_1.default.project(['orderHash', 'price', 'remainingBaseTokenAmount', 'remainingQuoteTokenAmount']), takeLimitOrderFn)(orders); }; OrderbookService.prototype.loadOrderbook = function (baseTokenAddress, quoteTokenAddress) { return __awaiter(this, void 0, void 0, function () { var _a, bids, asks, _b, _c, baseTokenContract, quoteTokenContract, _d, sortedBids, sortedAsks; return __generator(this, function (_e) { switch (_e.label) { case 0: return [4 /*yield*/, this.orderService.loadOrders(baseTokenAddress, quoteTokenAddress, types_1.OrderSide.BID)]; case 1: _b = [ _e.sent() ]; return [4 /*yield*/, this.orderService.loadOrders(baseTokenAddress, quoteTokenAddress, types_1.OrderSide.ASK)]; case 2: _a = _b.concat([ _e.sent() ]), bids = _a[0], asks = _a[1]; return [4 /*yield*/, this.dex.token.getToken(baseTokenAddress)]; case 3: _d = [ _e.sent() ]; return [4 /*yield*/, this.dex.token.getToken(quoteTokenAddress)]; case 4: _c = _d.concat([ _e.sent() ]), baseTokenContract = _c[0], quoteTokenContract = _c[1]; sortedBids = new sorted_array_1.default(bids, function (a, b) { return a.price .minus(b.price) .negated() .toNumber(); }); sortedAsks = new sorted_array_1.default(asks, function (a, b) { return a.price.minus(b.price).toNumber(); }); return [2 /*return*/, { baseToken: baseTokenContract.token, quoteToken: quoteTokenContract.token, bids: sortedBids, asks: sortedAsks }]; } }); }); }; OrderbookService.prototype.getTokenAddress = function (nameOrAddress) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { if (ethers_1.ethers.utils.isHexString(nameOrAddress)) { return [2 /*return*/, utils_1.getAddress(nameOrAddress)]; } return [2 /*return*/, this.dex.tokenRegistry.getTokenAddressBySymbol(nameOrAddress)]; }); }); }; __decorate([ decorators_1.localCache(12 * 60 * 60 * 1000), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], OrderbookService.prototype, "getMarkets", null); OrderbookService = __decorate([ common_1.Injectable(), __param(3, common_1.Inject(events_module_1.EventsModule.EventSubject)), __metadata("design:paramtypes", [order_service_1.OrderService, api_1.Dex, global_model_1.ObConfig, rxjs_1.Subject]) ], OrderbookService); return OrderbookService; }()); exports.OrderbookService = OrderbookService; //# sourceMappingURL=orderbook.service.js.map