"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RelayFeeCalculator = exports.DEFAULT_LOGGER = exports.expectedCapitalCostsKeys = void 0;
exports.getDefaultRelayer = getDefaultRelayer;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const constants_1 = require("../constants");
const utils_1 = require("../utils");
exports.expectedCapitalCostsKeys = ["lowerBound", "upperBound", "cutoff", "decimals"];
exports.DEFAULT_LOGGER = {
    debug: (...args) => console.debug(args),
    info: (...args) => console.info(args),
    warn: (...args) => console.warn(args),
    error: (...args) => console.error(args),
};
function getDefaultRelayer(chainId) {
    return (0, utils_1.isDefined)(chainId) && (0, utils_1.chainIsSvm)(chainId)
        ? utils_1.SvmAddress.from(constants_1.DEFAULT_SIMULATED_RELAYER_ADDRESS_SVM)
        : utils_1.EvmAddress.from(constants_1.DEFAULT_SIMULATED_RELAYER_ADDRESS);
}
const safeOutputAmount = (0, utils_1.toBN)(100);
class RelayFeeCalculator {
    queries;
    gasDiscountPercent;
    capitalDiscountPercent;
    feeLimitPercent;
    nativeTokenDecimals;
    capitalCostsConfig;
    logger;
    constructor(config, logger, destinationChainId) {
        (0, assert_1.default)(config, "config must be provided");
        if ("queries" in config) {
            this.queries = config.queries;
        }
        else {
            (0, assert_1.default)(destinationChainId !== undefined, "destinationChainId must be provided if queriesMap is provided");
            (0, assert_1.default)(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;
        (0, assert_1.default)(this.gasDiscountPercent >= 0 && this.gasDiscountPercent <= 100, "gasDiscountPercent must be between 0 and 100 percent");
        (0, assert_1.default)(this.capitalDiscountPercent >= 0 && this.capitalDiscountPercent <= 100, "capitalDiscountPercent must be between 0 and 100 percent");
        (0, assert_1.default)(this.feeLimitPercent >= 0 && this.feeLimitPercent <= 100, "feeLimitPercent must be between 0 and 100 percent");
        this.capitalCostsConfig = Object.fromEntries(Object.entries(config.capitalCostsConfig).map(([token, capitalCosts]) => {
            return [token.toUpperCase(), RelayFeeCalculator.validateAndTransformCapitalCostsConfigOverride(capitalCosts)];
        }));
        (0, assert_1.default)(Object.keys(this.capitalCostsConfig).length > 0, "capitalCostsConfig must have at least one entry");
        this.logger = logger || exports.DEFAULT_LOGGER;
        this.checkAllConfigConflicts();
    }
    static capitalCostConfigIsOverride(config) {
        return config.default !== undefined;
    }
    static validateAndTransformCapitalCostsConfigOverride(capitalCosts) {
        const config = this.capitalCostConfigIsOverride(capitalCosts)
            ? capitalCosts
            : { default: capitalCosts };
        this.validateCapitalCostsConfig(config.default);
        for (const toChainIdRoutes of Object.values(config.routeOverrides || {})) {
            Object.values(toChainIdRoutes).forEach(this.validateCapitalCostsConfig);
        }
        Object.values(config.originChainOverrides || {}).forEach(this.validateCapitalCostsConfig);
        Object.values(config.destinationChainOverrides || {}).forEach(this.validateCapitalCostsConfig);
        return config;
    }
    static validateCapitalCostsConfig(capitalCosts) {
        (0, assert_1.default)((0, utils_1.toBN)(capitalCosts.upperBound).lt((0, utils_1.toBNWei)("1")), "upper bound must be < 100%");
        (0, assert_1.default)(capitalCosts.decimals > 0 && capitalCosts.decimals <= 18, "invalid decimals");
    }
    getTokenPrice(tokenSymbol) {
        return this.queries.getTokenPrice(tokenSymbol);
    }
    async gasFeePercent(deposit, outputAmount, outputTokenInfo, simulateZeroFill = false, relayerAddress = getDefaultRelayer(deposit.destinationChainId), _tokenPrice, gasPrice, gasLimit, _tokenGasCost, transport) {
        if ((0, utils_1.toBN)(outputAmount).eq(utils_1.bnZero))
            return utils_1.MAX_BIG_INT;
        const simulatedAmount = simulateZeroFill ? safeOutputAmount : (0, utils_1.toBN)(outputAmount);
        deposit = { ...deposit, outputAmount: simulatedAmount };
        const getGasCosts = this.queries
            .getGasCosts(deposit, relayerAddress, { gasPrice, gasUnits: gasLimit, transport })
            .then(({ tokenGasCost }) => tokenGasCost)
            .catch((error) => {
            this.logger.error({
                at: "sdk/gasFeePercent",
                message: "Error while fetching gas costs",
                error,
                simulateZeroFill,
                deposit,
            });
            throw error;
        });
        const [tokenGasCost, tokenPrice] = await Promise.all([
            _tokenGasCost ? Promise.resolve(_tokenGasCost) : getGasCosts,
            this.resolveTokenPrice(outputTokenInfo, _tokenPrice, deposit),
        ]);
        const gasFeesInToken = (0, utils_1.nativeToToken)(tokenGasCost, tokenPrice, outputTokenInfo.decimals, this.nativeTokenDecimals);
        return (0, utils_1.percent)(gasFeesInToken, outputAmount.toString());
    }
    async auxNativeFeePercent(deposit, outputAmount, outputTokenInfo, _tokenPrice) {
        if ((0, utils_1.toBN)(outputAmount).eq(utils_1.bnZero))
            return utils_1.MAX_BIG_INT;
        let auxNativeCost = utils_1.bnZero;
        try {
            auxNativeCost = this.queries.getAuxiliaryNativeTokenCost(deposit);
        }
        catch (error) {
            this.logger.error({
                at: "sdk/auxNativeFeePercent",
                message: "Error while fetching auxiliary native token cost",
                error,
                destinationChainId: deposit.destinationChainId,
                inputToken: deposit.inputToken,
            });
            throw error;
        }
        const tokenPrice = await this.resolveTokenPrice(outputTokenInfo, _tokenPrice, deposit);
        const auxFeesInToken = (0, utils_1.nativeToToken)(auxNativeCost, tokenPrice, outputTokenInfo.decimals, this.nativeTokenDecimals);
        return (0, utils_1.percent)(auxFeesInToken, outputAmount);
    }
    capitalFeePercent(_outputAmount, _tokenSymbol, _originRoute, _destinationRoute) {
        if ((0, utils_1.toBN)(_outputAmount).eq((0, utils_1.toBN)(0)))
            return utils_1.MAX_BIG_INT;
        const tokenCostConfig = this.capitalCostsConfig[_tokenSymbol.toUpperCase()];
        if (!(0, utils_1.isDefined)(tokenCostConfig)) {
            this.logger.error({
                at: "sdk/capitalFeePercent",
                message: `No capital fee available for token ${_tokenSymbol}`,
            });
            throw new Error(`No capital cost config available for token ${_tokenSymbol}`);
        }
        else {
            const routeOverride = tokenCostConfig?.routeOverrides?.[_originRoute || ""]?.[_destinationRoute || ""];
            const destinationChainOverride = tokenCostConfig?.destinationChainOverrides?.[_destinationRoute || ""];
            const originChainOverride = tokenCostConfig?.originChainOverrides?.[_originRoute || ""];
            const config = routeOverride ?? destinationChainOverride ?? originChainOverride ?? tokenCostConfig.default;
            this.warnIfConfigConflicts(_tokenSymbol, _originRoute || "", _destinationRoute || "", routeOverride, destinationChainOverride, originChainOverride);
            const y = (0, utils_1.toBN)(_outputAmount).mul((0, utils_1.toBNWei)("1", 18 - config.decimals));
            const minCharge = (0, utils_1.toBN)(config.lowerBound).mul(y).div(utils_1.fixedPointAdjustment);
            const yTriangle = (0, utils_1.min)(config.cutoff, y);
            const triangleSlope = (0, utils_1.toBN)(config.cutoff).eq((0, utils_1.toBN)(0))
                ? (0, utils_1.toBN)(0)
                : (0, utils_1.toBN)(config.upperBound).sub(config.lowerBound).mul(utils_1.fixedPointAdjustment).div(config.cutoff);
            const triangleHeight = triangleSlope.mul(yTriangle).div(utils_1.fixedPointAdjustment);
            const triangleCharge = triangleHeight.mul(yTriangle).div((0, utils_1.toBNWei)(2));
            const yRemainder = (0, utils_1.max)((0, utils_1.toBN)(0), y.sub(config.cutoff));
            const remainderCharge = yRemainder.mul((0, utils_1.toBN)(config.upperBound).sub(config.lowerBound)).div(utils_1.fixedPointAdjustment);
            return minCharge.add(triangleCharge).add(remainderCharge).mul(utils_1.fixedPointAdjustment).div(y);
        }
    }
    checkAllConfigConflicts() {
        for (const [tokenSymbol, tokenConfig] of Object.entries(this.capitalCostsConfig)) {
            const originChains = new Set(Object.keys(tokenConfig.originChainOverrides || {}));
            const destChains = new Set(Object.keys(tokenConfig.destinationChainOverrides || {}));
            if (tokenConfig.routeOverrides) {
                Object.keys(tokenConfig.routeOverrides).forEach((originChain) => {
                    originChains.add(originChain);
                    Object.keys(tokenConfig.routeOverrides[originChain]).forEach((destChain) => {
                        destChains.add(destChain);
                    });
                });
            }
            if (originChains.size === 0 && destChains.size === 0) {
                continue;
            }
            for (const originChain of Array.from(originChains)) {
                for (const destChain of Array.from(destChains)) {
                    const routeOverride = tokenConfig.routeOverrides?.[originChain]?.[destChain];
                    const destinationChainOverride = tokenConfig.destinationChainOverrides?.[destChain];
                    const originChainOverride = tokenConfig.originChainOverrides?.[originChain];
                    this.warnIfConfigConflicts(tokenSymbol, originChain, destChain, routeOverride, destinationChainOverride, originChainOverride);
                }
            }
        }
    }
    warnIfConfigConflicts(tokenSymbol, originChain, destChain, routeOverride, destinationChainOverride, originChainOverride) {
        const overrideCount = [routeOverride, destinationChainOverride, originChainOverride].filter(Boolean).length;
        if (overrideCount > 1) {
            const 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 ${tokenSymbol} from chain ${originChain} to chain ${destChain}`,
                configUsed,
                routeOverride,
                destinationChainOverride,
                originChainOverride,
            });
        }
    }
    async relayerFeeDetails(deposit, outputAmount, simulateZeroFill = false, relayerAddress = getDefaultRelayer(deposit.destinationChainId), _tokenPrice, gasPrice, gasUnits, tokenGasCost) {
        outputAmount ??= deposit.outputAmount;
        const { inputTokenInfo, outputTokenInfo } = this.resolveInOutTokenInfos(deposit);
        const tokenPrice = await this.resolveTokenPrice(outputTokenInfo, _tokenPrice, deposit);
        const { tokenGasCost: _tokenGasCost, nativeGasCost: _nativeGasCost } = tokenGasCost && gasUnits
            ? { tokenGasCost, nativeGasCost: utils_1.BigNumber.from(gasUnits) }
            : await this.queries
                .getGasCosts(deposit, relayerAddress, { gasPrice, gasUnits })
                .then(({ tokenGasCost, nativeGasCost }) => ({ tokenGasCost, nativeGasCost }))
                .catch((error) => {
                this.logger.error({
                    at: "sdk/relayerFeeDetails",
                    message: "Error while fetching gas costs",
                    error,
                    simulateZeroFill,
                    deposit,
                });
                throw error;
            });
        const gasFeePercent = await this.gasFeePercent(deposit, outputAmount, outputTokenInfo, simulateZeroFill, relayerAddress, tokenPrice, gasPrice, _nativeGasCost, _tokenGasCost);
        const outToInDecimals = (0, utils_1.ConvertDecimals)(outputTokenInfo.decimals, inputTokenInfo.decimals);
        const gasFeeTotal = gasFeePercent.mul(outToInDecimals(outputAmount.toString())).div(utils_1.fixedPointAdjustment);
        const capitalFeePercent = this.capitalFeePercent(outputAmount, inputTokenInfo.symbol, deposit.originChainId.toString(), deposit.destinationChainId.toString());
        const capitalFeeTotal = capitalFeePercent.mul(outToInDecimals(outputAmount.toString())).div(utils_1.fixedPointAdjustment);
        const auxNativeFeePercent = await this.auxNativeFeePercent(deposit, outputAmount, outputTokenInfo, tokenPrice);
        const auxNativeFeeTotal = auxNativeFeePercent
            .mul(outToInDecimals(outputAmount.toString()))
            .div(utils_1.fixedPointAdjustment);
        const auxNativeDiscountPercent = this.gasDiscountPercent;
        const relayFeePercent = gasFeePercent.add(capitalFeePercent).add(auxNativeFeePercent);
        const relayFeeTotal = gasFeeTotal.add(capitalFeeTotal).add(auxNativeFeeTotal);
        const maxGasFeePercent = (0, utils_1.max)((0, utils_1.toBNWei)(this.feeLimitPercent / 100)
            .sub(capitalFeePercent)
            .sub(auxNativeFeePercent), (0, utils_1.toBN)(0));
        let minDeposit, isAmountTooLow;
        if (maxGasFeePercent.eq((0, utils_1.toBN)(0))) {
            minDeposit = utils_1.MAX_BIG_INT;
            isAmountTooLow = true;
        }
        else {
            minDeposit = gasFeeTotal.mul(utils_1.fixedPointAdjustment).div(maxGasFeePercent);
            isAmountTooLow = (0, utils_1.toBN)(outputAmount).lt(minDeposit);
        }
        return {
            amountToRelay: outputAmount.toString(),
            tokenSymbol: inputTokenInfo.symbol,
            gasFeePercent: gasFeePercent.toString(),
            gasFeeTotal: gasFeeTotal.toString(),
            gasUnits: _nativeGasCost.toString(),
            gasDiscountPercent: this.gasDiscountPercent,
            auxNativeFeePercent: auxNativeFeePercent.toString(),
            auxNativeFeeTotal: auxNativeFeeTotal.toString(),
            auxNativeDiscountPercent,
            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,
        };
    }
    resolveInOutTokenInfos(deposit, tokenMapping = constants_1.TOKEN_SYMBOLS_MAP) {
        const { inputToken, destinationChainId, originChainId } = deposit;
        const destinationChainTokenDetails = Object.values(tokenMapping).find((details) => (0, utils_1.compareAddressesSimple)(details.addresses[originChainId], inputToken.toNative()) &&
            (0, utils_1.isDefined)(details.addresses[destinationChainId]));
        const outputToken = deposit.outputToken.isZeroAddress()
            ? (0, utils_1.toAddressType)(destinationChainTokenDetails.addresses[destinationChainId], destinationChainId)
            : deposit.outputToken;
        const outputTokenInfo = (0, utils_1.getTokenInfo)(outputToken, destinationChainId, tokenMapping);
        const inputTokenInfo = (0, utils_1.getTokenInfo)(inputToken, originChainId, tokenMapping);
        if (!(0, utils_1.isDefined)(outputTokenInfo) || !(0, utils_1.isDefined)(inputTokenInfo)) {
            throw new Error(`Could not find token information for ${inputToken} or ${outputToken}`);
        }
        return { inputTokenInfo, outputTokenInfo };
    }
    async resolveTokenPrice(outputTokenInfo, _tokenPrice, deposit) {
        return (_tokenPrice ??
            (await this.queries.getTokenPrice(outputTokenInfo.symbol).catch((error) => {
                this.logger.error({
                    at: "sdk/resolveTokenPrice",
                    message: "Error while fetching token price",
                    error,
                    destinationChainId: deposit.destinationChainId,
                    inputToken: deposit.inputToken,
                });
                throw error;
            })));
    }
}
exports.RelayFeeCalculator = RelayFeeCalculator;
//# sourceMappingURL=relayFeeCalculator.js.map