import { __awaiter, __extends, __generator, __spreadArray } from "tslib";
import { ethers } from "ethers";
import { CHAIN_IDs } from "../constants";
import { delay, isDefined, isPromiseFulfilled, isPromiseRejected } from "../utils";
import { getOriginFromURL } from "../utils/NetworkUtils";
import { CacheProvider } from "./cachedProvider";
import { compareRpcResults, createSendErrorWithMessage, formatProviderError } from "./utils";
import { PROVIDER_CACHE_TTL } from "./constants";
import { JsonRpcError, RpcError } from "./types";
var RetryProvider = /** @class */ (function (_super) {
    __extends(RetryProvider, _super);
    function RetryProvider(params, chainId, nodeQuorumThreshold, retries, delay, maxConcurrency, providerCacheNamespace, pctRpcCallsLogged, redisClient, standardTtlBlockDistance, noTtlBlockDistance, providerCacheTtl, logger) {
        if (providerCacheTtl === void 0) { providerCacheTtl = PROVIDER_CACHE_TTL; }
        var _this = 
        // Initialize the super just with the chainId, which stops it from trying to immediately send out a .send before
        // this derived class is initialized.
        _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 (CacheProvider.bind.apply(CacheProvider, __spreadArray([void 0, providerCacheNamespace,
                redisClient,
                standardTtlBlockDistance,
                noTtlBlockDistance,
                providerCacheTtl,
                maxConcurrency,
                pctRpcCallsLogged,
                logger], inputs, false)))();
        });
        if (chainId !== 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 __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 __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) {
                                // Append the provider and error to the error array.
                                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                                errors.push([provider, (err === null || err === void 0 ? void 0 : err.stack) || (err === null || err === void 0 ? void 0 : err.toString())]);
                                // If there are no new fallback providers to use, terminate the recursion by throwing an error.
                                // Otherwise, we can try to call another provider.
                                if (fallbackProviders.length === 0) {
                                    throw err;
                                }
                                // If one RPC provider reverted, others likely will too. Skip them.
                                if (quorumThreshold === 1 && _this.failImmediate(method, err)) {
                                    throw err;
                                }
                                // This line does two things:
                                // 1. Removes a fallback provider from the array so it cannot be used as a fallback for another required
                                // provider.
                                // 2. Recursively calls this method with that provider so it goes through the same try logic as the previous one.
                                return tryWithFallback(fallbackProviders.shift());
                            });
                        };
                        return [4 /*yield*/, Promise.allSettled(requiredProviders.map(tryWithFallback))];
                    case 1:
                        results = _c.sent();
                        if (!results.every(isPromiseFulfilled)) {
                            errorTexts = errors.map(function (_a) {
                                var provider = _a[0], errorText = _a[1];
                                return formatProviderError(provider, errorText);
                            });
                            successfulProviderUrls = results.filter(isPromiseFulfilled).map(function (result) { return result.value[0].connection.url; });
                            throw createSendErrorWithMessage("Not enough providers succeeded. Errors:\n".concat(errorTexts.join("\n"), "\n") +
                                "Successful Providers:\n".concat(successfulProviderUrls.join("\n")), (_a = results.find(isPromiseRejected)) === null || _a === void 0 ? void 0 : _a.reason);
                        }
                        values = results.map(function (result) { return result.value; });
                        // Start at element 1 and begin comparing.
                        // If _all_ values are equal, we have hit quorum, so return.
                        if (values.slice(1).every(function (_a) {
                            var output = _a[1];
                            return compareRpcResults(method, values[0][1], output);
                        })) {
                            return [2 /*return*/, values[0][1]];
                        }
                        getMismatchedProviders = function (values) {
                            return values
                                .filter(function (_a) {
                                var result = _a[1];
                                return !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 formatProviderError(provider, errorText);
                                }),
                            });
                        };
                        throwQuorumError = function (fallbackValues) {
                            var errorTexts = errors.map(function (_a) {
                                var provider = _a[0], errorText = _a[1];
                                return formatProviderError(provider, errorText);
                            });
                            var successfulProviderUrls = values.map(function (_a) {
                                var provider = _a[0];
                                return provider.connection.url;
                            });
                            var mismatchedProviders = getMismatchedProviders(__spreadArray(__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"));
                        };
                        // Exit early if there are no fallback providers left.
                        if (fallbackProviders.length === 0) {
                            throwQuorumError();
                        }
                        return [4 /*yield*/, 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(isPromiseFulfilled).map(function (promise) { return promise.value; });
                        counts = __spreadArray(__spreadArray([], values, true), fallbackValues, true).reduce(function (acc, curr) {
                            var result = curr[1];
                            // Find the first result that matches the return value.
                            var existingMatch = acc.find(function (_a) {
                                var existingResult = _a[0];
                                return compareRpcResults(method, existingResult, result);
                            });
                            // Increment the count if a match is found, else add a new element to the match array with a count of 1.
                            if (existingMatch) {
                                existingMatch[1]++;
                            }
                            else {
                                acc.push([result, 1]);
                            }
                            // Return the same acc object because it was modified in place.
                            return acc;
                        }, [[undefined, 0]] // Initialize with [undefined, 0] as the first element so something is always returned.
                        );
                        // Sort so the result with the highest count is first.
                        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 this count is less than we need for quorum, throw the quorum error.
                        if (count < quorumThreshold) {
                            throwQuorumError(fallbackValues);
                        }
                        mismatchedProviders = getMismatchedProviders(__spreadArray(__spreadArray([], values, true), fallbackValues, true));
                        quorumProviders = __spreadArray(__spreadArray([], values, true), fallbackValues, true).filter(function (_a) {
                            var result = _a[1];
                            return 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 /*return*/, quorumResult];
                }
            });
        });
    };
    RetryProvider.prototype._validateResponse = function (method, _, response) {
        // Basic validation logic to start.
        // Note: eth_getTransactionReceipt is ignored here because null responses are expected in the case that ethers is
        // polling for the transaction receipt and receiving null until it does.
        return isDefined(response) || method === "eth_getTransactionReceipt";
    };
    RetryProvider.prototype._sendAndValidate = function (provider, method, params) {
        var _a;
        return __awaiter(this, void 0, void 0, function () {
            var response;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0: return [4 /*yield*/, provider.send(method, params)];
                    case 1:
                        response = _b.sent();
                        if (!this._validateResponse(method, params, response)) {
                            // Not a warning to avoid spam since this could trigger a lot.
                            (_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug({
                                at: "ProviderUtils",
                                message: "Provider returned invalid response",
                                provider: getOriginFromURL(provider.connection.url),
                                method: method,
                                params: params,
                                response: response,
                            });
                            throw new Error("Response failed validation");
                        }
                        return [2 /*return*/, response];
                }
            });
        });
    };
    /**
     * Validate and parse a possible JSON-RPC error response.
     * @param error An unknown error object received in response to a JSON-RPC request.
     * @returns A JSON-RPC error object, or undefined.
     */
    RetryProvider.prototype.parseError = function (response) {
        if (!RpcError.is(response)) {
            return;
        }
        try {
            var error = JSON.parse(response.body);
            if (!JsonRpcError.is(error)) {
                return;
            }
            return error.error;
        }
        catch (_a) {
            return;
        }
    };
    /**
     * Determine whether a JSON-RPC error response indicates an unrecoverable error.
     * @param method JSON-RPC method that produced the error.
     * @param error JSON-RPC error instance.
     * @returns True if the request should be aborted immediately, otherwise false.
     */
    RetryProvider.prototype.failImmediate = function (method, response) {
        var err = this.parseError(response);
        if (!err) {
            return false; // Not a JSON-RPC error.
        }
        // [-32768, -32100] is reserved by the JSON-RPC spec.
        // [-32099, -32000] is allocated for implementation-defined responses.
        // Everything else is available for use by the application space.
        // Most node implementations return 3 for an eth_call revert, but some return -32000.
        // See also https://www.jsonrpc.org/specification
        if (err.code >= -32768 && err.code <= -32100) {
            return false; // Cannot handle these errors.
        }
        // The `data` member of err _may_ be populated but would need to be verified.
        var message = err.message.toLowerCase();
        switch (method) {
            case "eth_call":
            case "eth_estimateGas":
                return message.includes("revert"); // Transaction will fail.
            case "eth_sendRawTransaction":
                // Nonce too low or gas price is too low.
                return message.includes("nonce") || message.includes("underpriced");
            default:
                break;
        }
        return false;
    };
    RetryProvider.prototype._trySend = function (provider, method, params) {
        return __awaiter(this, void 0, void 0, function () {
            var retries, settled;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        retries = this.retries;
                        _a.label = 1;
                    case 1:
                        if (!true) return [3 /*break*/, 4];
                        return [4 /*yield*/, Promise.allSettled([this._sendAndValidate(provider, method, params)])];
                    case 2:
                        settled = (_a.sent())[0];
                        if (settled.status === "fulfilled") {
                            return [2 /*return*/, settled.value];
                        }
                        if (retries-- <= 0 || this.failImmediate(method, settled.reason)) {
                            throw settled.reason;
                        }
                        return [4 /*yield*/, delay(this.delay)];
                    case 3:
                        _a.sent();
                        return [3 /*break*/, 1];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    RetryProvider.prototype._getQuorum = function (method, params) {
        // Only use quorum if this is a historical query that doesn't depend on the current block number.
        // All logs queries should use quorum.
        if (method === "eth_getLogs") {
            return this.nodeQuorumThreshold;
        }
        // getBlockByNumber should only use the quorum if it's not asking for the latest block.
        if (method === "eth_getBlockByNumber" && params[0] !== "latest" && params[0] !== "pending") {
            return this.nodeQuorumThreshold;
        }
        // eth_call should only use quorum for queries at a specific past block.
        if (method === "eth_call" && params[1] !== "latest") {
            return this.nodeQuorumThreshold;
        }
        // All other calls should use quorum 1 to avoid errors due to sync differences.
        return 1;
    };
    return RetryProvider;
}(ethers.providers.StaticJsonRpcProvider));
export { RetryProvider };
//# sourceMappingURL=retryProvider.js.map