From dbf08a5729f1c42b3a6285de4719a6b0f1901694 Mon Sep 17 00:00:00 2001 From: bartosz-lipinski <264380+bartosz-lipinski@users.noreply.github.com> Date: Wed, 16 Dec 2020 21:55:22 -0600 Subject: [PATCH] feat: ltv and health factor --- src/components/BorrowInput/index.tsx | 2 +- src/components/LiquidateInput/index.tsx | 5 +- src/components/RepayInput/index.tsx | 7 +- src/contexts/market.tsx | 29 ++++-- src/hooks/index.ts | 2 +- src/hooks/useEnrichedLendingObligations.ts | 102 +++++++++++++++++++++ src/hooks/useLiquidableObligations.ts | 62 ------------- src/hooks/useUserObligations.ts | 4 +- src/models/lending/reserve.ts | 10 +- src/utils/utils.ts | 6 +- src/views/dashboard/index.tsx | 1 + src/views/dashboard/obligationItem.tsx | 15 ++- src/views/dashboard/style.less | 10 +- src/views/liquidate/index.tsx | 10 +- src/views/liquidate/item.tsx | 22 ++--- src/views/liquidateReserve/index.tsx | 4 +- src/views/repayReserve/index.tsx | 4 +- 17 files changed, 173 insertions(+), 122 deletions(-) create mode 100644 src/hooks/useEnrichedLendingObligations.ts delete mode 100644 src/hooks/useLiquidableObligations.ts diff --git a/src/components/BorrowInput/index.tsx b/src/components/BorrowInput/index.tsx index a59db0e..a6eea6b 100644 --- a/src/components/BorrowInput/index.tsx +++ b/src/components/BorrowInput/index.tsx @@ -77,7 +77,7 @@ export const BorrowInput = (props: { // TODO: select exsisting obligations by collateral reserve userObligationsByReserve.length > 0 - ? userObligationsByReserve[0].obligation + ? userObligationsByReserve[0].obligation.account : undefined, userObligationsByReserve.length > 0 diff --git a/src/components/LiquidateInput/index.tsx b/src/components/LiquidateInput/index.tsx index a761c83..27589df 100644 --- a/src/components/LiquidateInput/index.tsx +++ b/src/components/LiquidateInput/index.tsx @@ -4,7 +4,8 @@ import React, { useCallback } from "react"; import { useState } from "react"; import { LABELS } from "../../constants"; import { ParsedAccount } from "../../contexts/accounts"; -import { LendingObligation, LendingReserve } from "../../models"; +import { EnrichedLendingObligation } from "../../hooks"; +import { LendingReserve } from "../../models"; import { BackButton } from "../BackButton"; import { CollateralSelector } from "../CollateralSelector"; import "./style.less"; @@ -13,7 +14,7 @@ export const LiquidateInput = (props: { className?: string; reserve: ParsedAccount; collateralReserve?: ParsedAccount; - obligation: ParsedAccount; + obligation: EnrichedLendingObligation; }) => { const { reserve, collateralReserve } = props; diff --git a/src/components/RepayInput/index.tsx b/src/components/RepayInput/index.tsx index 23f3b51..b9cf1d9 100644 --- a/src/components/RepayInput/index.tsx +++ b/src/components/RepayInput/index.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useState } from "react"; -import { InputType, useAccountByMint, useSliderInput, useTokenName, useUserBalance } from "../../hooks"; +import { EnrichedLendingObligation, InputType, useAccountByMint, useSliderInput, useTokenName, useUserBalance } from "../../hooks"; import { - LendingObligation, LendingReserve, } from "../../models"; import { TokenIcon } from "../TokenIcon"; @@ -22,7 +21,7 @@ export const RepayInput = (props: { className?: string; borrowReserve: ParsedAccount; collateralReserve?: ParsedAccount; - obligation: ParsedAccount; + obligation: EnrichedLendingObligation; }) => { const connection = useConnection(); const { wallet } = useWallet(); @@ -82,7 +81,7 @@ export const RepayInput = (props: { await repay( fromAccounts[0], toRepayLamports, - obligation, + obligation.account, obligationAccount, repayReserve, collateralReserve, diff --git a/src/contexts/market.tsx b/src/contexts/market.tsx index f0816ea..9c02426 100644 --- a/src/contexts/market.tsx +++ b/src/contexts/market.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useContext, useEffect, useState } from "react"; import { MINT_TO_MARKET } from "./../models/marketOverrides"; -import { STABLE_COINS } from "./../utils/utils"; +import { fromLamports, STABLE_COINS } from "./../utils/utils"; import { useConnectionConfig } from "./connection"; import { cache, getMultipleAccounts, ParsedAccount } from "./accounts"; import { Market, MARKETS, Orderbook, TOKEN_MINTS } from "@project-serum/serum"; @@ -249,8 +249,14 @@ export const usePrecacheMarket = () => { return context.precacheMarkets; }; -export const simulateMarketOrderFill = (amount: number, reserve: LendingReserve) => { - const marketInfo = cache.get(reserve?.dexMarket); +export const simulateMarketOrderFill = (amount: number, reserve: LendingReserve, dex: PublicKey) => { + const liquidityMint = cache.get(reserve.liquidityMint); + const collateralMint = cache.get(reserve.collateralMint); + if (!liquidityMint || !collateralMint) { + return 0.0; + } + + const marketInfo = cache.get(dex); if (!marketInfo) { return 0.0; } @@ -287,13 +293,20 @@ export const simulateMarketOrderFill = (amount: number, reserve: LendingReserve) const book = new Orderbook(dexMarket, bookInfo.accountFlags, bookInfo.slab); let cost = 0; - let remaining = amount; + let remaining = fromLamports(amount, liquidityMint.info); if (book) { - for (const level of book) { - let size = remaining > level.size ? level.size : level.size - remaining; - cost = cost + level.price * size; - remaining = remaining - size; + const depth = book.getL2(1000); + let price, sizeAtLevel: number; + + const op = book.isBids ? + (price: number, size: number) => size / price : + (price: number, size: number) => size * price; + + for ([price, sizeAtLevel] of depth) { + let filled = remaining > sizeAtLevel ? sizeAtLevel : remaining; + cost = cost + op(price, filled); + remaining = remaining - filled; if (remaining <= 0) { break; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 1701e6f..0a54656 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -10,4 +10,4 @@ export * from "./useUserObligationByReserve"; export * from "./useBorrowedAmount"; export * from "./useUserDeposits"; export * from "./useSliderInput"; -export * from "./useLiquidableObligations"; +export * from "./useEnrichedLendingObligations"; diff --git a/src/hooks/useEnrichedLendingObligations.ts b/src/hooks/useEnrichedLendingObligations.ts new file mode 100644 index 0000000..1382763 --- /dev/null +++ b/src/hooks/useEnrichedLendingObligations.ts @@ -0,0 +1,102 @@ +import { PublicKey } from "@solana/web3.js"; +import { useMemo } from "react"; +import { cache, ParsedAccount } from "./../contexts/accounts"; +import { useLendingObligations } from "./useLendingObligations"; +import { collateralToLiquidity, LendingObligation, LendingReserve } from "../models/lending"; +import { useLendingReserves } from "./useLendingReserves"; +import { fromLamports, wadToLamports } from "../utils/utils"; +import { MintInfo } from "@solana/spl-token"; +import { simulateMarketOrderFill } from "../contexts/market"; + +interface EnrichedLendingObligationInfo extends LendingObligation { + ltv: number; + health: number +} + +export interface EnrichedLendingObligation { + account: ParsedAccount; + info: EnrichedLendingObligationInfo; +} + +export function useEnrichedLendingObligations() { + const { obligations } = useLendingObligations(); + const { reserveAccounts } = useLendingReserves(); + + const availableReserves = useMemo(() => { + return reserveAccounts.reduce((map, reserve) => { + map.set(reserve.pubkey.toBase58(), reserve); + return map; + }, new Map>()) + }, [reserveAccounts]); + + // TODO: subscribe to market updates + + const enrichedObligations = useMemo(() => { + if (availableReserves.size === 0) { + return []; + } + + return obligations + .map(obligation => ( + { + obligation, + reserve: availableReserves.get(obligation.info.borrowReserve.toBase58()) as ParsedAccount, + collateralReserve: availableReserves.get(obligation.info.collateralReserve.toBase58()) as ParsedAccount + } + )) + // use obligations with reserves available + .filter(item => item.reserve) + // use reserves with borrow amount greater than zero + .filter(item => wadToLamports(item.obligation.info.borrowAmountWad).toNumber() > 0) + .map(item => { + const obligation = item.obligation; + const reserve = item.reserve.info; + const liquidityMint = cache.get(reserve.liquidityMint) as ParsedAccount; + let ltv = 0; + + if(liquidityMint) { + const collateral = fromLamports( + collateralToLiquidity(obligation.info.depositedCollateral, item.reserve.info), + cache.get(item.collateralReserve.info.liquidityMint)?.info); + + const borrowed = wadToLamports(obligation.info.borrowAmountWad).toNumber(); + + const borrowedAmount = simulateMarketOrderFill( + borrowed, + item.reserve.info, + item.reserve.info.dexMarketOption ? item.reserve.info.dexMarket : item.collateralReserve.info.dexMarket + ); + + ltv = 100 * collateral / borrowedAmount; + } + + const liquidationThreshold = item.reserve.info.config.liquidationThreshold; + const health = ltv / liquidationThreshold + return { + account: obligation, + info: { + ...obligation.info, + ltv, + health, + } + } as EnrichedLendingObligation; + }) + .sort((a, b) => a.info.health - b.info.health); + }, [obligations, availableReserves]); + + + return { + obligations: enrichedObligations, + }; +} + +export function useEnrichedLendingObligation(address?: string | PublicKey) { + const id = typeof address === "string" ? address : address?.toBase58(); + const { obligations } = useEnrichedLendingObligations(); + + const obligation = useMemo(() => { + return obligations.find(ob => ob.account.pubkey.toBase58() === id); + }, [obligations, id]); + + return obligation; +} diff --git a/src/hooks/useLiquidableObligations.ts b/src/hooks/useLiquidableObligations.ts deleted file mode 100644 index f3924ff..0000000 --- a/src/hooks/useLiquidableObligations.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useMemo } from "react"; -import { useLendingObligations } from "./useLendingObligations"; -import { collateralToLiquidity, LendingReserve } from "../models/lending"; -import { useLendingReserves } from "./useLendingReserves"; -import { ParsedAccount } from "../contexts/accounts"; -import { wadToLamports } from "../utils/utils"; -import { simulateMarketOrderFill } from "../contexts/market"; - -export const useLiquidableObligations = () => { - const { obligations } = useLendingObligations(); - const { reserveAccounts } = useLendingReserves(); - - const availableReserves = useMemo(() => { - return reserveAccounts.reduce((map, reserve) => { - map.set(reserve.pubkey.toBase58(), reserve); - return map; - }, new Map>()) - }, [reserveAccounts]) - - const liquidableObligations = useMemo(() => { - if (availableReserves.size === 0) { - return []; - } - - return obligations - .map(obligation => ( - { - obligation, - reserve: availableReserves.get(obligation.info.borrowReserve.toBase58()) as ParsedAccount - } - )) - // use obligations with reserves available - .filter(item => item.reserve) - // use reserves with borrow amount greater than zero - .filter(item => wadToLamports(item.obligation.info.borrowAmountWad).toNumber() > 0) - .map(item => { - const obligation = item.obligation; - const reserve = item.reserve.info; - const collateralLamports = collateralToLiquidity(obligation.info.depositedCollateral, reserve); - const cost = simulateMarketOrderFill(collateralLamports, reserve); - - console.log(cost); - - // TODO: calculate LTV - const ltv = 81; - const liquidationThreshold = item.reserve.info.config.liquidationThreshold; - const health = (ltv - liquidationThreshold) / liquidationThreshold - return { - obligation: item.obligation, - ltv, - liquidationThreshold, - health - } - }) - .filter(item => item.ltv > item.liquidationThreshold) - .sort((a, b) => b.health - a.health); - }, [obligations, availableReserves]); - - return { - liquidableObligations - }; -} \ No newline at end of file diff --git a/src/hooks/useUserObligations.ts b/src/hooks/useUserObligations.ts index fb49dd3..2a7524c 100644 --- a/src/hooks/useUserObligations.ts +++ b/src/hooks/useUserObligations.ts @@ -1,11 +1,11 @@ import { useMemo } from "react"; import { useUserAccounts } from "./useUserAccounts"; -import { useLendingObligations } from "./useLendingObligations"; +import { useEnrichedLendingObligations } from "./useEnrichedLendingObligations"; import { TokenAccount } from "../models"; export function useUserObligations() { const { userAccounts } = useUserAccounts(); - const { obligations } = useLendingObligations(); + const { obligations } = useEnrichedLendingObligations(); const accountsByMint = useMemo(() => { return userAccounts.reduce((res, acc) => { diff --git a/src/models/lending/reserve.ts b/src/models/lending/reserve.ts index 0e20f10..deb99a7 100644 --- a/src/models/lending/reserve.ts +++ b/src/models/lending/reserve.ts @@ -188,11 +188,13 @@ export const collateralExchangeRate = (reserve?: LendingReserve) => { return (reserve?.collateralMintSupply.toNumber() || 1) / reserveMarketCap(reserve); } -export const collateralToLiquidity = (collateralAmount: BN, reserve?: LendingReserve) => { - return Math.floor(collateralAmount.toNumber() / collateralExchangeRate(reserve)); +export const collateralToLiquidity = (collateralAmount: BN | number, reserve?: LendingReserve) => { + const amount = typeof collateralAmount === 'number' ? collateralAmount : collateralAmount.toNumber(); + return Math.floor(amount / collateralExchangeRate(reserve)); } -export const liquidityToCollateral = (liquidityAmount: BN, reserve?: LendingReserve) => { - return Math.floor(liquidityAmount.toNumber() * collateralExchangeRate(reserve)); +export const liquidityToCollateral = (liquidityAmount: BN | number, reserve?: LendingReserve) => { + const amount = typeof liquidityAmount === 'number' ? liquidityAmount : liquidityAmount.toNumber(); + return Math.floor(amount * collateralExchangeRate(reserve)); } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 712257b..de2e3ba 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -104,7 +104,7 @@ export function toLamports( typeof account === "number" ? account : account.info.amount?.toNumber(); const precision = Math.pow(10, mint?.decimals || 0); - return amount * precision; + return Math.floor(amount * precision); } export function wadToLamports(amount?: BN): BN { @@ -120,12 +120,12 @@ export function fromLamports( return 0; } - const amount = + const amount = Math.floor( typeof account === "number" ? account : BN.isBN(account) ? account.toNumber() - : account.info.amount.toNumber(); + : account.info.amount.toNumber()); const precision = Math.pow(10, mint?.decimals || 0); return (amount / precision) * rate; diff --git a/src/views/dashboard/index.tsx b/src/views/dashboard/index.tsx index 3cddcb4..6f0c122 100644 --- a/src/views/dashboard/index.tsx +++ b/src/views/dashboard/index.tsx @@ -43,6 +43,7 @@ export const DashboardView = () => {
{LABELS.TABLE_TITLE_YOUR_LOAN_BALANCE}
{LABELS.TABLE_TITLE_COLLATERAL_BALANCE}
{LABELS.TABLE_TITLE_APY}
+
{LABELS.TABLE_TITLE_LTV}
{LABELS.TABLE_TITLE_ACTION}
{userObligations.map((item) => { diff --git a/src/views/dashboard/obligationItem.tsx b/src/views/dashboard/obligationItem.tsx index e03c503..ff2cd69 100644 --- a/src/views/dashboard/obligationItem.tsx +++ b/src/views/dashboard/obligationItem.tsx @@ -1,9 +1,8 @@ import React, { useMemo } from "react"; -import { useTokenName } from "../../hooks"; +import { EnrichedLendingObligation, useTokenName } from "../../hooks"; import { calculateBorrowAPY, collateralToLiquidity, - LendingObligation, LendingReserve, } from "../../models/lending"; import { TokenIcon } from "../../components/TokenIcon"; @@ -18,7 +17,7 @@ import { Link } from "react-router-dom"; import { cache, ParsedAccount, useMint } from "../../contexts/accounts"; export const ObligationItem = (props: { - obligation: ParsedAccount; + obligation: EnrichedLendingObligation; }) => { const { obligation } = props; @@ -51,17 +50,14 @@ export const ObligationItem = (props: { return (
- -
+ +
- {collateralName} - / - {borrowName}
{formatNumber.format(borrowAmount)} {borrowName} @@ -70,13 +66,14 @@ export const ObligationItem = (props: { {formatNumber.format(collateral)} {collateralName}
{formatPct.format(borrowAPY)}
+
{formatPct.format(obligation.info.ltv / 100)}
- + diff --git a/src/views/dashboard/style.less b/src/views/dashboard/style.less index d30a82d..2c1c2d0 100644 --- a/src/views/dashboard/style.less +++ b/src/views/dashboard/style.less @@ -4,13 +4,13 @@ align-items: center; & > div, span { - flex: 20%; + flex: 15%; height: 22px; text-align: right; } & > :first-child { - flex: 120px + flex: 50px; } & > :last-child { @@ -22,17 +22,17 @@ margin: 0px 30px; & > div { - flex: 20%; + flex: 15%; text-align: right; } & > :first-child { text-align: left; - flex: 120px + flex: 50px; } & > :last-child { - flex: 200px + flex: 200px; } } diff --git a/src/views/liquidate/index.tsx b/src/views/liquidate/index.tsx index c5f8d3b..babfac4 100644 --- a/src/views/liquidate/index.tsx +++ b/src/views/liquidate/index.tsx @@ -1,15 +1,15 @@ import React from "react"; import { LABELS } from "../../constants"; import { LiquidateItem } from "./item"; -import { useLiquidableObligations } from "./../../hooks"; +import { useEnrichedLendingObligations } from "./../../hooks"; import "./style.less"; export const LiquidateView = () => { - const { liquidableObligations } = useLiquidableObligations(); + const { obligations } = useEnrichedLendingObligations(); return (
- {liquidableObligations.length === 0 ? ( + {obligations.length === 0 ? (
{LABELS.LIQUIDATE_NO_LOANS}
) : (
@@ -20,8 +20,8 @@ export const LiquidateView = () => {
{LABELS.TABLE_TITLE_LTV}
{LABELS.TABLE_TITLE_ACTION}
- {liquidableObligations.map((item) => ( - + {obligations.map((item) => ( + ))}
)} diff --git a/src/views/liquidate/item.tsx b/src/views/liquidate/item.tsx index 34859dd..e63ddc8 100644 --- a/src/views/liquidate/item.tsx +++ b/src/views/liquidate/item.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from "react"; import { cache, ParsedAccount, useMint } from "../../contexts/accounts"; -import { LendingObligation, LendingReserve, calculateBorrowAPY } from "../../models/lending"; -import { useTokenName } from "../../hooks"; +import { LendingReserve, calculateBorrowAPY } from "../../models/lending"; +import { EnrichedLendingObligation, useTokenName } from "../../hooks"; import { Link } from "react-router-dom"; import { Button, Card } from "antd"; import { TokenIcon } from "../../components/TokenIcon"; @@ -14,25 +14,23 @@ import { import { LABELS } from "../../constants"; export const LiquidateItem = (props: { - obligation: ParsedAccount; - ltv: number + item: EnrichedLendingObligation }) => { - - const { obligation, ltv } = props; + let obligation = props.item.info const borrowReserve = cache.get( - obligation.info.borrowReserve + obligation.borrowReserve ) as ParsedAccount; const collateralReserve = cache.get( - obligation.info.collateralReserve + obligation.collateralReserve ) as ParsedAccount; const tokenName = useTokenName(borrowReserve?.info.liquidityMint); const liquidityMint = useMint(borrowReserve.info.liquidityMint); const borrowAmount = fromLamports( - wadToLamports(obligation.info.borrowAmountWad), + wadToLamports(obligation.borrowAmountWad), liquidityMint ); @@ -44,7 +42,7 @@ export const LiquidateItem = (props: { const collateralName = useTokenName(collateralReserve?.info.liquidityMint); return ( - +
@@ -56,7 +54,7 @@ export const LiquidateItem = (props: {
{collateralName} - / + → {borrowName}
@@ -66,7 +64,7 @@ export const LiquidateItem = (props: { {formatPct.format(borrowAPY)}
- {formatPct.format(ltv / 100)} + {formatPct.format(obligation.ltv / 100)}