"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RetryProvider = void 0;
var tslib_1 = require("tslib");
var ethers_1 = require("ethers");
var constants_1 = require("../constants");
var utils_1 = require("../utils");
var NetworkUtils_1 = require("../utils/NetworkUtils");
var cachedProvider_1 = require("./cachedProvider");
var utils_2 = require("./utils");
var constants_2 = require("./constants");
var types_1 = require("./types");
var RetryProvider = (function (_super) {
    tslib_1.__extends(RetryProvider, _super);
    function RetryProvider(params, chainId, nodeQuorumThreshold, retries, delay, maxConcurrency, providerCacheNamespace, pctRpcCallsLogged, redisClient, standardTtlBlockDistance, noTtlBlockDistance, providerCacheTtl, logger) {
        if (providerCacheTtl === void 0) { providerCacheTtl = constants_2.PROVIDER_CACHE_TTL; }
        var _this = _super.call(this, undefined, chainId) || this;
        _this.nodeQuorumThreshold = nodeQuorumThreshold;
        _this.retries = retries;
        _this.delay = delay;
        _this.maxConcurrency = maxConcurrency;
        _this.logger = logger;
        _this.providers = params.map(function (inputs) {
            return new (cachedProvider_1.CacheProvider.bind.apply(cachedProvider_1.CacheProvider, tslib_1.__spreadArray([void 0, providerCacheNamespace,
                redisClient,
                standardTtlBlockDistance,
                noTtlBlockDistance,
                providerCacheTtl,
                maxConcurrency,
                pctRpcCallsLogged,
                logger], inputs, false)))();
        });
        if (chainId !== constants_1.CHAIN_IDs.MAINNET) {
            _this.pollingInterval = 1000;
            _this.providers.forEach(function (provider) { return (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 ".concat(_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 ".concat(_this.retries));
        }
        if (_this.delay < 0) {
            throw new Error("delay cannot be < 0. Currently set to ".concat(_this.delay));
        }
        if (_this.nodeQuorumThreshold > _this.providers.length) {
            throw new Error("nodeQuorumThreshold (".concat(_this.nodeQuorumThreshold, ") must be <= the number of providers (").concat(_this.providers.length, ")"));
        }
        return _this;
    }
    RetryProvider.prototype.send = function (method, params) {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var quorumThreshold, requiredProviders, fallbackProviders, errors, tryWithFallback, results, errorTexts, successfulProviderUrls, values, getMismatchedProviders, logQuorumMismatchOrFailureDetails, throwQuorumError, fallbackResults, fallbackValues, counts, _b, quorumResult, count, mismatchedProviders, quorumProviders;
            var _this = this;
            return tslib_1.__generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        quorumThreshold = this._getQuorum(method, params);
                        requiredProviders = this.providers.slice(0, quorumThreshold);
                        fallbackProviders = this.providers.slice(quorumThreshold);
                        errors = [];
                        tryWithFallback = function (provider) {
                            return _this._trySend(provider, method, params)
                                .then(function (result) { return [provider, result]; })
                                .catch(function (err) {
                                errors.push([provider, (err === null || err === void 0 ? void 0 : err.stack) || (err === null || err === void 0 ? void 0 : err.toString())]);
                                if (fallbackProviders.length === 0) {
                                    throw err;
                                }
                                if (quorumThreshold === 1 && _this.failImmediate(method, err)) {
                                    throw err;
                                }
                                return tryWithFallback(fallbackProviders.shift());
                            });
                        };
                        return [4, Promise.allSettled(requiredProviders.map(tryWithFallback))];
                    case 1:
                        results = _c.sent();
                        if (!results.every(utils_1.isPromiseFulfilled)) {
                            errorTexts = errors.map(function (_a) {
                                var provider = _a[0], errorText = _a[1];
                                return (0, utils_2.formatProviderError)(provider, errorText);
                            });
                            successfulProviderUrls = results.filter(utils_1.isPromiseFulfilled).map(function (result) { return result.value[0].connection.url; });
                            throw (0, utils_2.createSendErrorWithMessage)("Not enough providers succeeded. Errors:\n".concat(errorTexts.join("\n"), "\n") +
                                "Successful Providers:\n".concat(successfulProviderUrls.join("\n")), (_a = results.find(utils_1.isPromiseRejected)) === null || _a === void 0 ? void 0 : _a.reason);
                        }
                        values = results.map(function (result) { return result.value; });
                        if (values.slice(1).every(function (_a) {
                            var output = _a[1];
                            return (0, utils_2.compareRpcResults)(method, values[0][1], output);
                        })) {
                            return [2, values[0][1]];
                        }
                        getMismatchedProviders = function (values) {
                            return values
                                .filter(function (_a) {
                                var result = _a[1];
                                return !(0, utils_2.compareRpcResults)(method, result, quorumResult);
                            })
                                .map(function (_a) {
                                var provider = _a[0];
                                return provider.connection.url;
                            });
                        };
                        logQuorumMismatchOrFailureDetails = function (method, params, quorumProviders, mismatchedProviders, errors) {
                            var _a;
                            (_a = _this.logger) === null || _a === void 0 ? void 0 : _a.warn({
                                at: "ProviderUtils",
                                message: "Some providers mismatched with the quorum result or failed 🚸",
                                notificationPath: "across-warn",
                                method: method,
                                params: JSON.stringify(params),
                                quorumProviders: quorumProviders,
                                mismatchedProviders: mismatchedProviders,
                                erroringProviders: errors.map(function (_a) {
                                    var provider = _a[0], errorText = _a[1];
                                    return (0, utils_2.formatProviderError)(provider, errorText);
                                }),
                            });
                        };
                        throwQuorumError = function (fallbackValues) {
                            var errorTexts = errors.map(function (_a) {
                                var provider = _a[0], errorText = _a[1];
                                return (0, utils_2.formatProviderError)(provider, errorText);
                            });
                            var successfulProviderUrls = values.map(function (_a) {
                                var provider = _a[0];
                                return provider.connection.url;
                            });
                            var mismatchedProviders = getMismatchedProviders(tslib_1.__spreadArray(tslib_1.__spreadArray([], values, true), (fallbackValues || []), true));
                            logQuorumMismatchOrFailureDetails(method, params, successfulProviderUrls, mismatchedProviders, errors);
                            throw new Error("Not enough providers agreed to meet quorum.\n" +
                                "Providers that errored:\n" +
                                "".concat(errorTexts.join("\n"), "\n") +
                                "Providers that succeeded, but some failed to match:\n" +
                                successfulProviderUrls.join("\n"));
                        };
                        if (fallbackProviders.length === 0) {
                            throwQuorumError();
                        }
                        return [4, Promise.allSettled(fallbackProviders.map(function (provider) {
                                return _this._trySend(provider, method, params)
                                    .then(function (result) { return [provider, result]; })
                                    .catch(function (err) {
                                    errors.push([provider, (err === null || err === void 0 ? void 0 : err.stack) || (err === null || err === void 0 ? void 0 : err.toString())]);
                                    throw new Error("No fallbacks during quorum search");
                                });
                            }))];
                    case 2:
                        fallbackResults = _c.sent();
                        fallbackValues = fallbackResults.filter(utils_1.isPromiseFulfilled).map(function (promise) { return promise.value; });
                        counts = tslib_1.__spreadArray(tslib_1.__spreadArray([], values, true), fallbackValues, true).reduce(function (acc, curr) {
                            var result = curr[1];
                            var existingMatch = acc.find(function (_a) {
                                var existingResult = _a[0];
                                return (0, utils_2.compareRpcResults)(method, existingResult, result);
                            });
                            if (existingMatch) {
                                existingMatch[1]++;
                            }
                            else {
                                acc.push([result, 1]);
                            }
                            return acc;
                        }, [[undefined, 0]]);
                        counts.sort(function (_a, _b) {
                            var a = _a[1];
                            var b = _b[1];
                            return b - a;
                        });
                        _b = counts[0], quorumResult = _b[0], count = _b[1];
                        if (count < quorumThreshold) {
                            throwQuorumError(fallbackValues);
                        }
                        mismatchedProviders = getMismatchedProviders(tslib_1.__spreadArray(tslib_1.__spreadArray([], values, true), fallbackValues, true));
                        quorumProviders = tslib_1.__spreadArray(tslib_1.__spreadArray([], values, true), fallbackValues, true).filter(function (_a) {
                            var result = _a[1];
                            return (0, utils_2.compareRpcResults)(method, result, quorumResult);
                        })
                            .map(function (_a) {
                            var provider = _a[0];
                            return provider.connection.url;
                        });
                        if (mismatchedProviders.length > 0 || errors.length > 0) {
                            logQuorumMismatchOrFailureDetails(method, params, quorumProviders, mismatchedProviders, errors);
                        }
                        return [2, quorumResult];
                }
            });
        });
    };
    RetryProvider.prototype._validateResponse = function (method, _, response) {
        return (0, utils_1.isDefined)(response) || method === "eth_getTransactionReceipt";
    };
    RetryProvider.prototype._sendAndValidate = function (provider, method, params) {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var response;
            return tslib_1.__generator(this, function (_b) {
                switch (_b.label) {
                    case 0: return [4, provider.send(method, params)];
                    case 1:
                        response = _b.sent();
                        if (!this._validateResponse(method, params, response)) {
                            (_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug({
                                at: "ProviderUtils",
                                message: "Provider returned invalid response",
                                provider: (0, NetworkUtils_1.getOriginFromURL)(provider.connection.url),
                                method: method,
                                params: params,
                                response: response,
                            });
                            throw new Error("Response failed validation");
                        }
                        return [2, response];
                }
            });
        });
    };
    RetryProvider.prototype.parseError = function (response) {
        if (!types_1.RpcError.is(response)) {
            return;
        }
        try {
            var error = JSON.parse(response.body);
            if (!types_1.JsonRpcError.is(error)) {
                return;
            }
            return error.error;
        }
        catch (_a) {
            return;
        }
    };
    RetryProvider.prototype.failImmediate = function (method, response) {
        var err = this.parseError(response);
        if (!err) {
            return false;
        }
        if (err.code >= -32768 && err.code <= -32100) {
            return false;
        }
        var message = err.message.toLowerCase();
        switch (method) {
            case "eth_call":
            case "eth_estimateGas":
                return message.includes("revert");
            case "eth_sendRawTransaction":
                return message.includes("nonce") || message.includes("underpriced");
            default:
                break;
        }
        return false;
    };
    RetryProvider.prototype._trySend = function (provider, method, params) {
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var retries, settled;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        retries = this.retries;
                        _a.label = 1;
                    case 1:
                        if (!true) return [3, 4];
                        return [4, Promise.allSettled([this._sendAndValidate(provider, method, params)])];
                    case 2:
                        settled = (_a.sent())[0];
                        if (settled.status === "fulfilled") {
                            return [2, settled.value];
                        }
                        if (retries-- <= 0 || this.failImmediate(method, settled.reason)) {
                            throw settled.reason;
                        }
                        return [4, (0, utils_1.delay)(this.delay)];
                    case 3:
                        _a.sent();
                        return [3, 1];
                    case 4: return [2];
                }
            });
        });
    };
    RetryProvider.prototype._getQuorum = function (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;
    };
    return RetryProvider;
}(ethers_1.ethers.providers.StaticJsonRpcProvider));
exports.RetryProvider = RetryProvider;
//# sourceMappingURL=retryProvider.js.map