"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;
