Merge pull request #4 from SebastianBor/main

Implement Loan Liquidation
This commit is contained in:
Bartosz Lipinski 2020-12-15 12:20:11 -06:00 committed by GitHub
commit cc9e092835
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 352 additions and 5 deletions

View File

@ -29,7 +29,7 @@ export const CollateralSelector = (props: {
<Select
size="large"
showSearch
style={{ minWidth: 120, marginBottom: 10 }}
style={{ minWidth: 300 , margin: "5px 0px" }}
placeholder="Collateral"
value={props.collateralReserve}
disabled={props.disabled}

View File

@ -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}
</Link>
</Menu.Item>
<Menu.Item key="5" icon={<LoginOutlined />}>
<Link
to={{
pathname: "/liquidate",
}}
>
{LABELS.MENU_LIQUIDATE}
</Link>
</Menu.Item>
{env !== "mainnet-beta" && (
<Menu.Item key="5" icon={<RocketOutlined />}>
<Menu.Item key="6" icon={<RocketOutlined />}>
<Link
to={{
pathname: "/faucet",

View File

@ -0,0 +1,60 @@
import { Button } from "antd";
import Card from "antd/lib/card";
import React, { useCallback } from "react";
import { useState } from "react";
import { LABELS } from "../../constants";
import { ParsedAccount } from "../../contexts/accounts";
import { LendingObligation, LendingReserve } from "../../models";
import { BackButton } from "../BackButton";
import { CollateralSelector } from "../CollateralSelector";
import "./style.less";
export const LiquidateInput = (props: {
className?: string;
reserve: ParsedAccount<LendingReserve>;
obligation: ParsedAccount<LendingObligation>;
}) => {
const { reserve } = props;
const [collateralReserveMint, setCollateralReserveMint] = useState<string>();
const [pendingTx, setPendingTx] = useState(false);
const onLiquidate = useCallback(() => {
setPendingTx(true);
setPendingTx(false);
}, []);
const bodyStyle: React.CSSProperties = {
display: "flex",
flex: 1,
justifyContent: "center",
alignItems: "center",
height: "100%",
};
return (
<Card className={props.className} bodyStyle={bodyStyle} >
<div style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
}}>
<div className="liquidate-input-title">{LABELS.SELECT_COLLATERAL}</div>
<CollateralSelector
reserve={reserve.info}
mint={collateralReserveMint}
onMintChange={setCollateralReserveMint}
/>
<Button
type="primary"
onClick={onLiquidate}
loading={pendingTx}
>
{LABELS.LIQUIDATE_ACTION}
</Button>
<BackButton />
</div>
</Card>
)
}

View File

@ -0,0 +1,3 @@
.liquidate-input-title {
font-size: 1.05rem;
}

View File

@ -17,6 +17,7 @@ export const LABELS = {
NO_LOANS_NO_DEPOSITS: "No loans or deposits.",
MENU_DEPOSIT: "Deposit",
MENU_BORROW: "Borrow",
MENU_LIQUIDATE: "Liquidate",
MENU_FAUCET: "Faucet",
APP_TITLE: "Oyster Lending",
CONNECT_BUTTON: "Connect",
@ -26,11 +27,15 @@ export const LABELS = {
COLLATERAL: "Collateral",
BORROW_QUESTION: "How much would you like to borrow?",
BORROW_ACTION: "Borrow",
LIQUIDATE_ACTION: "Liquidate",
LIQUIDATE_NO_LOANS: "There are no loans to liquidate.",
TABLE_TITLE_ASSET: "Asset",
TABLE_TITLE_LOAN_BALANCE: "Your loan balance",
TABLE_TITLE_YOUR_LOAN_BALANCE: "Your loan balance",
TABLE_TITLE_LOAN_BALANCE: "Loan balance",
TABLE_TITLE_COLLATERAL_BALANCE: "Collateral",
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",

View File

@ -10,3 +10,4 @@ export * from "./useUserObligationByReserve";
export * from "./useBorrowedAmount";
export * from "./useUserDeposits";
export * from "./useSliderInput";
export * from "./useLiquidableObligations";

View File

@ -0,0 +1,50 @@
import { useMemo } from "react";
import { useLendingObligations } from "./useLendingObligations";
import { LendingReserve } from "../models/lending";
import { useLendingReserves } from "./useLendingReserves";
import { ParsedAccount } from "../contexts/accounts";
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<string, ParsedAccount<LendingReserve>>())
}, [reserveAccounts])
const liquidableObligations = useMemo(() => {
if (availableReserves.size === 0) {
return [];
}
return obligations
.map(obligation => (
{
obligation,
reserve: availableReserves.get(obligation.info.borrowReserve.toBase58()) as ParsedAccount<LendingReserve>
}
))
.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
};
}

View File

@ -18,6 +18,8 @@ import {
RepayReserveView,
ReserveView,
WithdrawView,
LiquidateView,
LiquidateReserveView
} from "./views";
export function Routes() {
@ -61,6 +63,15 @@ export function Routes() {
path="/repay/:reserve"
children={<RepayReserveView />}
/>
<Route
exact
path="/liquidate"
children={<LiquidateView />}
/>
<Route
path="/liquidate/:id"
children={<LiquidateReserveView />}
/>
<Route exact path="/faucet" children={<FaucetView />} />
</Switch>
</AppLayout>

View File

@ -40,7 +40,7 @@ export const DashboardView = () => {
<span>{LABELS.DASHBOARD_TITLE_LOANS}</span>
<div className="dashboard-item dashboard-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_LOAN_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_YOUR_LOAN_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_COLLATERAL_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_APY}</div>
<div>{LABELS.TABLE_TITLE_ACTION}</div>

View File

@ -8,3 +8,5 @@ export { ReserveView } from "./reserve";
export { WithdrawView } from "./withdraw";
export { FaucetView } from "./faucet";
export { RepayReserveView } from "./repayReserve";
export { LiquidateView } from "./liquidate";
export { LiquidateReserveView } from "./liquidateReserve";

View File

@ -0,0 +1,30 @@
import React from "react";
import { LABELS } from "../../constants";
import { LiquidateItem } from "./item";
import { useLiquidableObligations } from "./../../hooks";
import "./style.less";
export const LiquidateView = () => {
const { liquidableObligations } = useLiquidableObligations();
return (
<div className="liquidate-container">
{liquidableObligations.length === 0 ? (
<div className="liquidate-info">{LABELS.LIQUIDATE_NO_LOANS}</div>
) : (
<div className="flexColumn">
<div className="liquidate-item liquidate-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_LOAN_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_APY}</div>
<div>{LABELS.TABLE_TITLE_LTV}</div>
<div>{LABELS.TABLE_TITLE_ACTION}</div>
</div>
{liquidableObligations.map((item) => (
<LiquidateItem key={item.obligation.pubkey.toBase58()} obligation={item.obligation} ltv={item.ltv}></LiquidateItem>
))}
</div>
)}
</div>
);
};

View File

@ -0,0 +1,62 @@
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<LendingObligation>;
ltv: number
}) => {
const { obligation, ltv } = props;
const borrowReserve = cache.get(obligation.info.borrowReserve) as ParsedAccount<LendingReserve>;
const tokenName = useTokenName(borrowReserve?.info.liquidityMint);
const liquidityMint = useMint(borrowReserve.info.liquidityMint);
const borrowAmount = fromLamports(
wadToLamports(obligation.info.borrowAmountWad),
liquidityMint
);
const borrowAPY = useMemo(() => calculateBorrowAPY(borrowReserve.info), [
borrowReserve,
]);
return (
<Link to={`/liquidate/${obligation.pubkey.toBase58()}`}>
<Card>
<div className="liquidate-item">
<span style={{ display: "flex" }}>
<TokenIcon mintAddress={borrowReserve.info.liquidityMint} />
{tokenName}
</span>
<div>
{formatNumber.format(borrowAmount)} {tokenName}
</div>
<div>
{formatPct.format(borrowAPY)}
</div>
<div>
{formatPct.format(ltv / 100)}
</div>
<div>
<Button>
<span>{LABELS.LIQUIDATE_ACTION}</span>
</Button>
</div>
</div>
</Card>
</Link>
);
};

View File

@ -0,0 +1,43 @@
.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
}
}
.liquidate-info {
display: flex;
align-self: center;
justify-content: center;
flex: 1;
}
.liquidate-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
flex: 1;
}

