UNPKG

@fleupold/dex-contracts

Version:

Contracts for dFusion multi-token batch auction exchange

239 lines (238 loc) 11.2 kB
"use strict"; /** * Module containing event based orderbook reading implementation. The entire * orderbook is contructed from Ethreum events fetched in two stages: first, * the past events are fetched in block-based pages, second an event subsciption * is setup to listen to new incomming events that get added to the orderbook * snapshot. * * The streamed orderbook can be queried at any time to get the current open * orders (that is orders and balances as it would they would be included for * the next batch) or the finalized orderbook (orders and balances that are * being considered for trading by the solver). * * @packageDocumentation */ 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()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.deployment = exports.InvalidAuctionStateError = exports.StreamedOrderbook = exports.DEFAULT_ORDERBOOK_OPTIONS = void 0; const contracts_1 = require("../contracts"); const state_1 = require("./state"); /** * The default orderbook options. */ exports.DEFAULT_ORDERBOOK_OPTIONS = { blockPageSize: 10000, blockConfirmations: 6, strict: false, debug: () => { return; }, }; /** * The streamed orderbook that manages incoming events, and applies them to the * account state. */ class StreamedOrderbook { constructor(web3, contract, startBlock, options) { this.web3 = web3; this.contract = contract; this.startBlock = startBlock; this.options = options; this.batch = -1; this.confirmedState = new state_1.AuctionState(options); } /** * Create and return a new streamed orderbook. * * @remarks * This method returns a promise that resolves once all past events have been * applied to the current account state and the orderbook. * * @param web3 - The web3 provider to use. * @param options - Optional settings for tweaking the streamed orderbook. */ static init(web3, options = {}) { return __awaiter(this, void 0, void 0, function* () { const [contract, tx] = yield deployment(web3, contracts_1.BatchExchangeArtifact); const orderbook = new StreamedOrderbook(web3, contract, tx.blockNumber, Object.assign(Object.assign({}, exports.DEFAULT_ORDERBOOK_OPTIONS), options)); yield orderbook.applyPastEvents(); if (orderbook.options.endBlock === undefined) { yield orderbook.update(); } return orderbook; }); } /** * Retrieves the current open orders in the orderbook. */ getOpenOrders() { var _a; this.throwOnInvalidState(); const state = (_a = this.latestState) !== null && _a !== void 0 ? _a : this.confirmedState; return state.getOrders(this.batch); } /** * Apply all past events to the account state by querying the node for past * events with multiple queries to retrieve each block page at a time. */ applyPastEvents() { var _a; return __awaiter(this, void 0, void 0, function* () { const endBlock = (_a = this.options.endBlock) !== null && _a !== void 0 ? _a : (yield this.web3.eth.getBlockNumber()) - this.options.blockConfirmations; for (let fromBlock = this.startBlock; fromBlock < endBlock; fromBlock += this.options.blockPageSize) { // NOTE: `getPastEvents` block range is inclusive. const toBlock = Math.min(fromBlock + this.options.blockPageSize - 1, endBlock); this.options.debug(`fetching past events from ${fromBlock}-${toBlock}`); const events = yield this.getPastEvents({ fromBlock, toBlock }); this.options.debug(`applying ${events.length} past events`); this.confirmedState.applyEvents(events); } this.batch = yield this.getBatchId(endBlock); }); } /** * Apply new confirmed events to the account state and store the remaining * events that are subject to reorgs into the `pendingEvents` array. * * @param toBlock - Optional block number until which to fetch events, capped at latest block number. * @returns The block number up until which the streamed orderbook is up to * date * * @remarks * If there is an error retrieving the latest events from the node, then the * account state remains unmodified. This allows the updating orderbook to be * more fault-tolerant and deal with nodes being temporarily down and some * intermittent errors. However, if an error applying confirmed events occur, * then the streamed orderbook becomes invalid and can no longer apply new * events as the actual auction state is unknown. */ update(toBlock) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { this.throwOnInvalidState(); const fromBlock = this.confirmedState.nextBlock; this.options.debug(`fetching new events from ${fromBlock}-${toBlock !== null && toBlock !== void 0 ? toBlock : "latest"}`); const events = yield this.getPastEvents({ fromBlock, toBlock }); // NOTE: If the web3 instance is connected to nodes behind a load balancer, // it is possible that the events were queried on a node that includes an // additional block to the node that handled the query to the latest block // number, so use the max of `latestEvents.last().blockNumber` and the // queried latest block number. const latestBlock = Math.max(yield this.web3.eth.getBlockNumber(), (_b = (_a = events[events.length - 1]) === null || _a === void 0 ? void 0 : _a.blockNumber) !== null && _b !== void 0 ? _b : 0); const confirmedBlock = latestBlock - this.options.blockConfirmations; const endBlock = Math.min(latestBlock, toBlock !== null && toBlock !== void 0 ? toBlock : Number.MAX_SAFE_INTEGER); this.batch = yield this.getBatchId(endBlock); if (events.length === 0) { return endBlock; } const firstLatestEvent = events.findIndex((ev) => ev.blockNumber > confirmedBlock); const confirmedEventCount = firstLatestEvent !== -1 ? firstLatestEvent : events.length; const confirmedEvents = events.slice(0, confirmedEventCount); const latestEvents = events.slice(confirmedEventCount); this.options.debug(`applying ${confirmedEvents.length} confirmed events until block ${confirmedBlock}`); try { this.confirmedState.applyEvents(confirmedEvents); } catch (err) { this.invalidState = new InvalidAuctionStateError(confirmedBlock, err); this.options.debug(this.invalidState.message); throw this.invalidState; } this.latestState = undefined; this.options.debug(`reapplying ${latestEvents.length} latest events until block ${endBlock}`); if (latestEvents.length > 0) { // NOTE: Errors applying latest state are not considered fatal as we can // still recover from them (since the confirmed state is still valid). If // applying the latest events fails, just make sure that the `latestEvent` // property is not set, so that the query methods fall back to using the // confirmed state. const newLatestState = this.confirmedState.copy(); newLatestState.applyEvents(latestEvents); this.latestState = newLatestState; } return endBlock; }); } /** * Retrieves past events for the contract. */ getPastEvents(options) { return __awaiter(this, void 0, void 0, function* () { const events = yield this.contract.getPastEvents("allEvents", Object.assign({ toBlock: "latest" }, options)); return events; }); } /** * Retrieves the batch ID at a given block number. * * @remarks * The batch ID is locally calculated from the block header timestamp as it is * more reliable than executing an `eth_call` to calculate the batch ID on the * EVM since an archive node is required for sufficiently old blocks. */ getBatchId(blockNumber) { var _a; return __awaiter(this, void 0, void 0, function* () { const BATCH_DURATION = 300; const block = yield this.web3.eth.getBlock(blockNumber); // NOTE: Pending or future blocks return null when queried, so approximate // with system time const timestamp = (_a = block === null || block === void 0 ? void 0 : block.timestamp) !== null && _a !== void 0 ? _a : Date.now(); const batch = Math.floor(Number(timestamp) / BATCH_DURATION); return batch; }); } /** * Helper method to check for an unrecoverable invalid state in the current * streamed orderbook. * * @throws If the streamed orderbook is in an invalid state. */ throwOnInvalidState() { if (this.invalidState) { throw this.invalidState; } } } exports.StreamedOrderbook = StreamedOrderbook; /** * An error that is thrown on when the auction state is invalid and can no * longer be updated. */ class InvalidAuctionStateError extends Error { constructor(block, inner) { super(`invalid auction state at block ${block}: ${inner.message}`); this.block = block; this.inner = inner; } } exports.InvalidAuctionStateError = InvalidAuctionStateError; /** * Get a contract deployment, returning both the web3 contract object as well as * the transaction receipt for the contract deployment. * * @throws If the contract is not deployed on the network the web3 provider is * connected to. */ function deployment(web3, { abi, networks }) { return __awaiter(this, void 0, void 0, function* () { const chainId = yield web3.eth.getChainId(); const network = networks[chainId]; if (!networks) { throw new Error(`not deployed on network with chain ID ${chainId}`); } const tx = yield web3.eth.getTransactionReceipt(network.transactionHash); const contract = new web3.eth.Contract(abi, network.address); return [contract, tx]; }); } exports.deployment = deployment;