mirror of https://github.com/certusone/oyster.git
Merge pull request #4 from SebastianBor/main
Implement Loan Liquidation
This commit is contained in:
commit
cc9e092835
|
@ -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}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.liquidate-input-title {
|
||||
font-size: 1.05rem;
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -10,3 +10,4 @@ export * from "./useUserObligationByReserve";
|
|||
export * from "./useBorrowedAmount";
|
||||
export * from "./useUserDeposits";
|
||||
export * from "./useSliderInput";
|
||||
export * from "./useLiquidableObligations";
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue