"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.solutionObjectiveValueComputation = exports.solutionObjectiveValue = exports.orderDisregardedUtility = exports.orderUtility = exports.getExecutedSellAmount = exports.ERROR_EPSILON = exports.feeAdded = exports.feeSubtracted = exports.toETH = void 0;
const assert_1 = __importDefault(require("assert"));
const bn_js_1 = __importDefault(require("bn.js"));
const array_shims_1 = require("./array-shims");
/**
 * Converts the amount value to `ether` unit.
 * @param value - - The amount to convert
 * @returns The value in `ether` as a bignum
 */
function toETH(value) {
    const GWEI = 1000000000;
    return new bn_js_1.default(value * GWEI).mul(new bn_js_1.default(GWEI));
}
exports.toETH = toETH;
/**
 * The fee denominator used for calculating fees.
 */
const FEE_DENOMINATOR = new bn_js_1.default(1000);
/**
 * The fee denominator minus one.
 */
const FEE_DENOMINATOR_MINUS_ONE = FEE_DENOMINATOR.sub(new bn_js_1.default(1));
/**
 * Removes fees to the specified value `n` times.
 * @param x - - The value to apply the fee to
 * @param n - - The number of times to apply the fee, must be greater than 0
 * @returns The value minus fees
 */
function feeSubtracted(x, n = 1) {
    const result = x.mul(FEE_DENOMINATOR_MINUS_ONE).div(FEE_DENOMINATOR);
    return n === 1 ? result : feeSubtracted(result, n - 1);
}
exports.feeSubtracted = feeSubtracted;
/**
 * Adds fees to the specified.
 * @param x - The value to apply the fee to
 * @returns The value plus fees
 */
function feeAdded(x) {
    return x.mul(FEE_DENOMINATOR).div(FEE_DENOMINATOR_MINUS_ONE);
}
exports.feeAdded = feeAdded;
/**
 * The error epsilon required for buy/sell amounts to account for rounding
 * errors.
 */
exports.ERROR_EPSILON = new bn_js_1.default(999000);
/**
 * Calculates the executed buy amout given a buy volume and the settled buy and
 * sell prices.
 * @param executedBuyAmount - The executed buy amount
 * @param buyTokenPrice - The buy token price
 * @param sellTokenPrice - The sell token price
 * @returns The value plus fees
 */
function getExecutedSellAmount(executedBuyAmount, buyTokenPrice, sellTokenPrice) {
    return executedBuyAmount
        .mul(buyTokenPrice)
        .div(FEE_DENOMINATOR_MINUS_ONE)
        .mul(FEE_DENOMINATOR)
        .div(sellTokenPrice);
}
exports.getExecutedSellAmount = getExecutedSellAmount;
/**
 * Calculates the utility of an order given an executed buy amount and settled
 * solution prices.
 * @param order - The order
 * @param executedBuyAmount - The executed buy amount
 * @param prices - The prices
 * @returns The order's utility
 */
function orderUtility(order, executedBuyAmount, prices) {
    assert_1.default(prices.length > order.buyToken, "order buy token not included in prices");
    assert_1.default(prices.length > order.sellToken, "order sell token not included in prices");
    const executedSellAmount = getExecutedSellAmount(executedBuyAmount, prices[order.buyToken], prices[order.sellToken]);
    const execSellTimesBuy = executedSellAmount.mul(order.buyAmount);
    const roundedUtility = executedBuyAmount
        .sub(execSellTimesBuy.div(order.sellAmount))
        .mul(prices[order.buyToken]);
    const utilityError = execSellTimesBuy
        .mod(order.sellAmount)
        .mul(prices[order.buyToken])
        .div(order.sellAmount);
    return roundedUtility.sub(utilityError);
}
exports.orderUtility = orderUtility;
/**
 * Calculates the disregarded utility of an order given an executed buy amount
 * and settled solution prices.
 * @param order - The order
 * @param executedBuyAmount - The executed buy amount
 * @param prices - The prices
 * @returns The order's disregarded utility
 */
function orderDisregardedUtility(order, executedBuyAmount, prices) {
    assert_1.default(prices.length > order.buyToken, "order buy token not included in prices");
    assert_1.default(prices.length > order.sellToken, "order sell token not included in prices");
    const executedSellAmount = getExecutedSellAmount(executedBuyAmount, prices[order.buyToken], prices[order.sellToken]);
    // TODO: account for balances here.
    // Contract evaluates as: MIN(sellAmount - executedSellAmount, user.balance.sellToken)
    const leftoverSellAmount = order.sellAmount.sub(executedSellAmount);
    const limitTermLeft = prices[order.sellToken].mul(order.sellAmount);
    const limitTermRight = prices[order.buyToken]
        .mul(order.buyAmount)
        .mul(FEE_DENOMINATOR)
        .div(FEE_DENOMINATOR_MINUS_ONE);
    let limitTerm = toETH(0);
    if (limitTermLeft.gt(limitTermRight)) {
        limitTerm = limitTermLeft.sub(limitTermRight);
    }
    return leftoverSellAmount.mul(limitTerm).div(order.sellAmount);
}
exports.orderDisregardedUtility = orderDisregardedUtility;
/**
 * Calculates the total objective value for the specified solution given the
 * order book.
 * @param orders - The orders
 * @param solution - The solution
 * @returns The solution's objective value
 */
