"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QuorumFallbackSolanaRpcFactory = void 0;
const TypeGuards_1 = require("../../utils/TypeGuards");
const utils_1 = require("../utils");
const cachedRpcFactory_1 = require("./cachedRpcFactory");
const baseRpcFactories_1 = require("./baseRpcFactories");
const utils_2 = require("./utils");
class QuorumFallbackSolanaRpcFactory extends baseRpcFactories_1.SolanaBaseRpcFactory {
    nodeQuorumThreshold;
    logger;
    rpcFactories = [];
    constructor(factoryConstructorParams, nodeQuorumThreshold, logger) {
        super();
        this.nodeQuorumThreshold = nodeQuorumThreshold;
        this.logger = logger;
        factoryConstructorParams.forEach((params) => {
            const rpcFactory = new cachedRpcFactory_1.CachedSolanaRpcFactory(...params);
            this.rpcFactories.push({
                transport: rpcFactory.createTransport(),
                rpcClient: rpcFactory.createRpcClient(),
                rpcFactory,
            });
        });
        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}`);
        }
    }
    createTransport() {
        return async (...args) => {
            const { method, params } = args[0].payload;
            const quorumThreshold = this._getQuorum(method, params ?? []);
            const requiredFactories = this.rpcFactories.slice(0, quorumThreshold);
            const fallbackFactories = [...this.rpcFactories.slice(quorumThreshold)];
            const errors = [];
            const tryWithFallback = (factory, ...args) => {
                return factory
                    .transport(...args)
                    .then((result) => [factory.rpcFactory, result])
                    .catch((error) => {
                    errors.push([factory.rpcFactory, error?.stack || error?.toString()]);
                    if (fallbackFactories.length === 0) {
                        throw error;
                    }
                    if (quorumThreshold === 1 && (0, utils_2.shouldFailImmediate)(method, error)) {
                        throw new Error(`RPC provider reverted for method ${method}`);
                    }
                    const currentFactory = factory.rpcFactory.clusterUrl;
                    const nextFactory = fallbackFactories.shift();
                    this.logger.debug({
                        at: "FallbackSolanaRpcFactory#createTransport::tryWithFallback",
                        message: `[${method}] ${currentFactory} failed, falling back to ${nextFactory.rpcFactory.clusterUrl}, new fallback providers length: ${fallbackFactories.length}`,
                        method,
                        jsonError: error,
                    });
                    return tryWithFallback(nextFactory, ...args);
                });
            };
            const results = await Promise.allSettled(requiredFactories.map((factory) => {
                return tryWithFallback(factory, ...args);
            }));
            const getErrorStrings = () => {
                return errors.map(([factory, errorText]) => `Provider ${factory.clusterUrl} failed to call ${method} with error ${errorText}`);
            };
            if (!results.every(TypeGuards_1.isPromiseFulfilled)) {
                const errorTexts = getErrorStrings();
                const successfulProviderUrls = results.filter(TypeGuards_1.isPromiseFulfilled).map((result) => result.value[0].clusterUrl);
                throw (0, utils_1.createSendErrorWithMessage)(`Not enough providers succeeded on ${method} call. Errors:\n${errorTexts.join("\n")}\n` +
                    `Successful Providers:\n${successfulProviderUrls.join("\n")}`, results.find(TypeGuards_1.isPromiseRejected)?.reason);
            }
            const values = results.map((result) => result.value);
            if (values.slice(1).every(([, output]) => (0, utils_1.compareSvmRpcResults)(method, values[0][1], output))) {
                return values[0][1];
            }
            const getHighestCountResult = (values) => {
                const counts = [...values].reduce((acc, curr) => {
                    const [, result] = curr;
                    const existingMatch = acc.find(([existingResult]) => (0, utils_1.compareSvmRpcResults)(method, existingResult, result));
                    if (existingMatch) {
                        existingMatch[1]++;
                    }
                    else {
                        acc.push([result, 1]);
                    }
                    return acc;
                }, [[undefined, 0]]);
                counts.sort(([, a], [, b]) => b - a);
                const [mostFrequentResult, count] = counts[0];
                return [mostFrequentResult, count];
            };
            const logQuorumMismatchOrFailureDetails = (method, params, mismatchedProviders, successfulProviders, errors, quorumResult) => {
                this.logger.warn({
                    at: "FallbackSolanaRpcFactory#createTransport",
                    message: `[${method}] Some providers mismatched with the quorum result or failed 🚸`,
                    notificationPath: "across-warn",
                    method,
                    params: JSON.stringify(params),
                    quorumResult: METHODS_RETURNING_BIGINT.includes(method) ? Number(quorumResult) : undefined,
                    mismatchedProviders,
                    successfulProviders,
                    erroringProviders: errors.map(([factory, errorText]) => `Provider ${factory.clusterUrl} failed with error ${errorText}`),
                });
            };
            const throwQuorumError = (mostFrequentResult, allValues) => {
                const errorTexts = getErrorStrings();
                const successfulProviderUrls = values.map(([provider]) => provider.clusterUrl);
                const mismatchedProviders = allValues
                    .filter(([, result]) => !(0, utils_1.compareSvmRpcResults)(method, result, mostFrequentResult))
                    .map(([factory]) => factory.clusterUrl);
                logQuorumMismatchOrFailureDetails(method, params ?? [], mismatchedProviders, successfulProviderUrls, errors, mostFrequentResult);
                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 (fallbackFactories.length === 0) {
                const [mostFrequentResult] = getHighestCountResult(values);
                throwQuorumError(mostFrequentResult, values);
            }
            const fallbackResults = await Promise.allSettled(fallbackFactories.map((factory) => {
                return factory
                    .transport(...args)
                    .then((result) => [factory.rpcFactory, result])
                    .catch((err) => {
                    errors.push([factory.rpcFactory, err?.stack || err?.toString()]);
                    throw new Error("Fallback RPC call failed while trying to reach quorum");
                });
            }));
            const fallbackValues = fallbackResults.filter(TypeGuards_1.isPromiseFulfilled).map((promise) => promise.value);
            const [quorumResult, count] = getHighestCountResult([...values, ...fallbackValues]);
            if (count < quorumThreshold) {
                throwQuorumError(quorumResult, [...values, ...fallbackValues]);
            }
            const mismatchedProviders = [...values, ...fallbackValues]
                .filter(([, result]) => !(0, utils_1.compareSvmRpcResults)(method, result, quorumResult))
                .map(([factory]) => factory.clusterUrl);
            const successfulProviderUrls = [...values, ...fallbackValues].map(([provider]) => provider.clusterUrl);
            if (mismatchedProviders.length > 0 || errors.length > 0) {
                logQuorumMismatchOrFailureDetails(method, params ?? [], mismatchedProviders, successfulProviderUrls, errors, quorumResult);
            }
            return quorumResult;
        };
    }
    _getQuorum(method, _params) {
        switch (method) {
            case "getBlock":
            case "getBlockTime":
                return this.nodeQuorumThreshold;
        }
        return 1;
    }
}
exports.QuorumFallbackSolanaRpcFactory = QuorumFallbackSolanaRpcFactory;
const METHODS_RETURNING_BIGINT = ["getBlockTime", "getSlot"];
//# sourceMappingURL=quorumFallbackRpcFactory.js.map