"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ObligationOrderAtIndex = exports.KaminoObligationOrder = exports.DeleverageAllDebt = exports.DeleverageDebtAmount = exports.DebtCollPriceRatioBelow = exports.DebtCollPriceRatioAbove = exports.UserLtvBelow = exports.UserLtvAbove = exports.LiquidationLtvCloserThan = exports.Always = void 0;
/* eslint-disable max-classes-per-file */
const fraction_1 = require("./fraction");
const types_1 = require("../@codegen/klend/types");
const utils_1 = require("./utils");
const decimal_js_1 = __importDefault(require("decimal.js"));
const bn_js_1 = __importDefault(require("bn.js"));
const utils_2 = require("../utils");
const validations_1 = require("../utils/validations");
// All condition types:
/**
 * A condition that is always met, regardless of the obligation's state.
 *
 * The SC requires the threshold to be zero and the execution bonus to be constant (min == max)
 */
class Always {
    constructor(_threshold) { }
    threshold() {
        return new decimal_js_1.default(0);
    }
    evaluate(_obligation) {
        return { normalizedDistanceFromThreshold: null };
    }
}
exports.Always = Always;
/**
 * A condition met when the obligation's LTV is within the given threshold of its liquidation LTV.
 *
 * Hit when `liquidationLtv - userLtv < conditionThreshold`.
 *
 * Example: If an obligation liquidates at LTV = 81%, then `LiquidationLtvCloserThan(0.06)`
 * becomes executable when LTV > 75%.
 */
class LiquidationLtvCloserThan {
    diffToLiquidationLtv;
    constructor(diffToLiquidationLtv) {
        this.diffToLiquidationLtv = new decimal_js_1.default(diffToLiquidationLtv);
    }
    threshold() {
        return this.diffToLiquidationLtv;
    }
    evaluate(obligation) {
        const liquidationLtv = obligation.liquidationLtv();
        const effectiveThreshold = decimal_js_1.default.max(liquidationLtv.sub(this.diffToLiquidationLtv), new decimal_js_1.default(0));
        return evaluateStopLoss(obligation.loanToValue(), effectiveThreshold, liquidationLtv);
    }
}
exports.LiquidationLtvCloserThan = LiquidationLtvCloserThan;
/**
 * A condition met when obligation's overall "User LTV" is strictly higher than the given threshold.
 */
class UserLtvAbove {
    minUserLtvExclusive;
    constructor(minUserLtvExclusive) {
        this.minUserLtvExclusive = new decimal_js_1.default(minUserLtvExclusive);
    }
    threshold() {
        return this.minUserLtvExclusive;
    }
    evaluate(obligation) {
        // Note: below we deliberately use the LTV-related methods of `KaminoObligation` (instead of the precomputed fields
        // of the `ObligationStats`), since we care about using the same LTV computation as the KLend smart contract.
        // Please see their docs for details.
        return evaluateStopLoss(obligation.loanToValue(), this.minUserLtvExclusive, obligation.liquidationLtv());
    }
}
exports.UserLtvAbove = UserLtvAbove;
/**
 * A condition met when obligation's overall "User LTV" is strictly lower than the given threshold.
 */
class UserLtvBelow {
    maxUserLtvExclusive;
    constructor(maxUserLtvExclusive) {
        this.maxUserLtvExclusive = new decimal_js_1.default(maxUserLtvExclusive);
    }
    threshold() {
        return this.maxUserLtvExclusive;
    }
    evaluate(obligation) {
        // Note: below we deliberately use the `KaminoObligation.loanToValue()` method (instead of the precomputed field
        // `ObligationStats.loanToValue`), since we care about using the same LTV computation as the KLend smart contract.
        // Please see the method's docs for details.
        return evaluateTakeProfit(obligation.loanToValue(), this.maxUserLtvExclusive);
    }
}
exports.UserLtvBelow = UserLtvBelow;
/**
 * A condition met when the obligation's collateral token price (expressed in the debt token) is strictly higher than
 * the given threshold.
 *
 * May only be applied to single-collateral, single-debt obligations.
 */