function solutionObjectiveValue(orders, solution) {
    return solutionObjectiveValueComputation(orders, solution, true).result;
}
exports.solutionObjectiveValue = solutionObjectiveValue;
/**
 * Calculates the solutions objective value returning a computation object with
 * all the intermediate values - useful for debugging.
 * @param orders - The orders
 * @param solution - The solution
 * @param strict - Throw when solution is determined to be invalid
 * @returns The solution's objective value computation object
 */
function solutionObjectiveValueComputation(orders, solution, strict = true) {
    const tokenCount = Math.max(...array_shims_1.flat(orders.map((o) => [o.buyToken, o.sellToken]))) + 1;
    assert_1.default(orders.length === solution.buyVolumes.length, "solution buy volumes do not match orders");
    assert_1.default(tokenCount === solution.prices.length, "solution prices does not include all tokens");
    assert_1.default(toETH(1).eq(solution.prices[0]), "fee token price is not 1 ether");
    const touchedOrders = orders
        .map((o, i) => (solution.buyVolumes[i].isZero() ? null : [o, i]))
        .filter((pair) => !!pair);
    const orderExecutedAmounts = orders.map(() => {
        return { buy: new bn_js_1.default(0), sell: new bn_js_1.default(0) };
    });
    const orderTokenConservation = orders.map(() => solution.prices.map(() => new bn_js_1.default(0)));
    const tokenConservation = solution.prices.map(() => new bn_js_1.default(0));
    const utilities = orders.map(() => new bn_js_1.default(0));
    const disregardedUtilities = orders.map(() => new bn_js_1.default(0));
    for (const [order, i] of touchedOrders) {
        const buyVolume = solution.buyVolumes[i];
        const sellVolume = getExecutedSellAmount(solution.buyVolumes[i], solution.prices[order.buyToken], solution.prices[order.sellToken]);
        orderExecutedAmounts[i] = { buy: buyVolume, sell: sellVolume };
        orderTokenConservation[i][order.buyToken].isub(buyVolume);
        orderTokenConservation[i][order.sellToken].iadd(sellVolume);
        tokenConservation[order.buyToken].isub(buyVolume);
        tokenConservation[order.sellToken].iadd(sellVolume);
        utilities[i] = orderUtility(order, solution.buyVolumes[i], solution.prices);
        disregardedUtilities[i] = orderDisregardedUtility(order, solution.buyVolumes[i], solution.prices);
    }
    if (strict) {
        const feeTokenTouched = orders.findIndex((o, i) => !solution.buyVolumes[i].isZero() &&
            (o.buyToken === 0 || o.sellToken === 0)) !== -1;
        assert_1.default(feeTokenTouched, "fee token is not touched");
        assert_1.default(!tokenConservation[0].isNeg(), "fee token conservation is negative");
        tokenConservation
            .slice(1)
            .forEach((conservation, i) => assert_1.default(conservation.isZero(), `token conservation not respected for token ${i + 1}`));
        touchedOrders.forEach(([, id], i) => {
            assert_1.default(!utilities[i].isNeg(), `utility for order ${id} is negative`);
            assert_1.default(!disregardedUtilities[i].isNeg(), `disregarded utility for order ${id} is negative`);
        });
    }
    const totalUtility = utilities.reduce((acc, du) => acc.iadd(du), toETH(0));
    const totalDisregardedUtility = disregardedUtilities.reduce((acc, du) => acc.iadd(du), toETH(0));
    const burntFees = tokenConservation[0].div(new bn_js_1.default(2));
    const result = totalUtility.sub(totalDisregardedUtility).add(burntFees);
    if (strict) {
        assert_1.default(!result.isNeg() && !result.isZero(), "objective value negative or zero");
    }
    return {
        orderExecutedAmounts,
        orderTokenConservation,
        tokenConservation,
        utilities,
        disregardedUtilities,
        totalUtility,
        totalDisregardedUtility,
        burntFees,
        result,
    };
}
exports.solutionObjectiveValueComputation = solutionObjectiveValueComputation;
