import { __assign, __awaiter, __generator } from "tslib";
import assert from "assert";
import { DEFAULT_SIMULATED_RELAYER_ADDRESS, DEFAULT_SIMULATED_RELAYER_ADDRESS_SVM, TOKEN_SYMBOLS_MAP, } from "../constants";
import { MAX_BIG_INT, bnZero, fixedPointAdjustment, getTokenInfo, isDefined, max, min, nativeToToken, percent, toBN, toBNWei, compareAddressesSimple, ConvertDecimals, chainIsSvm, EvmAddress, SvmAddress, toAddressType, } from "../utils";
export var expectedCapitalCostsKeys = ["lowerBound", "upperBound", "cutoff", "decimals"];
export var DEFAULT_LOGGER = {
    debug: function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        return console.debug(args);
    },
    info: function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        return console.info(args);
    },
    warn: function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        return console.warn(args);
    },
    error: function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        return console.error(args);
    },
};
export function getDefaultRelayer(chainId) {
    return isDefined(chainId) && chainIsSvm(chainId)
        ? SvmAddress.from(DEFAULT_SIMULATED_RELAYER_ADDRESS_SVM)
        : EvmAddress.from(DEFAULT_SIMULATED_RELAYER_ADDRESS);
}
// Small amount to simulate filling with. Should be low enough to guarantee a successful fill.
var safeOutputAmount = toBN(100);
var RelayFeeCalculator = /** @class */ (function () {
    function RelayFeeCalculator(config, logger, destinationChainId) {
        assert(config, "config must be provided");
        if ("queries" in config) {
            this.queries = config.queries;
        }
        else {
            assert(destinationChainId !== undefined, "destinationChainId must be provided if queriesMap is provided");
            assert(config.queriesMap[destinationChainId], "No queries provided for destination chain");
            this.queries = config.queriesMap[destinationChainId];
        }
        this.gasDiscountPercent = config.gasDiscountPercent || 0;
        this.capitalDiscountPercent = config.capitalDiscountPercent || 0;
        this.feeLimitPercent = config.feeLimitPercent || 0;
        this.nativeTokenDecimals = config.nativeTokenDecimals || 18;
        assert(this.gasDiscountPercent >= 0 && this.gasDiscountPercent <= 100, "gasDiscountPercent must be between 0 and 100 percent");
        assert(this.capitalDiscountPercent >= 0 && this.capitalDiscountPercent <= 100, "capitalDiscountPercent must be between 0 and 100 percent");
        assert(this.feeLimitPercent >= 0 && this.feeLimitPercent <= 100, "feeLimitPercent must be between 0 and 100 percent");
        this.capitalCostsConfig = Object.fromEntries(Object.entries(config.capitalCostsConfig).map(function (_a) {
            var token = _a[0], capitalCosts = _a[1];
            return [token.toUpperCase(), RelayFeeCalculator.validateAndTransformCapitalCostsConfigOverride(capitalCosts)];
        }));
        assert(Object.keys(this.capitalCostsConfig).length > 0, "capitalCostsConfig must have at least one entry");
        this.logger = logger || DEFAULT_LOGGER;
        // warn developer of any possible conflicting config overrides
        this.checkAllConfigConflicts();
    }
    /**
     * Type guard to check if a config is a CapitalCostConfigOverride or a CapitalCostConfig.
     * @param config CapitalCostConfig or CapitalCostConfigOverride
     * @returns true if the config is a CapitalCostConfigOverride, false otherwise.
     * @private
     * @dev This is a type guard that is used to check if a config is a CapitalCostConfigOverride or a CapitalCostConfig.
     * This is needed because the config can be either a CapitalCostConfig or a CapitalCostConfigOverride. If it's a
     * CapitalCostConfig, then we need to convert it to a CapitalCostConfigOverride with the default config set with no route
     * overrides.
     */
    RelayFeeCalculator.capitalCostConfigIsOverride = function (config) {
        return config.default !== undefined;
    };
    /**
     * Validates a CapitalCostConfigOverride or a CapitalCostConfig.
     * @param capitalCosts CapitalCostConfig or CapitalCostConfigOverride
     * @returns CapitalCostConfigOverride
     */
    RelayFeeCalculator.validateAndTransformCapitalCostsConfigOverride = function (capitalCosts) {
        // We need to first convert the config to a baseline type. This is because the config can be either a CapitalCostConfig
        // or a CapitalCostConfigOverride. If it's a CapitalCostConfig, then we need to convert it to a CapitalCostConfigOverride with
        // the default config set with no route overrides.
        var config = this.capitalCostConfigIsOverride(capitalCosts)
            ? capitalCosts
            : { default: capitalCosts };
        // Validate the default config.
        this.validateCapitalCostsConfig(config.default);
        // Iterate over all the route overrides and validate them.
        for (var _i = 0, _a = Object.values(config.routeOverrides || {}); _i < _a.length; _i++) {
            var toChainIdRoutes = _a[_i];
            Object.values(toChainIdRoutes).forEach(this.validateCapitalCostsConfig);
        }
        // Validate origin chain overrides
        Object.values(config.originChainOverrides || {}).forEach(this.validateCapitalCostsConfig);
        // Validate destination chain overrides
        Object.values(config.destinationChainOverrides || {}).forEach(this.validateCapitalCostsConfig);
        return config;
    };
    /**
     * Validates a CapitalCostConfig.
     * @param capitalCosts CapitalCostConfig
     */
    RelayFeeCalculator.validateCapitalCostsConfig = function (capitalCosts) {
        assert(toBN(capitalCosts.upperBound).lt(toBNWei("1")), "upper bound must be < 100%");
        assert(capitalCosts.decimals > 0 && capitalCosts.decimals <= 18, "invalid decimals");
    };
    RelayFeeCalculator.prototype.getTokenPrice = function (tokenSymbol) {
        return this.queries.getTokenPrice(tokenSymbol);
    };
    /**
     * Calculate the gas fee as a % of the amount to relay.
     * @param deposit A valid deposit object to reason about
     * @param amountToRelay The amount that we should fill the deposit for
     * @param simulateZeroFill Whether to simulate a zero fill for the gas cost simulation
     *        A fill of 1 wei which would result in a slow/partial fill.
     *        You should do this if you're not worried about simulating a proper fill of a deposit
     *        with a message or if you are worried a fill amount that could exceed the balance of
     *        the relayer.
     * @param relayerAddress The relayer that will be used for the gas cost simulation
     * @param _tokenPrice The token price for normalizing fees
     * @returns The fee as a % of the amount to relay.
     * @note Setting simulateZeroFill to true will result on the gas costs being estimated
     *       on a zero fill. However, the percentage will be returned as a percentage of the
     *       amount to relay. This is useful for determining the maximum gas fee % that a
     *       relayer may need to make on a regular fill. You will get differing results if
     *       a message & recipient contract is provided as this function may not simulate with
     *       the correct parameters to see a full fill.
     */
    RelayFeeCalculator.prototype.gasFeePercent = function (deposit, outputAmount, simulateZeroFill, relayerAddress, _tokenPrice, tokenMapping, gasPrice, gasLimit, _tokenGasCost, transport) {
        if (simulateZeroFill === void 0) { simulateZeroFill = false; }
        if (relayerAddress === void 0) { relayerAddress = getDefaultRelayer(deposit.destinationChainId); }
        if (tokenMapping === void 0) { tokenMapping = TOKEN_SYMBOLS_MAP; }
        return __awaiter(this, void 0, void 0, function () {
            var inputToken, destinationChainId, originChainId, destinationChainTokenDetails, outputToken, outputTokenInfo, inputTokenInfo, simulatedAmount, getGasCosts, _a, tokenGasCost, tokenPrice, gasFeesInToken;
            var _this = this;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        if (toBN(outputAmount).eq(bnZero))
                            return [2 /*return*/, MAX_BIG_INT];
                        inputToken = deposit.inputToken, destinationChainId = deposit.destinationChainId, originChainId = deposit.originChainId;
                        destinationChainTokenDetails = Object.values(tokenMapping).find(function (details) {
                            return compareAddressesSimple(details.addresses[originChainId], inputToken.toNative()) &&
                                isDefined(details.addresses[destinationChainId]);
                        });
                        outputToken = deposit.outputToken.isZeroAddress()
                            ? toAddressType(destinationChainTokenDetails.addresses[destinationChainId], destinationChainId)
                            : deposit.outputToken;
                        outputTokenInfo = getTokenInfo(outputToken, destinationChainId, tokenMapping);
                        inputTokenInfo = getTokenInfo(inputToken, originChainId, tokenMapping);
                        if (!isDefined(outputTokenInfo) || !isDefined(inputTokenInfo)) {
                            throw new Error("Could not find token information for ".concat(inputToken, " or ").concat(outputToken));
                        }
                        simulatedAmount = simulateZeroFill ? safeOutputAmount : toBN(outputAmount);
                        deposit = __assign(__assign({}, deposit), { outputAmount: simulatedAmount });
                        getGasCosts = this.queries
                            .getGasCosts(deposit, relayerAddress, { gasPrice: gasPrice, gasUnits: gasLimit, transport: transport })
                            .then(function (_a) {
                            var tokenGasCost = _a.tokenGasCost;
                            return tokenGasCost;
                        })
                            .catch(function (error) {
                            _this.logger.error({
                                at: "sdk/gasFeePercent",
                                message: "Error while fetching gas costs",
                                error: error,
                                simulateZeroFill: simulateZeroFill,
                                deposit: deposit,
                            });
                            throw error;
                        });
                        return [4 /*yield*/, Promise.all([
                                _tokenGasCost ? Promise.resolve(_tokenGasCost) : getGasCosts,
                                _tokenPrice !== null && _tokenPrice !== void 0 ? _tokenPrice : this.queries.getTokenPrice(outputTokenInfo.symbol).catch(function (error) {
                                    _this.logger.error({
                                        at: "sdk/gasFeePercent",
                                        message: "Error while fetching token price",
                                        error: error,
                                        destinationChainId: deposit.destinationChainId,
                                        inputToken: inputToken,
                                    });
                                    throw error;
                                }),
                            ])];
                    case 1:
                        _a = _b.sent(), tokenGasCost = _a[0], tokenPrice = _a[1];
                        gasFeesInToken = nativeToToken(tokenGasCost, tokenPrice, outputTokenInfo.decimals, this.nativeTokenDecimals);
                        return [2 /*return*/, percent(gasFeesInToken, outputAmount.toString())];
                }
            });
        });
    };
    // Note: these variables are unused now, but may be needed in future versions of this function that are more complex.
    RelayFeeCalculator.prototype.capitalFeePercent = function (_outputAmount, _tokenSymbol, _originRoute, _destinationRoute) {
        var _a, _b, _c, _d, _e, _f;
        // If amount is 0, then the capital fee % should be the max 100%
        if (toBN(_outputAmount).eq(toBN(0)))
            return MAX_BIG_INT;
        // V0: Ensure that there is a capital fee available for the token.
        // If not, then we should throw an error because this is indicative
        // of a misconfiguration.
        var tokenCostConfig = this.capitalCostsConfig[_tokenSymbol.toUpperCase()];
        if (!isDefined(tokenCostConfig)) {
            this.logger.error({
                at: "sdk/capitalFeePercent",
                message: "No capital fee available for token ".concat(_tokenSymbol),
            });
            throw new Error("No capital cost config available for token ".concat(_tokenSymbol));
        }
        // V1: Charge fee that scales with size. This will charge a fee % based on a linear fee curve with a "kink" at a
        // cutoff in the same units as _amountToRelay. Before the kink, the fee % will increase linearly from a lower
        // bound to an upper bound. After the kink, the fee % increase will be fixed, and slowly approach the upper bound
        // for very large amount inputs.
        else {
            // Order of specificity (most specific to least specific):
            // 1. Route overrides (both origin and destination)
            // 2. Destination chain overrides
            // 3. Origin chain overrides
            // 4. Default config
            var routeOverride = (_b = (_a = tokenCostConfig === null || tokenCostConfig === void 0 ? void 0 : tokenCostConfig.routeOverrides) === null || _a === void 0 ? void 0 : _a[_originRoute || ""]) === null || _b === void 0 ? void 0 : _b[_destinationRoute || ""];
            var destinationChainOverride = (_c = tokenCostConfig === null || tokenCostConfig === void 0 ? void 0 : tokenCostConfig.destinationChainOverrides) === null || _c === void 0 ? void 0 : _c[_destinationRoute || ""];
            var originChainOverride = (_d = tokenCostConfig === null || tokenCostConfig === void 0 ? void 0 : tokenCostConfig.originChainOverrides) === null || _d === void 0 ? void 0 : _d[_originRoute || ""];
            var config = (_f = (_e = routeOverride !== null && routeOverride !== void 0 ? routeOverride : destinationChainOverride) !== null && _e !== void 0 ? _e : originChainOverride) !== null && _f !== void 0 ? _f : tokenCostConfig.default;
            // Check and log warnings for configuration conflicts
            this.warnIfConfigConflicts(_tokenSymbol, _originRoute || "", _destinationRoute || "", routeOverride, destinationChainOverride, originChainOverride);
            // Scale amount "y" to 18 decimals.
            var y = toBN(_outputAmount).mul(toBNWei("1", 18 - config.decimals));
            // At a minimum, the fee will be equal to lower bound fee * y
            var minCharge = toBN(config.lowerBound).mul(y).div(fixedPointAdjustment);
            // Charge an increasing marginal fee % up to min(cutoff, y). If y is very close to the cutoff, the fee %
            // will be equal to half the sum of (upper bound + lower bound).
            var yTriangle = min(config.cutoff, y);
            // triangleSlope is slope of fee curve from lower bound to upper bound. If cutoff is 0, slope is 0.
            // triangleCharge is interval of curve from 0 to y for curve = triangleSlope * y
            var triangleSlope = toBN(config.cutoff).eq(toBN(0))
                ? toBN(0)
                : toBN(config.upperBound).sub(config.lowerBound).mul(fixedPointAdjustment).div(config.cutoff);
            var triangleHeight = triangleSlope.mul(yTriangle).div(fixedPointAdjustment);
            var triangleCharge = triangleHeight.mul(yTriangle).div(toBNWei(2));
            // For any amounts above the cutoff, the marginal fee % will not increase but will be fixed at the upper bound
            // value.
            var yRemainder = max(toBN(0), y.sub(config.cutoff));
            var remainderCharge = yRemainder.mul(toBN(config.upperBound).sub(config.lowerBound)).div(fixedPointAdjustment);
            return minCharge.add(triangleCharge).add(remainderCharge).mul(fixedPointAdjustment).div(y);
        }
    };
    /**
     * Checks for configuration conflicts across all token symbols and their associated chain configurations.
     * This method examines the capital costs configuration for each token and identifies any overlapping
     * or conflicting configurations between route overrides, destination chain overrides, and origin chain overrides.
     * If conflicts are found, warnings will be logged via the warnIfConfigConflicts method.
     */
    RelayFeeCalculator.prototype.checkAllConfigConflicts = function () {
        var _a, _b, _c, _d;
        var _loop_1 = function (tokenSymbol, tokenConfig) {
            // Get all origin chains that have specific configurations
            var originChains = new Set(Object.keys(tokenConfig.originChainOverrides || {}));
            // Get all destination chains that have specific configurations
            var destChains = new Set(Object.keys(tokenConfig.destinationChainOverrides || {}));
            // Add all chains from route overrides
            if (tokenConfig.routeOverrides) {
                Object.keys(tokenConfig.routeOverrides).forEach(function (originChain) {
                    originChains.add(originChain);
                    Object.keys(tokenConfig.routeOverrides[originChain]).forEach(function (destChain) {
                        destChains.add(destChain);
                    });
                });
            }
            // If there are no specific chain configurations, just check the default case
            if (originChains.size === 0 && destChains.size === 0) {
                return "continue";
            }
            // Check for conflicts between all combinations of origin and destination chains
            for (var _g = 0, _h = Array.from(originChains); _g < _h.length; _g++) {
                var originChain = _h[_g];
                for (var _j = 0, _k = Array.from(destChains); _j < _k.length; _j++) {
                    var destChain = _k[_j];
                    var routeOverride = (_b = (_a = tokenConfig.routeOverrides) === null || _a === void 0 ? void 0 : _a[originChain]) === null || _b === void 0 ? void 0 : _b[destChain];
                    var destinationChainOverride = (_c = tokenConfig.destinationChainOverrides) === null || _c === void 0 ? void 0 : _c[destChain];
                    var originChainOverride = (_d = tokenConfig.originChainOverrides) === null || _d === void 0 ? void 0 : _d[originChain];
                    this_1.warnIfConfigConflicts(tokenSymbol, originChain, destChain, routeOverride, destinationChainOverride, originChainOverride);
                }
            }
        };
        var this_1 = this;
        for (var _i = 0, _e = Object.entries(this.capitalCostsConfig); _i < _e.length; _i++) {
            var _f = _e[_i], tokenSymbol = _f[0], tokenConfig = _f[1];
            _loop_1(tokenSymbol, tokenConfig);
        }
    };
    /**
     * Log a warning if multiple configuration types apply to the same route
     * @private
     */
    RelayFeeCalculator.prototype.warnIfConfigConflicts = function (tokenSymbol, originChain, destChain, routeOverride, destinationChainOverride, originChainOverride) {
        var overrideCount = [routeOverride, destinationChainOverride, originChainOverride].filter(Boolean).length;
        if (overrideCount > 1) {
            var configUsed = routeOverride
                ? "route override"
                : destinationChainOverride
                    ? "destination chain override"
                    : originChainOverride
                        ? "origin chain override"
                        : "default override";
            this.logger.warn({
                at: "RelayFeeCalculator",
                message: "Multiple configurations found for token ".concat(tokenSymbol, " from chain ").concat(originChain, " to chain ").concat(destChain),
                configUsed: configUsed,
                routeOverride: routeOverride,
                destinationChainOverride: destinationChainOverride,
                originChainOverride: originChainOverride,
            });
        }
    };
    /**
     * Retrieves the relayer fee details for a deposit.
     * @param deposit A valid deposit object to reason about
     * @param amountToRelay The amount that the relayer would simulate a fill for
     * @param simulateZeroFill Whether to simulate a zero fill for the gas cost simulation
     *       For simulateZeroFill: A fill of 1 wei which would result in a slow/partial fill.
     *       You should do this if you're not worried about simulating a proper fill of a deposit
     *       with a message or if you are worried a fill amount that could exceed the balance of
     *       the relayer.
     * @param relayerAddress The relayer that will be used for the gas cost simulation
     * @param _tokenPrice The token price for normalizing fees
     * @param gasPrice Optional gas price to use for the simulation
     * @param gasUnits Optional gas units to use for the simulation
     * @returns A resulting `RelayerFeeDetails` object
     */
    RelayFeeCalculator.prototype.relayerFeeDetails = function (deposit, outputAmount, simulateZeroFill, relayerAddress, _tokenPrice, gasPrice, gasUnits, tokenGasCost) {
        if (simulateZeroFill === void 0) { simulateZeroFill = false; }
        if (relayerAddress === void 0) { relayerAddress = getDefaultRelayer(deposit.destinationChainId); }
        return __awaiter(this, void 0, void 0, function () {
            var inputToken, originChainId, outputToken, destinationChainId, inputTokenInfo, outputTokenInfo, gasFeePercent, outToInDecimals, gasFeeTotal, capitalFeePercent, capitalFeeTotal, relayFeePercent, relayFeeTotal, maxGasFeePercent, minDeposit, isAmountTooLow;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        // If the amount to relay is not provided, then we
                        // should use the full deposit amount.
                        outputAmount !== null && outputAmount !== void 0 ? outputAmount : (outputAmount = deposit.outputAmount);
                        inputToken = deposit.inputToken, originChainId = deposit.originChainId, outputToken = deposit.outputToken, destinationChainId = deposit.destinationChainId;
                        inputTokenInfo = getTokenInfo(inputToken, originChainId);
                        outputTokenInfo = getTokenInfo(outputToken, destinationChainId);
                        if (!isDefined(inputTokenInfo) || !isDefined(outputTokenInfo)) {
                            throw new Error("Could not find token information for ".concat(inputToken, " or ").concat(outputToken));
                        }
                        return [4 /*yield*/, this.gasFeePercent(deposit, outputAmount, simulateZeroFill, relayerAddress, _tokenPrice, undefined, gasPrice, gasUnits, tokenGasCost)];
                    case 1:
                        gasFeePercent = _a.sent();
                        outToInDecimals = ConvertDecimals(outputTokenInfo.decimals, inputTokenInfo.decimals);
                        gasFeeTotal = gasFeePercent.mul(outToInDecimals(outputAmount.toString())).div(fixedPointAdjustment);
                        capitalFeePercent = this.capitalFeePercent(outputAmount, inputTokenInfo.symbol, deposit.originChainId.toString(), deposit.destinationChainId.toString());
                        capitalFeeTotal = capitalFeePercent.mul(outToInDecimals(outputAmount.toString())).div(fixedPointAdjustment);
                        relayFeePercent = gasFeePercent.add(capitalFeePercent);
                        relayFeeTotal = gasFeeTotal.add(capitalFeeTotal);
                        maxGasFeePercent = max(toBNWei(this.feeLimitPercent / 100).sub(capitalFeePercent), toBN(0));
                        if (maxGasFeePercent.eq(toBN(0))) {
                            minDeposit = MAX_BIG_INT;
                            isAmountTooLow = true;
                        }
                        else {
                            minDeposit = gasFeeTotal.mul(fixedPointAdjustment).div(maxGasFeePercent);
                            isAmountTooLow = toBN(outputAmount).lt(minDeposit);
                        }
                        return [2 /*return*/, {
                                amountToRelay: outputAmount.toString(),
                                tokenSymbol: inputTokenInfo.symbol,
                                gasFeePercent: gasFeePercent.toString(),
                                gasFeeTotal: gasFeeTotal.toString(),
                                gasDiscountPercent: this.gasDiscountPercent,
                                capitalFeePercent: capitalFeePercent.toString(),
                                capitalFeeTotal: capitalFeeTotal.toString(),
                                capitalDiscountPercent: this.capitalDiscountPercent,
                                relayFeePercent: relayFeePercent.toString(),
                                relayFeeTotal: relayFeeTotal.toString(),
                                feeLimitPercent: this.feeLimitPercent,
                                maxGasFeePercent: maxGasFeePercent.toString(),
                                minDeposit: minDeposit.toString(),
                                isAmountTooLow: isAmountTooLow,
                            }];
                }
            });
        });
    };
    return RelayFeeCalculator;
}());
export { RelayFeeCalculator };
//# sourceMappingURL=relayFeeCalculator.js.map