class DebtCollPriceRatioAbove {
    minDebtCollPriceRatioExclusive;
    constructor(minDebtCollPriceRatioExclusive) {
        this.minDebtCollPriceRatioExclusive = new decimal_js_1.default(minDebtCollPriceRatioExclusive);
    }
    threshold() {
        return this.minDebtCollPriceRatioExclusive;
    }
    evaluate(obligation) {
        const priceRatio = calculateDebtCollPriceRatio(obligation);
        return evaluateStopLoss(priceRatio, this.minDebtCollPriceRatioExclusive, 
        // For single-debt-single-coll obligations, the price ratio is directly proportional
        // to LTV - so we can calculate the "liquidation price ratio" simply by scaling the
        // current value by the ratio of unhealthy/current borrow value:
        priceRatio
            .mul(obligation.refreshedStats.borrowLiquidationLimit)
            .div(obligation.refreshedStats.userTotalBorrowBorrowFactorAdjusted));
    }
}
exports.DebtCollPriceRatioAbove = DebtCollPriceRatioAbove;
/**
 * A condition met when the obligation's collateral token price (expressed in the debt token) is strictly higher than
 * the given threshold.
 *
 * May only be applied to single-collateral, single-debt obligations.
 */
class DebtCollPriceRatioBelow {
    maxDebtCollPriceRatioExclusive;
    constructor(maxDebtCollPriceRatioExclusive) {
        this.maxDebtCollPriceRatioExclusive = new decimal_js_1.default(maxDebtCollPriceRatioExclusive);
    }
    threshold() {
        return this.maxDebtCollPriceRatioExclusive;
    }
    evaluate(obligation) {
        return evaluateTakeProfit(calculateDebtCollPriceRatio(obligation), this.maxDebtCollPriceRatioExclusive);
    }
}
exports.DebtCollPriceRatioBelow = DebtCollPriceRatioBelow;
// All opportunity types:
/**
 * An opportunity for repaying the given amount of the obligation's debt token.
 *
 * May only be applied to single-debt obligations.
 */
class DeleverageDebtAmount {
    amount;
    constructor(amount) {
        this.amount = new decimal_js_1.default(amount);
    }
    parameter() {
        return this.amount;
    }
    getMaxRepay(borrows) {
        const singleBorrow = (0, validations_1.getSingleElement)(borrows, 'borrow');
        return {
            mint: singleBorrow.mintAddress,
            amount: decimal_js_1.default.min(singleBorrow.amount, this.amount),
            reserveAddress: singleBorrow.reserveAddress,
        };
    }
}
exports.DeleverageDebtAmount = DeleverageDebtAmount;
/**
 * An opportunity for repaying all debt(s) of an obligation.
 */
class DeleverageAllDebt {
    /**
     * The only legal value for the {@link parameter()} of this opportunity type.
     */
    static FRACTION_MAX = new fraction_1.Fraction(fraction_1.Fraction.MAX_F_BN).toDecimal();
    constructor(fixed_parameter) {
        if (fixed_parameter !== undefined && !new decimal_js_1.default(fixed_parameter).eq(DeleverageAllDebt.FRACTION_MAX)) {
            throw new Error(`invalid DeleverageAllDebt parameter: ${fixed_parameter} (if given, must be FRACTION_MAX = ${DeleverageAllDebt.FRACTION_MAX})`);
        }
    }
    parameter() {
        return DeleverageAllDebt.FRACTION_MAX;
    }
    getMaxRepay(borrows) {
        if (borrows.length === 0) {
            throw new Error(`Opportunity type not valid on obligation with no borrows`);
        }
        const highestValueBorrow = borrows
            .sort((left, right) => left.marketValueRefreshed.comparedTo(right.marketValueRefreshed))
            .at(-1);
        return {
            mint: highestValueBorrow.mintAddress,
            amount: highestValueBorrow.amount,
            reserveAddress: highestValueBorrow.reserveAddress,
        };
    }
}
exports.DeleverageAllDebt = DeleverageAllDebt;
// Internal type ID mappings:
const CONDITION_TO_TYPE_ID = new Map([
    // Note: the special value of 0 (Never) is represented in the SDK by `KaminoObligationOrder === null`.
    [UserLtvAbove, 1],
    [UserLtvBelow, 2],
    [DebtCollPriceRatioAbove, 3],
    [DebtCollPriceRatioBelow, 4],
    [Always, 5],
    [LiquidationLtvCloserThan, 6],
]);
const OPPORTUNITY_TO_TYPE_ID = new Map([
    [DeleverageDebtAmount, 0],
    [DeleverageAllDebt, 1],
]);
const TYPE_ID_TO_CONDITION = new Map([...CONDITION_TO_TYPE_ID].map(([type, id]) => [id, type]));
const TYPE_ID_TO_OPPORTUNITY = new Map([...OPPORTUNITY_TO_TYPE_ID].map(([type, id]) => [id, type]));
// Core types:
/**
 * A business wrapper around the on-chain {@link ObligationOrder} account data.
 */
