From cbdccb18f89aecc628cfb934e1c1f6be397d46db Mon Sep 17 00:00:00 2001 From: "Sebastian.Bor" Date: Sat, 5 Dec 2020 19:53:11 +0000 Subject: [PATCH 1/8] feat: Add Liquidate view --- src/components/Layout/index.tsx | 15 +++++++++++++-- src/constants/labels.ts | 1 + src/routes.tsx | 6 ++++++ src/views/index.tsx | 1 + src/views/liquidate/index.tsx | 13 +++++++++++++ 5 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 src/views/liquidate/index.tsx diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 37f5cd4..f305c0e 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -6,6 +6,7 @@ import { GithubOutlined, BankOutlined, LogoutOutlined, + LoginOutlined, HomeOutlined, RocketOutlined, } from "@ant-design/icons"; @@ -25,7 +26,8 @@ export const AppLayout = (props: any) => { "/dashboard": "2", "/deposit": "3", "/borrow": "4", - "/faucet": "4", + "/liquidate": "5", + "/faucet": "6", }; const current = @@ -92,8 +94,17 @@ export const AppLayout = (props: any) => { {LABELS.MENU_BORROW} + }> + + {LABELS.MENU_LIQUIDATE} + + {env !== "mainnet-beta" && ( - }> + }> } /> + } + /> } /> diff --git a/src/views/index.tsx b/src/views/index.tsx index 41c9247..df92ae2 100644 --- a/src/views/index.tsx +++ b/src/views/index.tsx @@ -8,3 +8,4 @@ export { ReserveView } from "./reserve"; export { WithdrawView } from "./withdraw"; export { FaucetView } from "./faucet"; export { RepayReserveView } from "./repayReserve"; +export { LiquidateView } from "./liquidate"; diff --git a/src/views/liquidate/index.tsx b/src/views/liquidate/index.tsx new file mode 100644 index 0000000..07d0886 --- /dev/null +++ b/src/views/liquidate/index.tsx @@ -0,0 +1,13 @@ +import React from "react"; + + +export const LiquidateView = () => { + + return ( +
+
Liquidation
+
+ + ); + +}; \ No newline at end of file From ccc78fc6ae2d59c2ba3c0d6c7bafa5a171f25d21 Mon Sep 17 00:00:00 2001 From: "Sebastian.Bor" Date: Sat, 5 Dec 2020 23:00:43 +0000 Subject: [PATCH 2/8] feat: add liquidate view item --- src/components/Layout/index.tsx | 2 +- src/constants/labels.ts | 4 +- src/views/dashboard/index.tsx | 2 +- src/views/liquidate/index.tsx | 43 ++++++++++++++++++--- src/views/liquidate/item.tsx | 60 ++++++++++++++++++++++++++++++ src/views/liquidate/itemStyle.less | 29 +++++++++++++++ 6 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 src/views/liquidate/item.tsx create mode 100644 src/views/liquidate/itemStyle.less diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index f305c0e..6401b12 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -102,7 +102,7 @@ export const AppLayout = (props: any) => { > {LABELS.MENU_LIQUIDATE} -
+
{env !== "mainnet-beta" && ( }> { {LABELS.DASHBOARD_TITLE_LOANS}
{LABELS.TABLE_TITLE_ASSET}
-
{LABELS.TABLE_TITLE_LOAN_BALANCE}
+
{LABELS.TABLE_TITLE_YOUR_LOAN_BALANCE}
{LABELS.TABLE_TITLE_APY}
{LABELS.TABLE_TITLE_ACTION}
diff --git a/src/views/liquidate/index.tsx b/src/views/liquidate/index.tsx index 07d0886..1302dbf 100644 --- a/src/views/liquidate/index.tsx +++ b/src/views/liquidate/index.tsx @@ -1,13 +1,44 @@ +import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; import React from "react"; - +import { LABELS, ZERO } from "../../constants"; +import { LiquidateItem } from "./item"; +import "./itemStyle.less"; export const LiquidateView = () => { + // ParsedAccount + const obligations = [ + { + pubkey: new PublicKey("2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW"), + account: { + executable: false, + owner: new PublicKey("2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW"), + lamports: 0, + data: new Buffer("x"), + }, + info: { + lastUpdateSlot: ZERO, + collateralAmount: ZERO, + collateralSupply: new PublicKey("2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW"), + cumulativeBorrowRateWad: ZERO, + borrowAmountWad: new BN(0), + borrowReserve: new PublicKey("EwhnKnkwcAeVxHDbR5wMpjwipHFuafxTUhQaaagjUxQG"), + tokenMint: new PublicKey("2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW"), + } + } + ]; return ( -
-
Liquidation
+
+
+
{LABELS.TABLE_TITLE_ASSET}
+
{LABELS.TABLE_TITLE_LOAN_BALANCE}
+
{LABELS.TABLE_TITLE_APY}
+
{LABELS.TABLE_TITLE_ACTION}
+
+ {obligations.map((obligation) => ( + + ))}
- ); - -}; \ No newline at end of file +}; diff --git a/src/views/liquidate/item.tsx b/src/views/liquidate/item.tsx new file mode 100644 index 0000000..6952424 --- /dev/null +++ b/src/views/liquidate/item.tsx @@ -0,0 +1,60 @@ +import React, { useMemo } from "react"; +import { cache, ParsedAccount, useMint } from "../../contexts/accounts"; +import { LendingObligation, LendingReserve, calculateBorrowAPY } from "../../models/lending"; +import { useTokenName } from "../../hooks"; +import { Link } from "react-router-dom"; +import { Button, Card } from "antd"; +import { TokenIcon } from "../../components/TokenIcon"; +import { + wadToLamports, + formatNumber, + fromLamports, + formatPct, +} from "../../utils/utils"; +import { LABELS } from "../../constants"; + +export const LiquidateItem = (props: { + obligation: ParsedAccount; +}) => { + + const { obligation } = props; + + const borrowReserve = cache.get(obligation.info.borrowReserve) as ParsedAccount; + const tokenName = useTokenName(borrowReserve?.info.liquidityMint); + const liquidityMint = useMint(borrowReserve.info.liquidityMint); + + console.log("wad",obligation.info.borrowAmountWad) + + const borrowAmount = fromLamports( + wadToLamports(obligation.info.borrowAmountWad), + liquidityMint + ); + + const borrowAPY = useMemo(() => calculateBorrowAPY(borrowReserve.info), [ + borrowReserve, + ]); + + return ( + + +
+ + + {tokenName} + +
+ {formatNumber.format(borrowAmount)} {tokenName} +
+
+ {formatPct.format(borrowAPY)} +
+
+ +
+
+
+ + ); +}; \ No newline at end of file diff --git a/src/views/liquidate/itemStyle.less b/src/views/liquidate/itemStyle.less new file mode 100644 index 0000000..1c8178a --- /dev/null +++ b/src/views/liquidate/itemStyle.less @@ -0,0 +1,29 @@ +.liquidate-item { + display: flex; + justify-content: space-between; + align-items: center; + + & > div, span { + flex: 20%; + height: 22px; + text-align: right; + } + + & > :first-child { + flex: 80px + } +} + +.liquidate-header { + margin: 0px 30px; + + & > div { + flex: 20%; + text-align: right; + } + + & > :first-child { + text-align: left; + flex: 80px + } +} \ No newline at end of file From db67a387d6153a0d5653f48e0d8937f6b12710f7 Mon Sep 17 00:00:00 2001 From: "Sebastian.Bor" Date: Sun, 13 Dec 2020 01:30:58 +0000 Subject: [PATCH 3/8] feat: select liquidable loans --- src/constants/labels.ts | 3 + src/hooks/index.ts | 1 + src/hooks/useLiquidableObligations.ts | 54 ++++++++++++++++ src/views/liquidate/index.tsx | 62 ++++++++----------- src/views/liquidate/item.tsx | 12 ++-- .../liquidate/{itemStyle.less => style.less} | 14 +++++ 6 files changed, 106 insertions(+), 40 deletions(-) create mode 100644 src/hooks/useLiquidableObligations.ts rename src/views/liquidate/{itemStyle.less => style.less} (65%) diff --git a/src/constants/labels.ts b/src/constants/labels.ts index fbaa6db..d607dd5 100644 --- a/src/constants/labels.ts +++ b/src/constants/labels.ts @@ -27,11 +27,14 @@ export const LABELS = { BORROW_QUESTION: "How much would you like to borrow?", BORROW_ACTION: "Borrow", LIQUIDATE_ACTION: "Liquidate", + LIQUIDATE_INFO: "Connect to a wallet to view liquidable loans.", + LIQUIDATE_NO_LOANS: "There are no loans to liquidate with your wallet.", TABLE_TITLE_ASSET: "Asset", TABLE_TITLE_YOUR_LOAN_BALANCE: "Your loan balance", TABLE_TITLE_LOAN_BALANCE: "Loan balance", TABLE_TITLE_DEPOSIT_BALANCE: "Your deposit balance", TABLE_TITLE_APY: "APY", + TABLE_TITLE_LTV: "LTV", TABLE_TITLE_BORROW_APY: "Borrow APY", TABLE_TITLE_DEPOSIT_APY: "Deposit APY", TABLE_TITLE_TOTAL_BORROWED: "Total Borrowed", diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 6c3bc96..1701e6f 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -10,3 +10,4 @@ export * from "./useUserObligationByReserve"; export * from "./useBorrowedAmount"; export * from "./useUserDeposits"; export * from "./useSliderInput"; +export * from "./useLiquidableObligations"; diff --git a/src/hooks/useLiquidableObligations.ts b/src/hooks/useLiquidableObligations.ts new file mode 100644 index 0000000..9dd7ec2 --- /dev/null +++ b/src/hooks/useLiquidableObligations.ts @@ -0,0 +1,54 @@ +import { useMemo } from "react"; +import { useUserAccounts } from "./useUserAccounts"; +import { useLendingObligations } from "./useLendingObligations"; +import { LendingReserve } from "../models/lending"; +import { useLendingReserves } from "./useLendingReserves"; +import { ParsedAccount } from "../contexts/accounts"; + +export const useLiquidableObligations = () => { + const { userAccounts } = useUserAccounts(); + const { obligations } = useLendingObligations(); + const { reserveAccounts } = useLendingReserves(); + + const availableReserves = useMemo(() => { + return reserveAccounts.reduce((map, reserve) => { + if (userAccounts.some(acc => acc.info.mint.toBase58() === reserve.info.liquidityMint.toBase58())) { + map.set(reserve.pubkey.toBase58(), reserve); + } + return map; + }, new Map>()) + }, [reserveAccounts, userAccounts]) + + const liquidableObligations = useMemo(() => { + if (availableReserves.size === 0) { + return []; + } + + return obligations + .map(obligation => ( + { + obligation, + reserve: availableReserves.get(obligation.info.borrowReserve.toBase58()) as ParsedAccount + } + )) + .filter(item => item.reserve) + .map(item => { + // 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/views/liquidate/index.tsx b/src/views/liquidate/index.tsx index 1302dbf..2d25393 100644 --- a/src/views/liquidate/index.tsx +++ b/src/views/liquidate/index.tsx @@ -1,44 +1,36 @@ -import { PublicKey } from "@solana/web3.js"; -import BN from "bn.js"; import React from "react"; -import { LABELS, ZERO } from "../../constants"; +import { LABELS } from "../../constants"; import { LiquidateItem } from "./item"; -import "./itemStyle.less"; +import { useLiquidableObligations } from "./../../hooks"; +import { useWallet } from "../../contexts/wallet"; +import "./style.less"; export const LiquidateView = () => { + const { liquidableObligations } = useLiquidableObligations(); + const { connected } = useWallet(); - // ParsedAccount - const obligations = [ - { - pubkey: new PublicKey("2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW"), - account: { - executable: false, - owner: new PublicKey("2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW"), - lamports: 0, - data: new Buffer("x"), - }, - info: { - lastUpdateSlot: ZERO, - collateralAmount: ZERO, - collateralSupply: new PublicKey("2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW"), - cumulativeBorrowRateWad: ZERO, - borrowAmountWad: new BN(0), - borrowReserve: new PublicKey("EwhnKnkwcAeVxHDbR5wMpjwipHFuafxTUhQaaagjUxQG"), - tokenMint: new PublicKey("2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW"), - } - } - ]; return ( -
-
-
{LABELS.TABLE_TITLE_ASSET}
-
{LABELS.TABLE_TITLE_LOAN_BALANCE}
-
{LABELS.TABLE_TITLE_APY}
-
{LABELS.TABLE_TITLE_ACTION}
-
- {obligations.map((obligation) => ( - - ))} +
+ {!connected && ( +
{LABELS.LIQUIDATE_INFO}
+ )} + {connected && liquidableObligations.length === 0 && ( +
{LABELS.LIQUIDATE_NO_LOANS}
+ )} + {connected && liquidableObligations.length > 0 && ( +
+
+
{LABELS.TABLE_TITLE_ASSET}
+
{LABELS.TABLE_TITLE_LOAN_BALANCE}
+
{LABELS.TABLE_TITLE_APY}
+
{LABELS.TABLE_TITLE_LTV}
+
{LABELS.TABLE_TITLE_ACTION}
+
+ {liquidableObligations.map((item) => ( + + ))} +
+ )}
); }; diff --git a/src/views/liquidate/item.tsx b/src/views/liquidate/item.tsx index 6952424..d3ae4ef 100644 --- a/src/views/liquidate/item.tsx +++ b/src/views/liquidate/item.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React, { useMemo } from "react"; import { cache, ParsedAccount, useMint } from "../../contexts/accounts"; import { LendingObligation, LendingReserve, calculateBorrowAPY } from "../../models/lending"; import { useTokenName } from "../../hooks"; @@ -15,16 +15,15 @@ import { LABELS } from "../../constants"; export const LiquidateItem = (props: { obligation: ParsedAccount; + ltv: number }) => { - const { obligation } = props; + const { obligation, ltv } = props; const borrowReserve = cache.get(obligation.info.borrowReserve) as ParsedAccount; const tokenName = useTokenName(borrowReserve?.info.liquidityMint); const liquidityMint = useMint(borrowReserve.info.liquidityMint); - console.log("wad",obligation.info.borrowAmountWad) - const borrowAmount = fromLamports( wadToLamports(obligation.info.borrowAmountWad), liquidityMint @@ -35,7 +34,7 @@ export const LiquidateItem = (props: { ]); return ( - +
@@ -48,6 +47,9 @@ export const LiquidateItem = (props: {
{formatPct.format(borrowAPY)}
+
+ {formatPct.format(ltv / 100)} +