@nexex/orderbook
Version:
539 lines • 29.3 kB
JavaScript
"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