class KaminoObligationOrder {
    /**
     * An on-chain data representing a `null` order.
     */
    static NULL_STATE = new types_1.ObligationOrder({
        conditionType: 0,
        conditionThresholdSf: new bn_js_1.default(0),
        opportunityType: 0,
        opportunityParameterSf: new bn_js_1.default(0),
        minExecutionBonusBps: 0,
        maxExecutionBonusBps: 0,
        padding1: Array(10).fill(0),
        padding2: Array(5).fill(new bn_js_1.default(0)),
    });
    /**
     * The order's condition.
     */
    condition;
    /**
     * The order's opportunity.
     */
    opportunity;
    /**
     * The minimum bonus rate (e.g. `0.01` meaning "1%") offered to a liquidator executing this order when its condition
     * threshold has been barely crossed.
     */
    minExecutionBonusRate;
    /**
     * The maximum bonus rate (e.g. `0.04` meaning "4%") offered to a liquidator executing this order when its condition
     * threshold has already been exceeded by a very large margin (to be specific: maximum possible margin - e.g. for
     * LTV-based stop-loss order, that would be when the obligation's LTV is approaching its liquidation LTV).
     */
    maxExecutionBonusRate;
    /**
     * Direct constructor.
     *
     * Please see {@link fromState()} if you are constructing an instance representing existing on-chain data.
     */
    constructor(condition, opportunity, minExecutionBonusRate, maxExecutionBonusRate = minExecutionBonusRate) {
        this.condition = condition;
        this.opportunity = opportunity;
        this.minExecutionBonusRate = minExecutionBonusRate;
        this.maxExecutionBonusRate = maxExecutionBonusRate;
    }
    /**
     * Returns the highest-valued {@link AvailableOrderExecution} currently offered by this order.
     *
     * May return `undefined` when the order's condition is not met.
     */
    findMaxAvailableExecution(kaminoMarket, obligation) {
        const conditionHit = this.condition.evaluate(obligation);
        if (conditionHit === null) {
            return undefined; // condition not met - cannot execute
        }
        const maxRepay = this.opportunity.getMaxRepay(obligation.getBorrows());
        const repayBorrow = obligation.getBorrowByReserve(maxRepay.reserveAddress);
        const maxRepayValue = tokenAmountToValue(maxRepay, repayBorrow);
        const executionBonusRate = this.calculateExecutionBonusRate(conditionHit, obligation);
        const executionBonusFactor = new decimal_js_1.default(1).add(executionBonusRate);
        const maxWithdrawValue = maxRepayValue.mul(executionBonusFactor);
        // The order execution only allows us to pick the lowest-liquidation-LTV deposit for withdrawal (excluding 0-LTV
        // assets, which are never liquidatable), hence we pre-filter the candidate deposits:
        const liquidationLtvsOfDeposits = obligation
            .getDeposits()
            .map((deposit) => [
            obligation.getLtvForReserve(kaminoMarket, deposit.reserveAddress).liquidationLtv,
            deposit,
        ]);
        const liquidatableDeposits = liquidationLtvsOfDeposits.filter(([liquidationLtv, _deposit]) => liquidationLtv.gt(0));
        // Note: in theory, we could use the Obligation's `lowestReserveDepositLiquidationLtv` (cached by SC) here, but it
        // is equally easy to just find the minimum (and avoid any issues related to stale `KaminoObligation` state or
        // `Decimal` rounding/comparison).
        const minLiquidationLtv = decimal_js_1.default.min(...liquidatableDeposits.map(([liquidationLtv, _deposit]) => liquidationLtv));
        const [actualWithdrawValue, withdrawDeposit] = liquidatableDeposits
            .filter(([liquidationLtv, _deposit]) => liquidationLtv.eq(minLiquidationLtv))
            .map(([_liquidationLtv, deposit]) => {
            const availableWithdrawValue = decimal_js_1.default.min(deposit.marketValueRefreshed, maxWithdrawValue);
            return [availableWithdrawValue, deposit];
        })
            .sort(([leftValue, leftDeposit], [rightValue, rightDeposit]) => {
            const valueComparison = leftValue.comparedTo(rightValue);
            if (valueComparison !== 0) {
                return valueComparison;
            }
            // Just for deterministic selection in case of multiple equally-good deposits: pick the one with lower mint pubkey (mostly for test stability purposes)
            return leftDeposit.reserveAddress.localeCompare(rightDeposit.reserveAddress);
        })
            .at(-1);
        const actualRepayValue = actualWithdrawValue.div(executionBonusFactor);
        return {
            repay: valueToTokenAmount(actualRepayValue, repayBorrow),
            withdraw: valueToTokenAmount(actualWithdrawValue, withdrawDeposit),
            bonusRate: executionBonusRate,
        };
    }
    /**
     * Constructs an instance based on the given on-chain data.
     *
     * Returns `null` if the input represents just an empty slot in the orders' array.
     */
    static fromState(state) {
        if (state.conditionType === KaminoObligationOrder.NULL_STATE.conditionType) {
            return null; // In practice an entire null order is zeroed, but technically the "condition == never" is enough to consider the order "not active"
        }
        const conditionConstructor = TYPE_ID_TO_CONDITION.get(state.conditionType) ?? (0, utils_1.orThrow)(`Unknown condition type ${state.conditionType}`);
        const condition = new conditionConstructor(new fraction_1.Fraction(state.conditionThresholdSf).toDecimal());
        const opportunityConstructor = TYPE_ID_TO_OPPORTUNITY.get(state.opportunityType) ?? (0, utils_1.orThrow)(`Unknown opportunity type ${state.opportunityType}`);
        const opportunity = new opportunityConstructor(new fraction_1.Fraction(state.opportunityParameterSf).toDecimal());
        const minExecutionBonusRate = fraction_1.Fraction.fromBps(state.minExecutionBonusBps).toDecimal();
        const maxExecutionBonusRate = fraction_1.Fraction.fromBps(state.maxExecutionBonusBps).toDecimal();
        return new KaminoObligationOrder(condition, opportunity, minExecutionBonusRate, maxExecutionBonusRate);
    }
    /**
     * Returns the on-chain state represented by this instance.
     *
     * See {@link NULL_STATE} for the state of a `null` order.
     */
    toState() {
        return new types_1.ObligationOrder({
            ...KaminoObligationOrder.NULL_STATE.toEncodable(),
            conditionType: CONDITION_TO_TYPE_ID.get(Object.getPrototypeOf(this.condition).constructor) ??
                (0, utils_1.orThrow)(`Unknown condition ${this.condition.constructor}`),
            conditionThresholdSf: fraction_1.Fraction.fromDecimal(this.condition.threshold()).getValue(),
            opportunityType: OPPORTUNITY_TO_TYPE_ID.get(Object.getPrototypeOf(this.opportunity).constructor) ??
                (0, utils_1.orThrow)(`Unknown opportunity ${this.opportunity.constructor}`),
            opportunityParameterSf: fraction_1.Fraction.fromDecimal(this.opportunity.parameter()).getValue(),
            minExecutionBonusBps: (0, utils_1.roundNearest)(this.minExecutionBonusRate.mul(utils_2.ONE_HUNDRED_PCT_IN_BPS)).toNumber(),
            maxExecutionBonusBps: (0, utils_1.roundNearest)(this.maxExecutionBonusRate.mul(utils_2.ONE_HUNDRED_PCT_IN_BPS)).toNumber(),
        });
    }
    /**
     * Binds this order to the given slot.
     *
     * This is just a convenience method for easier interaction with {@link KaminoAction#buildSetObligationOrderIxn()}.
     */
    atIndex(index) {
        return new ObligationOrderAtIndex(index, this);
    }
    /**
     * Calculates the given order's actual execution bonus rate.
     *
     * The min-max bonus range is configured by the user on a per-order basis, and the actual value is interpolated based
     * on the given obligation's state at the moment of order execution.
     * In short: the minimum bonus applies if the order is executed precisely at the point when the condition is met.
     * Then, as the distance from condition's threshold grows, the bonus approaches the configured maximum.
     *
     * On top of that, similar to regular liquidation, the bonus cannot exceed the ceiled limit of `1.0 - user_no_bf_ltv`
     * (which ensures that order execution improves LTV).
     */
    calculateExecutionBonusRate(conditionHit, obligation) {
        const theoreticalBonusRate = conditionHit.normalizedDistanceFromThreshold !== null
            ? interpolateBonusRate(conditionHit.normalizedDistanceFromThreshold, this.minExecutionBonusRate, this.maxExecutionBonusRate)
            : this.minExecutionBonusRate; // constant bonus (min == max enforced by SC)
        // In order to ensure that LTV improves on order execution, we apply the same heuristic formula as for the regular
        // liquidations. Please note that we deliberately use the `obligation.noBfLoanToValue()`, which is consistent with
        // the smart contract's calculation:
        const diffToBadDebt = new decimal_js_1.default(1).sub(obligation.noBfLoanToValue());
        return decimal_js_1.default.min(theoreticalBonusRate, diffToBadDebt);
    }
}
exports.KaminoObligationOrder = KaminoObligationOrder;
/**
 * A single slot within {@link Obligation.orders} (which may contain an order or not).
 *
 * This is used as an argument to {@link KaminoAction.buildSetObligationOrderIxn()} to easily set or cancel an order.
 */
