"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RetryProvider = void 0;
const ethers_1 = require("ethers");
const constants_1 = require("../constants");
const utils_1 = require("../utils");
const NetworkUtils_1 = require("../utils/NetworkUtils");
const cachedProvider_1 = require("./cachedProvider");
const utils_2 = require("./utils");
const constants_2 = require("./constants");
class RetryProvider extends ethers_1.ethers.providers.StaticJsonRpcProvider {
    nodeQuorumThreshold;
    retries;
    delay;
    maxConcurrency;
    logger;
    providers;
    constructor(params, chainId, nodeQuorumThreshold, retries, delay, maxConcurrency, providerCacheNamespace, pctRpcCallsLogged, redisClient, standardTtlBlockDistance, noTtlBlockDistance, providerCacheTtl = constants_2.PROVIDER_CACHE_TTL, logger) {
        super(undefined, chainId);
        this.nodeQuorumThreshold = nodeQuorumThreshold;
        this.retries = retries;
        this.delay = delay;
        this.maxConcurrency = maxConcurrency;
        this.logger = logger;
        this.providers = params.map((inputs) => new cachedProvider_1.CacheProvider(providerCacheNamespace, redisClient, standardTtlBlockDistance, noTtlBlockDistance, providerCacheTtl, maxConcurrency, pctRpcCallsLogged, logger, ...inputs));
        if (chainId !== constants_1.CHAIN_IDs.MAINNET) {
            this.pollingInterval = 1000;
            this.providers.forEach((provider) => (provider.pollingInterval = this.pollingInterval));
        }
        if (this.nodeQuorumThreshold < 1 || !Number.isInteger(this.nodeQuorumThreshold)) {
            throw new Error(`nodeQuorum,Threshold cannot be < 1 and must be an integer. Currently set to ${this.nodeQuorumThreshold}`);
        }
        if (this.retries < 0 || !Number.isInteger(this.retries)) {
            throw new Error(`retries cannot be < 0 and must be an integer. Currently set to ${this.retries}`);
        }
        if (this.delay < 0) {
            throw new Error(`delay cannot be < 0. Currently set to ${this.delay}`);
        }
        if (this.nodeQuorumThreshold > this.providers.length) {
            throw new Error(`nodeQuorumThreshold (${this.nodeQuorumThreshold}) must be <= the number of providers (${this.providers.length})`);
        }
    }
    async send(method, params) {
        const quorumThreshold = this._getQuorum(method, params);
        const requiredProviders = this.providers.slice(0, quorumThreshold);
        const fallbackProviders = this.providers.slice(quorumThreshold);
        const errors = [];
        const tryWithFallback = (provider) => {
            return this._trySend(provider, method, params)
                .then((result) => [provider, result])
                .catch((err) => {
                errors.push([provider, err?.stack || err?.toString()]);
                if (fallbackProviders.length === 0) {
                    throw err;
                }
                if (quorumThreshold === 1 && this.failImmediate(method, err)) {
                    throw err;
                }
                return tryWithFallback(fallbackProviders.shift());
            });
        };
        const results = await Promise.allSettled(requiredProviders.map(tryWithFallback));
        if (!results.every(utils_1.isPromiseFulfilled)) {
            const errorTexts = errors.map(([provider, errorText]) => (0, utils_2.formatProviderError)(provider, errorText));
            const successfulProviderUrls = results
                .filter(utils_1.isPromiseFulfilled)
                .map((result) => (0, NetworkUtils_1.getOriginFromURL)(result.value[0].connection.url));
            throw (0, utils_2.createSendErrorWithMessage)(`Not enough providers succeeded. Errors:\n${errorTexts.join("\n")}\n` +
                `Successful Providers:\n${successfulProviderUrls.join("\n")}`, results.find(utils_1.isPromiseRejected)?.reason);
        }
        const values = results.map((result) => result.value);
        if (values.slice(1).every(([, output]) => (0, utils_2.compareRpcResults)(method, values[0][1], output))) {
            return values[0][1];
        }
        const getMismatchedProviders = (values) => {
            return values
                .filter(([, result]) => !(0, utils_2.compareRpcResults)(method, result, quorumResult))
                .map(([provider]) => (0, NetworkUtils_1.getOriginFromURL)(provider.connection.url));
        };
        const logQuorumMismatchOrFailureDetails = (method, params, quorumProviders, mismatchedProviders, errors) => {
            this.logger?.warn({
                at: "ProviderUtils",
                message: "Some providers mismatched with the quorum result or failed 🚸",
                notificationPath: "across-warn",
                method,
                params: JSON.stringify(params),
                quorumProviders,
                mismatchedProviders,
                erroringProviders: errors.map(([provider, errorText]) => (0, utils_2.formatProviderError)(provider, errorText)),
            });
        };
        const throwQuorumError = (fallbackValues) => {
            const errorTexts = errors.map(([provider, errorText]) => (0, utils_2.formatProviderError)(provider, errorText));
            const successfulProviderUrls = values.map(([provider]) => (0, NetworkUtils_1.getOriginFromURL)(provider.connection.url));
            const mismatchedProviders = getMismatchedProviders([...values, ...(fallbackValues || [])]);
            logQuorumMismatchOrFailureDetails(method, params, successfulProviderUrls, mismatchedProviders, errors);
            throw new Error("Not enough providers agreed to meet quorum.\n" +
                "Providers that errored:\n" +
                `${errorTexts.join("\n")}\n` +
                "Providers that succeeded, but some failed to match:\n" +
                successfulProviderUrls.join("\n"));
        };
        if (fallbackProviders.length === 0) {
            throwQuorumError();
        }
        const fallbackResults = await Promise.allSettled(fallbackProviders.map((provider) => this._trySend(provider, method, params)
            .then((result) => [provider, result])
            .catch((err) => {
            errors.push([provider, err?.stack || err?.toString()]);
            throw new Error("No fallbacks during quorum search");
        })));
        const fallbackValues = fallbackResults.filter(utils_1.isPromiseFulfilled).map((promise) => promise.value);
        const counts = [...values, ...fallbackValues].reduce((acc, curr) => {
            const [, result] = curr;
            const existingMatch = acc.find(([existingResult]) => (0, utils_2.compareRpcResults)(method, existingResult, result));
            if (existingMatch) {
                existingMatch[1]++;
            }
            else {
                acc.push([result, 1]);
            }
            return acc;
        }, [[undefined, 0]]);
        counts.sort(([, a], [, b]) => b - a);
        const [quorumResult, count] = counts[0];
        if (count < quorumThreshold) {
            throwQuorumError(fallbackValues);
        }
        const mismatchedProviders = getMismatchedProviders([...values, ...fallbackValues]);
        const quorumProviders = [...values, ...fallbackValues]
            .filter(([, result]) => (0, utils_2.compareRpcResults)(method, result, quorumResult))
            .map(([provider]) => (0, NetworkUtils_1.getOriginFromURL)(provider.connection.url));
        if (mismatchedProviders.length > 0 || errors.length > 0) {
            logQuorumMismatchOrFailureDetails(method, params, quorumProviders, mismatchedProviders, errors);
        }
        return quorumResult;
    }
    _validateResponse(method, _, response) {
        return (0, utils_1.isDefined)(response) || method === "eth_getTransactionReceipt" || method === "eth_getTransactionByHash";
    }
    async _sendAndValidate(provider, method, params) {
        const response = await provider.send(method, params);
        if (!this._validateResponse(method, params, response)) {
            this.logger?.debug({
                at: "ProviderUtils",
                message: "Provider returned invalid response",
                provider: (0, NetworkUtils_1.getOriginFromURL)(provider.connection.url),
                method,
                params,
                response,
            });
            throw new Error("Response failed validation");
        }
        return response;
    }
    failImmediate(method, response) {
        const err = (0, utils_2.parseJsonRpcError)(response);
        if (!err) {
            return false;
        }
        if (err.code >= -32768 && err.code <= -32100) {
            return false;
        }
        const message = err.message.toLowerCase();
        switch (method) {
            case "eth_call":
            case "eth_estimateGas":
            case "eth_sendRawTransaction": {
                const keywords = ["revert", "nonce", "underpriced", "gas", "fee"];
                return keywords.some((keyword) => message.includes(keyword));
            }
            default:
                break;
        }
        return false;
    }
    async _trySend(provider, method, params) {
        let { retries } = this;
        while (true) {
            const [settled] = await Promise.allSettled([this._sendAndValidate(provider, method, params)]);
            if (settled.status === "fulfilled") {
                return settled.value;
            }
            if (retries-- <= 0 || this.failImmediate(method, settled.reason)) {
                throw settled.reason;
            }
            await (0, utils_1.delay)(this.delay);
        }
    }
    _getQuorum(method, params) {
        if (method === "eth_getLogs") {
            return this.nodeQuorumThreshold;
        }
        if (method === "eth_getBlockByNumber" && params[0] !== "latest" && params[0] !== "pending") {
            return this.nodeQuorumThreshold;
        }
        if (method === "eth_call" && params[1] !== "latest") {
            return this.nodeQuorumThreshold;
        }
        return 1;
    }
}
exports.RetryProvider = RetryProvider;
//# sourceMappingURL=retryProvider.js.map