"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultLoansAPI = void 0;
const big_js_1 = __importDefault(require("big.js"));
const utils_1 = require("../utils");
class DefaultLoansAPI {
    constructor(api, assetRegistryAPI, transactionAPI) {
        this.api = api;
        this.assetRegistryAPI = assetRegistryAPI;
        this.transactionAPI = transactionAPI;
    }
    // Wrapped call to make mocks in tests simple.
    getLoansMarketsEntries() {
        return __awaiter(this, void 0, void 0, function* () {
            const entries = yield this.api.query.loans.markets.entries();
            return entries.filter((entry) => entry[1].isSome);
        });
    }
    static getLendTokenFromUnderlyingCurrency(currency, lendTokenId) {
        return {
            name: `q${currency.name}`,
            ticker: `q${currency.ticker}`,
            decimals: currency.decimals,
            lendToken: {
                id: lendTokenId.asLendToken.toNumber(),
            },
        };
    }
    getUnderlyingCurrencyFromLendTokenId(lendTokenId) {
        return __awaiter(this, void 0, void 0, function* () {
            const underlyingCurrencyId = yield this.api.query.loans.underlyingAssetId(lendTokenId);
            const underlyingCurrency = yield utils_1.currencyIdToMonetaryCurrency(this.assetRegistryAPI, this, underlyingCurrencyId.unwrap());
            return underlyingCurrency;
        });
    }
    getLendTokenIdFromUnderlyingCurrency(currency) {
        return __awaiter(this, void 0, void 0, function* () {
            const currencyId = utils_1.newCurrencyId(this.api, currency);
            const { value } = yield this.api.query.loans.markets(currencyId);
            return value.lendTokenId;
        });
    }
    convertLendTokenToUnderlyingCurrency(amount, underlyingCurrencyId) {
        return __awaiter(this, void 0, void 0, function* () {
            const exchangeRate = yield this.api.query.loans.exchangeRate(underlyingCurrencyId);
            const decodedExchangeRate = utils_1.decodeFixedPointType(exchangeRate);
            return amount.mul(decodedExchangeRate);
        });
    }
    /**
     * Get lend position amounts in both underlying and lend currencies.
     *
     * @param accountId AccountId to get position information about
     * @param lendTokenId LendToken CurrencyId of the position
     * @param underlyingCurrencyId Underlying CurrencyId of the position
     * @returns Lend position amounts in underlying currency and lend token
     */
    getLendPositionAmounts(accountId, lendTokenId, underlyingCurrencyId) {
        return __awaiter(this, void 0, void 0, function* () {
            const lendTokenBalance = yield this.api.query.tokens.accounts(accountId, lendTokenId);
            const lendTokenBalanceTotal = lendTokenBalance.free.add(lendTokenBalance.reserved);
            const lendTokenBalanceInBig = big_js_1.default(lendTokenBalanceTotal.toString());
            const amountInUnderlying = yield this.convertLendTokenToUnderlyingCurrency(lendTokenBalanceInBig, underlyingCurrencyId);
            return [amountInUnderlying, lendTokenBalanceInBig];
        });
    }
    getLendTokens() {
        return __awaiter(this, void 0, void 0, function* () {
            const marketEntries = yield this.getLoansMarketsEntries();
            return Promise.all(marketEntries.map(([key, market]) => __awaiter(this, void 0, void 0, function* () {
                const lendTokenId = market.unwrap().lendTokenId;
                const underlyingCurrencyId = utils_1.storageKeyToNthInner(key);
                const underlyingCurrency = yield utils_1.currencyIdToMonetaryCurrency(this.assetRegistryAPI, this, underlyingCurrencyId);
                return DefaultLoansAPI.getLendTokenFromUnderlyingCurrency(underlyingCurrency, lendTokenId);
            })));
        });
    }
    _getLendPosition(accountId, underlyingCurrency, underlyingCurrencyId, lendTokenId) {
        return __awaiter(this, void 0, void 0, function* () {
            const [underlyingCurrencyAmount] = yield this.getLendPositionAmounts(accountId, lendTokenId, underlyingCurrencyId);
            // Returns null if position does not exist
            if (underlyingCurrencyAmount.eq(0)) {
                return null;
            }
            const accountDeposits = yield this.api.query.loans.accountDeposits(lendTokenId, accountId);
            const isCollateral = !accountDeposits.isZero();
            return {
                currency: underlyingCurrency,
                amount: utils_1.newMonetaryAmount(underlyingCurrencyAmount, underlyingCurrency),
                isCollateral,
            };
        });
    }
    _calculateAccumulatedDebt(borrowedAmount, snapshotBorrowIndex, currentBorrowIndex) {
        if (snapshotBorrowIndex.eq(0)) {
            return big_js_1.default(0);
        }
        // @note Formula for computing total debt: https://docs.parallel.fi/parallel-finance/#2.6-interest-rate-index
        // To compute only earned debt, subtract 1 from factor
        const factor = currentBorrowIndex.div(snapshotBorrowIndex).sub(1);
        return borrowedAmount.mul(factor);
    }
    _getBorrowPosition(accountId, underlyingCurrency, underlyingCurrencyId) {
        return __awaiter(this, void 0, void 0, function* () {
            const [borrowSnapshot, marketStatus] = yield Promise.all([
                this.api.query.loans.accountBorrows(underlyingCurrencyId, accountId),
                this.api.rpc.loans.getMarketStatus(underlyingCurrencyId),
            ]);
            const borrowedAmount = big_js_1.default(borrowSnapshot.principal.toString());
            if (borrowedAmount.eq(0)) {
                return null;
            }
            const snapshotBorrowIndex = big_js_1.default(utils_1.decodeFixedPointType(borrowSnapshot.borrowIndex));
            const currentBorrowIndex = big_js_1.default(utils_1.decodeFixedPointType(marketStatus[6]));
            const accumulatedDebt = this._calculateAccumulatedDebt(borrowedAmount, snapshotBorrowIndex, currentBorrowIndex);
            return {
                amount: utils_1.newMonetaryAmount(borrowedAmount, underlyingCurrency),
                currency: underlyingCurrency,
                accumulatedDebt: utils_1.newMonetaryAmount(accumulatedDebt, underlyingCurrency),
            };
        });
    }
    _getPositionsOfAccount(accountId, getSinglePosition) {
        return __awaiter(this, void 0, void 0, function* () {
            const marketsEntries = yield this.getLoansMarketsEntries();
            const marketsCurrencies = marketsEntries.map(([key, value]) => [
                utils_1.storageKeyToNthInner(key),
                value.unwrap().lendTokenId,
            ]);
            const allMarketsPositions = yield Promise.all(marketsCurrencies.map(([underlyingCurrencyId, lendTokenId]) => __awaiter(this, void 0, void 0, function* () {
                const underlyingCurrency = yield utils_1.currencyIdToMonetaryCurrency(this.assetRegistryAPI, this, underlyingCurrencyId);
                return getSinglePosition(accountId, underlyingCurrency, underlyingCurrencyId, lendTokenId);
            })));
            return allMarketsPositions.filter((position) => position !== null);
        });
    }
    getLendPositionsOfAccount(accountId) {
        return __awaiter(this, void 0, void 0, function* () {
            return this._getPositionsOfAccount(accountId, this._getLendPosition.bind(this));
        });
    }
    getBorrowPositionsOfAccount(accountId) {
        return __awaiter(this, void 0, void 0, function* () {
            return this._getPositionsOfAccount(accountId, this._getBorrowPosition.bind(this));
        });
    }
    _getLendApy(underlyingCurrencyId) {
        return __awaiter(this, void 0, void 0, function* () {
            const rawLendApy = yield this.api.query.loans.supplyRate(underlyingCurrencyId);
            // Return percentage
            return utils_1.decodeFixedPointType(rawLendApy).mul(100);
        });
    }
    _getBorrowApy(underlyingCurrencyId) {
        return __awaiter(this, void 0, void 0, function* () {
            const rawBorrowApy = yield this.api.query.loans.borrowRate(underlyingCurrencyId);
            // Return percentage
            return utils_1.decodeFixedPointType(rawBorrowApy).mul(100);
        });
    }
    _getTotalLiquidityCapacityAndBorrows(underlyingCurrency, underlyingCurrencyId) {
        return __awaiter(this, void 0, void 0, function* () {
            const lendTokenId = yield this.getLendTokenIdFromUnderlyingCurrency(underlyingCurrency);
            const [lendTokenTotalIssuance, totalBorrows, exchangeRate] = yield Promise.all([
                this.api.query.tokens.totalIssuance(lendTokenId),
                this.api.query.loans.totalBorrows(underlyingCurrencyId),
                this.api.query.loans.exchangeRate(underlyingCurrencyId),
            ]);
            const [totalLiquidity, availableCapacity] = this._calculateLiquidityAndCapacityAmounts(underlyingCurrency, big_js_1.default(lendTokenTotalIssuance.toString()), big_js_1.default(totalBorrows.toString()), utils_1.decodeFixedPointType(exchangeRate));
            const totalBorrowsMonetary = utils_1.newMonetaryAmount(totalBorrows.toString(), underlyingCurrency);
            return [totalLiquidity, availableCapacity, totalBorrowsMonetary];
        });
    }
    _calculateLiquidityAndCapacityAmounts(underlyingCurrency, lendTokenTotalIssuance, totalBorrows, exchangeRate) {
        const totalLiquidity = lendTokenTotalIssuance.mul(exchangeRate);
        // @note Available capacity to borrow is being computed in a different way
        // than in the runtime: https://docs.parallel.fi/parallel-finance/#2.1-internal-exchange-rate
        const availableCapacity = totalLiquidity.sub(totalBorrows);
        const totalLiquidityMonetary = utils_1.newMonetaryAmount(totalLiquidity, underlyingCurrency);
        const availableCapacityMonetary = utils_1.newMonetaryAmount(availableCapacity, underlyingCurrency);
        return [totalLiquidityMonetary, availableCapacityMonetary];
    }
    /**
     * Get the lend and borrow annual rewards for 1 UNIT of a given underlying currency id.
     *
     * @param underlyingCurrencyId currency id to get reward amounts for.
     * @returns Annualized lend and borrow rewards for 1 unit of the given underlying currency.
     */
    _getLendAndBorrowYearlyRewardAmount(underlyingCurrencyId, totalLiquidity, totalBorrows) {
        return __awaiter(this, void 0, void 0, function* () {
            const blockTimeMs = (yield this.api.call.auraApi.slotDuration()).toNumber();
            const [lendRewardPerPool, borrowRewardPerPool] = (yield Promise.all([
                this.api.query.loans.rewardSupplySpeed(underlyingCurrencyId),
                this.api.query.loans.rewardBorrowSpeed(underlyingCurrencyId),
            ]))
                .map((rewardSpeedRaw) => big_js_1.default(rewardSpeedRaw.toString()))
                .map((rewardSpeed) => this._calculateAnnualizedRewardAmount(rewardSpeed, blockTimeMs));
            // @note could be refactored to compute APR in lib if we can get underlyingCurrency/rewardCurrency exchange rate,
            // but is it safe to assume that exchange rate for btc/underlyingCurrency will be
            // always fed to the oracle and available?
            // Return rate per 1 UNIT of underlying currency and compute APR
            // on UI where all exchange rates are available.
            const lendRewardPerUnit = totalLiquidity.eq(0) ? lendRewardPerPool : lendRewardPerPool.div(totalLiquidity);
            const borrowRewardPerUnit = totalBorrows.eq(0) ? borrowRewardPerPool : borrowRewardPerPool.div(totalBorrows);
            return [lendRewardPerUnit, borrowRewardPerUnit];
        });
    }
    _calculateAnnualizedRewardAmount(amountPerBlock, blockTimeMs) {
        const blocksPerYear = utils_1.MS_PER_YEAR.div(blockTimeMs);
        return amountPerBlock.mul(blocksPerYear);
    }
    _getRewardCurrency() {
        return __awaiter(this, void 0, void 0, function* () {
            const rewardCurrencyId = this.api.consts.loans.rewardAssetId;
            return utils_1.currencyIdToMonetaryCurrency(this.assetRegistryAPI, this, rewardCurrencyId);
        });
    }
    _getSubsidyReward(amount, rewardCurrency) {
        if (amount.eq(0)) {
            return null;
        }
        // Assumes native currency of parachain is always reward currency.
        return utils_1.newMonetaryAmount(amount, rewardCurrency);
    }
    _getLoanAsset(underlyingCurrencyId, marketData) {
        return __awaiter(this, void 0, void 0, function* () {
            const underlyingCurrency = yield utils_1.currencyIdToMonetaryCurrency(this.assetRegistryAPI, this, underlyingCurrencyId);
            const [lendApy, borrowApy, [totalLiquidity, availableCapacity, totalBorrows], rewardCurrency] = yield Promise.all([
                this._getLendApy(underlyingCurrencyId),
                this._getBorrowApy(underlyingCurrencyId),
                this._getTotalLiquidityCapacityAndBorrows(underlyingCurrency, underlyingCurrencyId),
                this._getRewardCurrency(),
            ]);
            // Format data.
            const liquidationThreshold = utils_1.decodePermill(marketData.liquidationThreshold);
            const collateralThreshold = utils_1.decodePermill(marketData.collateralFactor);
            const [lendRewardAmountYearly, borrowRewardAmountYearly] = yield this._getLendAndBorrowYearlyRewardAmount(underlyingCurrencyId, totalLiquidity.toBig(), totalBorrows.toBig());
            const lendReward = this._getSubsidyReward(lendRewardAmountYearly, rewardCurrency);
            const borrowReward = this._getSubsidyReward(borrowRewardAmountYearly, rewardCurrency);
            return [
                underlyingCurrency,
                {
                    isActive: marketData.state.isActive,
                    lendApy,
                    borrowApy,
                    currency: underlyingCurrency,
                    totalLiquidity,
                    availableCapacity,
                    totalBorrows,
                    liquidationThreshold,
                    collateralThreshold,
                    lendReward,
                    borrowReward,
                },
            ];
        });
    }
    getLoanAssets() {
        return __awaiter(this, void 0, void 0, function* () {
            const marketsEntries = yield this.getLoansMarketsEntries();
            const loanAssetsArray = yield Promise.all(marketsEntries.map(([key, marketData]) => this._getLoanAsset(utils_1.storageKeyToNthInner(key), marketData.unwrap())));
            const loanAssets = loanAssetsArray.reduce((result, [currency, loanAsset]) => (Object.assign(Object.assign({}, result), { [currency.ticker]: loanAsset })), {});
            return loanAssets;
        });
    }
    getAccruedRewardsOfAccount(accountId) {
        return __awaiter(this, void 0, void 0, function* () {
            const [rewardAccrued, rewardCurrency] = yield Promise.all([
                this.api.query.loans.rewardAccrued(accountId),
                this._getRewardCurrency(),
            ]);
            return utils_1.newMonetaryAmount(rewardAccrued.toString(), rewardCurrency);
        });
    }
    // Check that market for given currency is added and in active state.
    _checkMarketState(currency, action) {
        return __awaiter(this, void 0, void 0, function* () {
            const underlyingCurrencyId = utils_1.newCurrencyId(this.api, currency);
            const underlyingCurrencyMarket = yield this.api.query.loans.markets(underlyingCurrencyId);
            if (underlyingCurrencyMarket.isNone) {
                throw new Error(`Cannot ${action} currency that does not have market created`);
            }
            if (underlyingCurrencyMarket.unwrap().state.isPending) {
                throw new Error(`Cannot ${action} currency that does not have active market.`);
            }
        });
    }
    lend(underlyingCurrency, amount) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._checkMarketState(underlyingCurrency, "lend");
            const underlyingCurrencyId = utils_1.newCurrencyId(this.api, underlyingCurrency);
            const lendExtrinsic = this.api.tx.loans.mint(underlyingCurrencyId, amount.toString(true));
            yield this.transactionAPI.sendLogged(lendExtrinsic, this.api.events.loans.Deposited, true);
        });
    }
    withdraw(underlyingCurrency, amount) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._checkMarketState(underlyingCurrency, "withdraw");
            const underlyingCurrencyId = utils_1.newCurrencyId(this.api, underlyingCurrency);
            const withdrawExtrinsic = this.api.tx.loans.redeem(underlyingCurrencyId, amount.toString(true));
            yield this.transactionAPI.sendLogged(withdrawExtrinsic, this.api.events.loans.Redeemed, true);
        });
    }
    withdrawAll(underlyingCurrency) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._checkMarketState(underlyingCurrency, "withdraw");
            const underlyingCurrencyId = utils_1.newCurrencyId(this.api, underlyingCurrency);
            const withdrawExtrinsic = this.api.tx.loans.redeemAll(underlyingCurrencyId);
            yield this.transactionAPI.sendLogged(withdrawExtrinsic, this.api.events.loans.Redeemed, true);
        });
    }
    enableAsCollateral(underlyingCurrency) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._checkMarketState(underlyingCurrency, "enable collateral");
            const underlyingCurrencyId = utils_1.newCurrencyId(this.api, underlyingCurrency);
            const enableCollateralExtrinsic = this.api.tx.loans.depositAllCollateral(underlyingCurrencyId);
            yield this.transactionAPI.sendLogged(enableCollateralExtrinsic, this.api.events.loans.DepositCollateral, true);
        });
    }
    disableAsCollateral(underlyingCurrency) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._checkMarketState(underlyingCurrency, "disable collateral");
            const underlyingCurrencyId = utils_1.newCurrencyId(this.api, underlyingCurrency);
            const enableCollateralExtrinsic = this.api.tx.loans.withdrawAllCollateral(underlyingCurrencyId);
            yield this.transactionAPI.sendLogged(enableCollateralExtrinsic, this.api.events.loans.WithdrawCollateral, true);
        });
    }
    claimAllSubsidyRewards() {
        return __awaiter(this, void 0, void 0, function* () {
            const claimRewardsExtrinsic = this.api.tx.loans.claimReward();
            yield this.transactionAPI.sendLogged(claimRewardsExtrinsic, this.api.events.loans.RewardPaid, true);
        });
    }
    borrow(underlyingCurrency, amount) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._checkMarketState(underlyingCurrency, "borrow");
            const underlyingCurrencyId = utils_1.newCurrencyId(this.api, underlyingCurrency);
            const borrowExtrinsic = this.api.tx.loans.borrow(underlyingCurrencyId, amount.toString(true));
            yield this.transactionAPI.sendLogged(borrowExtrinsic, this.api.events.loans.Borrowed, true);
        });
    }
    repay(underlyingCurrency, amount) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._checkMarketState(underlyingCurrency, "repay debt of");
            const underlyingCurrencyId = utils_1.newCurrencyId(this.api, underlyingCurrency);
            const repay = this.api.tx.loans.repayBorrow(underlyingCurrencyId, amount.toString(true));
            yield this.transactionAPI.sendLogged(repay, this.api.events.loans.RepaidBorrow, true);
        });
    }
    repayAll(underlyingCurrency) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._checkMarketState(underlyingCurrency, "repay all debt of");
            const underlyingCurrencyId = utils_1.newCurrencyId(this.api, underlyingCurrency);
            const repayAllExtrinsic = this.api.tx.loans.repayBorrowAll(underlyingCurrencyId);
            yield this.transactionAPI.sendLogged(repayAllExtrinsic, this.api.events.loans.RepaidBorrow, true);
        });
    }
    liquidateBorrowPosition(borrower, liquidationCurrency, repayAmount, collateralCurrency) {
        return __awaiter(this, void 0, void 0, function* () {
            const liquidationCurrencyId = utils_1.newCurrencyId(this.api, liquidationCurrency);
            const collateralCurrencyId = utils_1.newCurrencyId(this.api, collateralCurrency);
            const rawAmount = repayAmount.toString(true);
            const liquidateBorrowExtrinsic = this.api.tx.loans.liquidateBorrow(borrower, liquidationCurrencyId, rawAmount, collateralCurrencyId);
            yield this.transactionAPI.sendLogged(liquidateBorrowExtrinsic, this.api.events.loans.LiquidatedBorrow, true);
        });
    }
}
exports.DefaultLoansAPI = DefaultLoansAPI;
//# sourceMappingURL=loans.js.map