class ObligationOrderAtIndex {
    index;
    order;
    constructor(index, order) {
        this.index = index;
        this.order = order;
    }
    /**
     * Creates an empty slot representation (suitable for cancelling an order).
     */
    static empty(index) {
        return new ObligationOrderAtIndex(index, null);
    }
    /**
     * Returns the on-chain state of the order (potentially a zeroed account data, if the order is not set).
     */
    orderState() {
        return this.order !== null ? this.order.toState() : KaminoObligationOrder.NULL_STATE;
    }
}
exports.ObligationOrderAtIndex = ObligationOrderAtIndex;
// Internal calculation functions:
function tokenAmountToValue(tokenAmount, position) {
    if (tokenAmount.reserveAddress !== position.reserveAddress) {
        throw new Error(`Value of token amount ${tokenAmount} cannot be computed using data from ${position}`);
    }
    return tokenAmount.amount.mul(position.marketValueRefreshed).div(position.amount);
}
function valueToTokenAmount(value, position) {
    const fractionalAmount = value.mul(position.amount).div(position.marketValueRefreshed);
    return {
        amount: (0, utils_1.roundNearest)(fractionalAmount),
        mint: position.mintAddress,
        reserveAddress: position.reserveAddress,
    };
}
function evaluateStopLoss(current_value, conditionThreshold, liquidationThreshold) {
    if (current_value.lte(conditionThreshold)) {
        return null; // SL not hit
    }
    let normalizedDistanceFromThreshold;
    if (conditionThreshold.gt(liquidationThreshold)) {
        // A theoretically-impossible case (the user may of course set his order's condition
        // threshold that high, but then the current value is above liquidation threshold, so
        // liquidation logic should kick in and never reach this function). Anyway, let's
        // interpret it as maximum distance from threshold:
        normalizedDistanceFromThreshold = new decimal_js_1.default(1);
    }
    else {
        // By now we know they are both > 0:
        const currentDistance = current_value.sub(conditionThreshold);
        const maximumDistance = liquidationThreshold.sub(conditionThreshold);
        normalizedDistanceFromThreshold = currentDistance.div(maximumDistance);
    }
    return { normalizedDistanceFromThreshold };
}
function evaluateTakeProfit(currentValue, conditionThreshold) {
    if (currentValue.gte(conditionThreshold)) {
        return null; // TP not hit
    }
    const distanceTowards0 = conditionThreshold.sub(currentValue); // by now we know it is > 0
    return { normalizedDistanceFromThreshold: distanceTowards0.div(conditionThreshold) };
}
function calculateDebtCollPriceRatio(obligation) {
    const singleBorrow = (0, validations_1.getSingleElement)(obligation.getBorrows(), 'borrow');
    const singleDeposit = (0, validations_1.getSingleElement)(obligation.getDeposits(), 'deposit');
    return calculateTokenPrice(singleBorrow).div(calculateTokenPrice(singleDeposit));
}
function calculateTokenPrice(position) {
    return position.marketValueRefreshed.mul(position.mintFactor).div(position.amount);
}
function interpolateBonusRate(normalizedDistanceFromThreshold, minBonusRate, maxBonusRate) {
    return minBonusRate.add(normalizedDistanceFromThreshold.mul(maxBonusRate.sub(minBonusRate)));
}
//# sourceMappingURL=obligationOrder.js.map