mirror of https://github.com/certusone/oyster.git
feat: fix borrowing power
This commit is contained in:
parent
21051f776a
commit
c204ac5ad9
26
src/App.less
26
src/App.less
|
@ -329,6 +329,32 @@ em {
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.small-statisitc {
|
||||
.ant-statistic-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-statistic-content {
|
||||
max-height: 20px;
|
||||
line-height: 2px;;
|
||||
}
|
||||
|
||||
.ant-statistic-content-value-int {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-statistic-content-value-decimal {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-amount-quote-stat {
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.exchange-card {
|
||||
width: 360px;
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import React from "react";
|
||||
import { LendingReserve } from "../../models/lending";
|
||||
import { Card } from "antd";
|
||||
import { calculateDepositAPY, LendingReserve } from "../../models/lending";
|
||||
import { Card, Col, Row, Statistic } from "antd";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import "./style.less";
|
||||
import { LABELS } from "../../constants";
|
||||
import { GUTTER, LABELS } from "../../constants";
|
||||
import { ReserveUtilizationChart } from "./../../components/ReserveUtilizationChart";
|
||||
import { useMemo } from "react";
|
||||
import { formatNumber, fromLamports, wadToLamports } from "../../utils/utils";
|
||||
import { useMint } from "../../contexts/accounts";
|
||||
import { useMidPriceInUSD } from "../../contexts/market";
|
||||
import { TokenIcon } from "../TokenIcon";
|
||||
|
||||
export const ReserveStatus = (props: {
|
||||
className?: string;
|
||||
|
@ -17,20 +22,113 @@ export const ReserveStatus = (props: {
|
|||
alignItems: "center",
|
||||
};
|
||||
|
||||
const mintAddress = props.reserve.liquidityMint?.toBase58();
|
||||
const liquidityMint = useMint(mintAddress);
|
||||
const { price } = useMidPriceInUSD(mintAddress);
|
||||
const availableLiquidity = fromLamports(
|
||||
props.reserve.availableLiquidity.toNumber(),
|
||||
liquidityMint
|
||||
);
|
||||
|
||||
const availableLiquidityInUSD = price * availableLiquidity;
|
||||
|
||||
const totalBorrows = useMemo(
|
||||
() =>
|
||||
fromLamports(
|
||||
wadToLamports(props.reserve.borrowedLiquidityWad),
|
||||
liquidityMint
|
||||
),
|
||||
[props.reserve, liquidityMint]
|
||||
);
|
||||
|
||||
const totalBorrowsInUSD = price * totalBorrows;
|
||||
|
||||
const depositAPY = useMemo(() => calculateDepositAPY(props.reserve), [
|
||||
props.reserve
|
||||
]);
|
||||
|
||||
const liquidationThreshold = props.reserve.config.liquidationThreshold;
|
||||
const liquidationPenalty = props.reserve.config.liquidationBonus;
|
||||
const maxLTV = props.reserve.config.loanToValueRatio;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={props.className}
|
||||
title={<>{LABELS.RESERVE_STATUS_TITLE}</>}
|
||||
title={<>
|
||||
<TokenIcon style={{ marginRight: 0, marginTop: 0, position: 'absolute', left: 15 }}
|
||||
mintAddress={mintAddress} size={30} />
|
||||
{LABELS.RESERVE_STATUS_TITLE}</>}
|
||||
bodyStyle={bodyStyle}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
}}
|
||||
>
|
||||
<ReserveUtilizationChart reserve={props.reserve} />
|
||||
<div className="flexColumn">
|
||||
<Row gutter={GUTTER}>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title="Available Liquidity"
|
||||
value={availableLiquidity}
|
||||
valueRender={(node) => <div>
|
||||
{node}
|
||||
<div className="dashboard-amount-quote-stat">${formatNumber.format(availableLiquidityInUSD)}</div>
|
||||
</div>}
|
||||
precision={2} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title="Total Borrowed"
|
||||
value={totalBorrows}
|
||||
valueRender={(node) => <div>
|
||||
{node}
|
||||
<div className="dashboard-amount-quote-stat">${formatNumber.format(totalBorrowsInUSD)}</div>
|
||||
</div>}
|
||||
precision={2} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={GUTTER}>
|
||||
<Col
|
||||
span={24}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
}}
|
||||
>
|
||||
<ReserveUtilizationChart reserve={props.reserve} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={GUTTER}>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Maximum LTV"
|
||||
className="small-statisitc"
|
||||
value={maxLTV}
|
||||
precision={2}
|
||||
suffix="%" />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Liquidation threashold"
|
||||
className="small-statisitc"
|
||||
value={liquidationThreshold}
|
||||
precision={2}
|
||||
suffix="%" />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Liquidation penalty"
|
||||
className="small-statisitc"
|
||||
value={liquidationPenalty}
|
||||
precision={2}
|
||||
suffix="%" />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="APY"
|
||||
className="small-statisitc"
|
||||
value={depositAPY * 100}
|
||||
precision={2}
|
||||
suffix="%" />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -3,9 +3,11 @@ import { LendingReserve } from "../../models/lending";
|
|||
import { fromLamports, wadToLamports } from "../../utils/utils";
|
||||
import { useMint } from "../../contexts/accounts";
|
||||
import { WaterWave } from "./../WaterWave";
|
||||
import { Statistic } from "antd";
|
||||
|
||||
export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
|
||||
const liquidityMint = useMint(props.reserve.liquidityMint);
|
||||
const mintAddress = props.reserve.liquidityMint?.toBase58();
|
||||
const liquidityMint = useMint(mintAddress);
|
||||
const availableLiquidity = fromLamports(
|
||||
props.reserve.availableLiquidity.toNumber(),
|
||||
liquidityMint
|
||||
|
@ -20,10 +22,20 @@ export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
|
|||
[props.reserve, liquidityMint]
|
||||
);
|
||||
|
||||
const percent = (availableLiquidity * 100) / (availableLiquidity + totalBorrows);
|
||||
|
||||
return (
|
||||
<WaterWave
|
||||
style={{ height: 300 }}
|
||||
percent={(availableLiquidity * 100) / (availableLiquidity + totalBorrows)}
|
||||
showPercent={false}
|
||||
title={
|
||||
<Statistic
|
||||
title="Utilization"
|
||||
value={percent}
|
||||
suffix="%"
|
||||
precision={2} />
|
||||
}
|
||||
percent={percent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,19 +7,22 @@ import { PublicKey } from "@solana/web3.js";
|
|||
export const TokenIcon = (props: {
|
||||
mintAddress?: string | PublicKey;
|
||||
style?: React.CSSProperties;
|
||||
size?: number;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { tokenMap } = useConnectionConfig();
|
||||
const icon = getTokenIcon(tokenMap, props.mintAddress);
|
||||
|
||||
const size = props.size || 20;
|
||||
|
||||
if (icon) {
|
||||
return (
|
||||
<img
|
||||
alt="Token icon"
|
||||
className={props.className}
|
||||
key={icon}
|
||||
width={props.style?.width || "20"}
|
||||
height={props.style?.height || "20"}
|
||||
width={props.style?.width || size.toString()}
|
||||
height={props.style?.height || size.toString()}
|
||||
src={icon}
|
||||
style={{
|
||||
marginRight: "0.5rem",
|
||||
|
@ -38,8 +41,8 @@ export const TokenIcon = (props: {
|
|||
address={props.mintAddress}
|
||||
style={{
|
||||
marginRight: "0.5rem",
|
||||
width: 20,
|
||||
height: 20,
|
||||
width: size,
|
||||
height: size,
|
||||
marginTop: 2,
|
||||
...props.style,
|
||||
}}
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
useTokenName,
|
||||
useUserBalance,
|
||||
useBorrowedAmount,
|
||||
useBorrowingPower,
|
||||
} from "./../../hooks";
|
||||
import { LendingReserve } from "../../models/lending";
|
||||
import { formatNumber } from "../../utils/utils";
|
||||
|
@ -29,9 +30,7 @@ export const UserLendingCard = (props: {
|
|||
);
|
||||
|
||||
const { borrowed: totalBorrowed, borrowedInUSD, ltv, health } = useBorrowedAmount(address);
|
||||
|
||||
// TODO: calculate
|
||||
const available = 0; // use all available deposits and convert using market rate
|
||||
const { totalInQuote: borrowingPowerInUSD, borrowingPower } = useBorrowingPower(address);
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
@ -82,7 +81,10 @@ export const UserLendingCard = (props: {
|
|||
Available to you:
|
||||
</Text>
|
||||
<div className="card-cell ">
|
||||
{formatNumber.format(available)} {name}
|
||||
<div>
|
||||
<div><em>{formatNumber.format(borrowingPower)}</em> {name}</div>
|
||||
<div className="dashboard-amount-quote">${formatNumber.format(borrowingPowerInUSD)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -4,13 +4,15 @@
|
|||
display: inline-block;
|
||||
position: relative;
|
||||
transform-origin: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: calc(50% - 15px);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
span {
|
||||
.title {
|
||||
color: @text-color-secondary;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
|
|
|
@ -5,7 +5,7 @@ export const WaterWave = (props: any) => {
|
|||
const node = useRef<HTMLCanvasElement>();
|
||||
const root = useRef<HTMLDivElement>();
|
||||
const [radio, setRadio] = useState(1);
|
||||
const { percent, title, style, color } = props;
|
||||
const { percent, title, style, color, showPercent } = props;
|
||||
const { height } = style;
|
||||
|
||||
const resize = useCallback(() => {
|
||||
|
@ -54,8 +54,8 @@ export const WaterWave = (props: any) => {
|
|||
/>
|
||||
</div>
|
||||
<div className="text" style={{ width: height }}>
|
||||
{title && <span>{title}</span>}
|
||||
<h4>{percent.toFixed(2)}%</h4>
|
||||
{title}
|
||||
<h4>{showPercent && `${percent.toFixed(2)}%`}</h4>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ export const LABELS = {
|
|||
BORROWING_POWER_VALUE: "Borrowing Power",
|
||||
BORROWED_VALUE: "You borrowed",
|
||||
GIVE_SOL: "Give me SOL",
|
||||
LIQUIDATION_INFO: "This view displays all loans that can be liquidated. A liquidation is a process where borrower collateral does not cover value of the loan. It is represented by health factor falling below 1.o. When a loan is liquidated, an liquidator can purchase collateral at a discount by repaing the portio of the loan. ",
|
||||
LIQUIDATION_INFO: "This view displays all loans that can be liquidated. A liquidation is a process where borrower collateral does not cover value of the loan. It is represented by health factor falling below 1.0. When a loan is liquidated, an liquidator can purchase collateral at a discount by repaing the portio of the loan. ",
|
||||
FAUCET_INFO:
|
||||
"This faucet will help you fund your accounts outside of Solana main network.",
|
||||
ACCOUNT_FUNDED: "Account funded.",
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
export const GUTTER = [16, { xs: 8, sm: 16, md: 16, lg: 16 }] as any;
|
||||
export const GUTTER = [16, { xs: 8, sm: 16, md: 16, lg: 16 }] as any;
|
||||
|
||||
export const SMALL_STATISTIC: React.CSSProperties = {
|
||||
fontSize: 10,
|
||||
};
|
|
@ -55,7 +55,7 @@ export function useBorrowedAmount(address?: string | PublicKey) {
|
|||
borrowedInUSD: 0,
|
||||
colateralInUSD: 0,
|
||||
ltv: 0,
|
||||
health: 0,
|
||||
health: 0,
|
||||
};
|
||||
|
||||
let liquidationThreshold = 0;
|
||||
|
@ -88,7 +88,6 @@ export function useBorrowedAmount(address?: string | PublicKey) {
|
|||
result.health = Number.isFinite(result.health) ? result.health : 0;
|
||||
}
|
||||
|
||||
|
||||
setBorrowedInfo(result);
|
||||
})();
|
||||
}, [connection, userObligationsByReserve, setBorrowedInfo]);
|
||||
|
|
|
@ -19,7 +19,7 @@ export const HomeView = () => {
|
|||
const [totals, setTotals] = useState<Totals>({
|
||||
marketSize: 0,
|
||||
borrowed: 0,
|
||||
lentOutPct: 0,
|
||||
lentOutPct: 0,
|
||||
items: [],
|
||||
})
|
||||
|
||||
|
@ -28,7 +28,7 @@ export const HomeView = () => {
|
|||
let newTotals: Totals = {
|
||||
marketSize: 0,
|
||||
borrowed: 0,
|
||||
lentOutPct: 0,
|
||||
lentOutPct: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
|
@ -44,14 +44,17 @@ export const HomeView = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const price = midPriceInUSD(liquidityMint?.pubkey.toBase58());
|
||||
|
||||
let leaf = {
|
||||
key: item.pubkey.toBase58(),
|
||||
marketSize: fromLamports(marketCapLamports, liquidityMint?.info) *
|
||||
midPriceInUSD(liquidityMint?.pubkey.toBase58()),
|
||||
price,
|
||||
borrowed: fromLamports(
|
||||
wadToLamports(item.info?.borrowedLiquidityWad).toNumber(),
|
||||
liquidityMint.info
|
||||
),
|
||||
) *
|
||||
price,
|
||||
name: getTokenName(tokenMap, item.info.liquidityMint.toBase58())
|
||||
}
|
||||
|
||||
|
@ -59,7 +62,7 @@ export const HomeView = () => {
|
|||
|
||||
newTotals.marketSize = newTotals.marketSize + leaf.marketSize;
|
||||
newTotals.borrowed = newTotals.borrowed + leaf.borrowed;
|
||||
|
||||
|
||||
});
|
||||
|
||||
newTotals.lentOutPct = newTotals.borrowed / newTotals.marketSize;
|
||||
|
@ -82,8 +85,8 @@ export const HomeView = () => {
|
|||
|
||||
return (
|
||||
<div className="flexColumn">
|
||||
<Row
|
||||
gutter={GUTTER}
|
||||
<Row
|
||||
gutter={GUTTER}
|
||||
className="home-info-row" >
|
||||
<Col xs={24} xl={5}>
|
||||
<Card>
|
||||
|
@ -128,16 +131,16 @@ export const HomeView = () => {
|
|||
</Row>
|
||||
|
||||
<Card>
|
||||
<div className="home-item home-header">
|
||||
<div>{LABELS.TABLE_TITLE_ASSET}</div>
|
||||
<div>{LABELS.TABLE_TITLE_MARKET_SIZE}</div>
|
||||
<div>{LABELS.TABLE_TITLE_TOTAL_BORROWED}</div>
|
||||
<div>{LABELS.TABLE_TITLE_DEPOSIT_APY}</div>
|
||||
<div>{LABELS.TABLE_TITLE_BORROW_APY}</div>
|
||||
</div>
|
||||
{reserveAccounts.map((account) => (
|
||||
<LendingReserveItem reserve={account.info} address={account.pubkey} item={totals.items.find(item => item.key === account.pubkey.toBase58())} />
|
||||
))}
|
||||
<div className="home-item home-header">
|
||||
<div>{LABELS.TABLE_TITLE_ASSET}</div>
|
||||
<div>{LABELS.TABLE_TITLE_MARKET_SIZE}</div>
|
||||
<div>{LABELS.TABLE_TITLE_TOTAL_BORROWED}</div>
|
||||
<div>{LABELS.TABLE_TITLE_DEPOSIT_APY}</div>
|
||||
<div>{LABELS.TABLE_TITLE_BORROW_APY}</div>
|
||||
</div>
|
||||
{reserveAccounts.map((account) => (
|
||||
<LendingReserveItem reserve={account.info} address={account.pubkey} item={totals.items.find(item => item.key === account.pubkey.toBase58())} />
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,6 +5,8 @@ import "./style.less";
|
|||
|
||||
import { UserLendingCard } from "./../../components/UserLendingCard";
|
||||
import { ReserveStatus } from "./../../components/ReserveStatus";
|
||||
import { Col, Row } from "antd";
|
||||
import { GUTTER } from "../../constants";
|
||||
|
||||
export const ReserveView = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
@ -16,19 +18,21 @@ export const ReserveView = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="reserve-overview">
|
||||
<div className="reserve-overview-container">
|
||||
<ReserveStatus
|
||||
className="reserve-overview-item reserve-overview-item-left"
|
||||
reserve={reserve}
|
||||
address={lendingReserve.pubkey}
|
||||
/>
|
||||
<UserLendingCard
|
||||
className="reserve-overview-item reserve-overview-item-right"
|
||||
reserve={reserve}
|
||||
address={lendingReserve.pubkey}
|
||||
/>
|
||||
</div>
|
||||
<div className="flexColumn">
|
||||
<Row gutter={GUTTER}>
|
||||
<Col sm={24} md={12} lg={14} xl={15} xxl={18}>
|
||||
<ReserveStatus
|
||||
reserve={reserve}
|
||||
address={lendingReserve.pubkey} />
|
||||
</Col>
|
||||
<Col sm={24} md={12} lg={10} xl={9} xxl={6}>
|
||||
<UserLendingCard
|
||||
className="user-lending-card"
|
||||
reserve={reserve}
|
||||
address={lendingReserve.pubkey}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,27 +4,6 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.reserve-overview-item {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.reserve-overview-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reserve-overview-item-left {
|
||||
flex: 60%;
|
||||
}
|
||||
|
||||
.reserve-overview-item-right {
|
||||
flex: 30%;
|
||||
}
|
||||
|
||||
/* Responsive layout - makes a one column layout instead of a two-column layout */
|
||||
@media (max-width: 600px) {
|
||||
.reserve-overview-item-right, .reserve-overview-item-left {
|
||||
flex: 100%;
|
||||
}
|
||||
.user-lending-card {
|
||||
height: 100%;
|
||||
}
|
Loading…
Reference in New Issue