View File

@ -0,0 +1,39 @@
import React from "react";
import { useParams } from "react-router-dom";
import { useLendingObligation, useLendingReserve } from "../../hooks";
import {
SideReserveOverview,
SideReserveOverviewMode,
} from "../../components/SideReserveOverview";
import { LiquidateInput } from "../../components/LiquidateInput";
import "./style.less";
export const LiquidateReserveView = () => {
const { id } = useParams<{ id: string }>();
const obligation = useLendingObligation(id);
const reserve = useLendingReserve(obligation?.info.borrowReserve);
if (!obligation || !reserve) {
return null;
}
return (
<div className="liquidate-reserve">
<div className="liquidate-reserve-container">
<LiquidateInput
className="liquidate-reserve-item liquidate-reserve-item-left"
obligation={obligation}
reserve={reserve}
/>
<SideReserveOverview
className="liquidate-reserve-item liquidate-reserve-item-right"
reserve={reserve}
mode={SideReserveOverviewMode.Deposit}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,30 @@
.liquidate-reserve {
display: flex;
flex-direction: column;
flex: 1;
}
.liquidate-reserve-item {
margin: 4px;
}
.liquidate-reserve-container {
display: flex;
flex-wrap: wrap;
flex: 1;
}
.liquidate-reserve-item-left {
flex: 60%;
}
.liquidate-reserve-item-right {
flex: 30%;
}
/* Responsive layout - makes a one column layout instead of a two-column layout */
@media (max-width: 600px) {
.liquidate-reserve-item-right, .liquidate-reserve-item-left {
flex: 100%;
}
}