"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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChainEventPoller = exports.POLLS_SINCE_LAST_FROM_BLOCK_UPDATE_THRESHOLD = void 0;
const utils_1 = require("@streamr/utils");
const ethers_1 = require("ethers");
const remove_1 = __importDefault(require("lodash/remove"));
const sample_1 = __importDefault(require("lodash/sample"));
const uniq_1 = __importDefault(require("lodash/uniq"));
const tsyringe_1 = require("tsyringe");
const Config_1 = require("../Config");
const RpcProviderSource_1 = require("../RpcProviderSource");
const BLOCK_NUMBER_QUERY_RETRY_DELAY = 1000;
exports.POLLS_SINCE_LAST_FROM_BLOCK_UPDATE_THRESHOLD = 30;
let ChainEventPoller = class ChainEventPoller {
    listeners = [];
    providers;
    pollInterval;
    abortController;
    constructor(rpcProviderSource, config) {
        this.providers = rpcProviderSource.getSubProviders();
        this.pollInterval = config.contracts.pollInterval;
    }
    on(definition) {
        const started = this.listeners.length > 0;
        this.listeners.push(definition);
        if (!started) {
            this.start();
        }
    }
    off(definition) {
        const started = this.listeners.length > 0;
        (0, remove_1.default)(this.listeners, (l) => {
            return (l.contractAddress === definition.contractAddress)
                && (l.contractInterfaceFragment.topicHash === definition.contractInterfaceFragment.topicHash)
                && (l.onEvent == definition.onEvent);
        });
        if (started && this.listeners.length === 0) {
            this.abortController.abort();
        }
    }
    start() {
        const abortController = new AbortController();
        this.abortController = abortController;
        setImmediate(async () => {
            const logger = new utils_1.Logger(module, { sessionId: (0, utils_1.randomString)(6) });
            logger.debug('Start polling', { pollInterval: this.pollInterval });
            let fromBlock = undefined;
            do {
                try {
                    fromBlock = await (0, sample_1.default)(this.providers).getBlockNumber();
                }
                catch (err) {
                    logger.debug('Failed to query block number', { err });
                    await (0, utils_1.wait)(BLOCK_NUMBER_QUERY_RETRY_DELAY); // TODO: pass signal?
                }
            } while (fromBlock === undefined);
            let pollsSinceFromBlockUpdate = 0;
            await (0, utils_1.scheduleAtInterval)(async () => {
                const provider = (0, sample_1.default)(this.providers);
                const eventNames = this.listeners.map((l) => l.contractInterfaceFragment.name);
                let newFromBlock = 0;
                let events = undefined;
                try {
                    // If we haven't updated `fromBlock` for a while, fetch the latest block number explicitly. If
                    // `fromBlock` falls too much behind the current block number, the RPCs may start rejecting our
                    // eth_getLogs requests (presumably for performance reasons).
                    if (pollsSinceFromBlockUpdate >= exports.POLLS_SINCE_LAST_FROM_BLOCK_UPDATE_THRESHOLD) {
                        newFromBlock = await provider.getBlockNumber() + 1;
                        logger.debug('Fetch next block number explicitly', { newFromBlock });
                        if (abortController.signal.aborted) {
                            return;
                        }
                    }
                    logger.debug('Polling', { fromBlock, eventNames });
                    // This creates a filter with the following criteria: 
                    // - logs must originate from any of the specified addresses: [address1, address2] (OR condition)
                    // - logs must match any of the specified topics: [[topic1, topic2, topic3]] (OR condition for topic[0])
                    // - logs must be within the specified fromBlock range
                    //
                    // In the topics filter:
                    // - the inner array ([topic1, topic2, topic3]) applies an OR condition, meaning the log’s first topic can be any of these
                    // - the outer array ([[topic1, topic2, topic3]]) applies an AND condition across topic positions (i.e. only one AND expression)
                    //   See: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_newfilter
                    //
                    // Ideally, we would specify exact address-topic pairs, such as:
                    //   (addr=111 AND topic=222) OR (addr=333 AND topic=444)
                    // However, Ethereum's API does not support this level of filtering.
                    //
                    // As a result, this filter may return additional logs beyond the intended matches. 
                    // For example, if we want:
                    //   - Topic T1 from addresses A1 and A2
                    //   - Topic T2 from address A3
                    // We might also receive:
                    //   - T2 from A1 or A2
                    //   - T1 from A3
                    // These extra events are safely ignored, as the event propagation logic (see line 148) ensures that
                    // only relevant listeners process them.
                    const filter = {
                        address: (0, uniq_1.default)(this.listeners.map((l) => l.contractAddress)),
                        topics: [(0, uniq_1.default)(this.listeners.map((l) => l.contractInterfaceFragment.topicHash))],
                        fromBlock
                    };
                    const logItems = await provider.getLogs(filter);
                    events = [];
                    for (const logItem of logItems) {
                        const definition = this.listeners.find((l) => {
                            return (l.contractAddress === (0, utils_1.toEthereumAddress)(logItem.address))
                                && (l.contractInterfaceFragment.topicHash === logItem.topics[0]);
                        });
                        if (definition !== undefined) {
                            const contractInterface = new ethers_1.Interface([definition.contractInterfaceFragment.format('minimal')]);
                            const args = contractInterface.decodeEventLog(definition.contractInterfaceFragment.name, logItem.data, logItem.topics);
                            events.push({
                                contractAddress: definition.contractAddress,
                                name: definition.contractInterfaceFragment.name,
                                args,
                                blockNumber: logItem.blockNumber
                            });
                        }
                    }
                    logger.debug('Polled', { fromBlock, events: events.length });
                }
                catch (err) {
                    logger.debug('Failed to poll', { reason: err?.reason, eventNames, fromBlock });
                }
                if (abortController.signal.aborted) {
                    return;
                }
                if (events !== undefined && events.length > 0) {
                    for (const event of events) {
                        const listeners = this.listeners.filter((l) => (l.contractAddress === event.contractAddress) && (l.contractInterfaceFragment.name === event.name));
                        for (const listener of listeners) {
                            listener.onEvent(...event.args, event.blockNumber);
                        }
                    }
                    newFromBlock = Math.max(...events.map((e) => e.blockNumber)) + 1;
                }
                // note: do not update fromBlock if polling events failed
                if (events !== undefined && newFromBlock > fromBlock) {
                    logger.debug('Forward fromBlock', { before: fromBlock, after: newFromBlock });
                    fromBlock = newFromBlock;
                    // eslint-disable-next-line require-atomic-updates
                    pollsSinceFromBlockUpdate = 0;
                }
                else {
                    pollsSinceFromBlockUpdate += 1;
                }
            }, this.pollInterval, true, abortController.signal);
        });
    }
};
exports.ChainEventPoller = ChainEventPoller;
exports.ChainEventPoller = ChainEventPoller = __decorate([
    (0, tsyringe_1.scoped)(tsyringe_1.Lifecycle.ContainerScoped),
    __param(1, (0, tsyringe_1.inject)(Config_1.ConfigInjectionToken)),
    __metadata("design:paramtypes", [RpcProviderSource_1.RpcProviderSource, Object])
], ChainEventPoller);
//# sourceMappingURL=ChainEventPoller.js.map