mirror of https://github.com/certusone/oyster.git
feat: add borrow page overview
This commit is contained in:
parent
c5d1e51963
commit
9805849b85
20
src/App.less
20
src/App.less
|
@ -231,6 +231,10 @@ em {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.card-fill {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
box-sizing: border-box;
|
||||
margin: 5px 0px;
|
||||
|
@ -295,6 +299,12 @@ em {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
.token-balance {
|
||||
display: none;
|
||||
};
|
||||
};
|
||||
|
||||
.token-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -303,6 +313,16 @@ em {
|
|||
margin: 5px 0px;
|
||||
}
|
||||
|
||||
.token-balance {
|
||||
margin-left: auto;
|
||||
margin-right: 5px;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
|
||||
[class="ant-layout-header"] {
|
||||
height: 16px !important;
|
||||
}
|
||||
|
||||
.dashboard-amount-quote {
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
|
|
|
@ -1,14 +1,44 @@
|
|||
import React from "react";
|
||||
import { useLendingReserves } from "../../hooks";
|
||||
import { useLendingReserves, UserDeposit, useUserDeposits } from "../../hooks";
|
||||
import { LendingMarket, LendingReserve } from "../../models";
|
||||
import { TokenIcon } from "../TokenIcon";
|
||||
import { getTokenName } from "../../utils/utils";
|
||||
import { formatAmount, getTokenName } from "../../utils/utils";
|
||||
import { Select } from "antd";
|
||||
import { useConnectionConfig } from "../../contexts/connection";
|
||||
import { cache, ParsedAccount } from "../../contexts/accounts";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export const CollateralItem = (props: {
|
||||
mint: string;
|
||||
reserve: ParsedAccount<LendingReserve>;
|
||||
userDeposit?: UserDeposit;
|
||||
name: string;
|
||||
}) => {
|
||||
const {
|
||||
mint,
|
||||
name,
|
||||
userDeposit,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<TokenIcon mintAddress={mint} />
|
||||
{name}
|
||||
<span
|
||||
className="token-balance"
|
||||
>
|
||||
{" "}
|
||||
{userDeposit ? formatAmount(userDeposit.info.amount): '--'}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const CollateralSelector = (props: {
|
||||
reserve: LendingReserve;
|
||||
collateralReserve?: string;
|
||||
|
@ -17,13 +47,13 @@ export const CollateralSelector = (props: {
|
|||
}) => {
|
||||
const { reserveAccounts } = useLendingReserves();
|
||||
const { tokenMap } = useConnectionConfig();
|
||||
const { userDeposits } = useUserDeposits();
|
||||
|
||||
const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<
|
||||
const market = cache.get(props.reserve?.lendingMarket) as ParsedAccount<
|
||||
LendingMarket
|
||||
>;
|
||||
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(
|
||||
market?.info?.quoteMint
|
||||
);
|
||||
const onlyQuoteAllowed = props.reserve?.liquidityMint?.toBase58() !==
|
||||
market?.info?.quoteMint?.toBase58();
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
@ -54,17 +84,15 @@ export const CollateralSelector = (props: {
|
|||
const mint = reserve.info.liquidityMint.toBase58();
|
||||
const address = reserve.pubkey.toBase58();
|
||||
const name = getTokenName(tokenMap, mint);
|
||||
return (
|
||||
<Option key={address} value={address} name={name} title={address}>
|
||||
<div
|
||||
key={address}
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<TokenIcon mintAddress={mint} />
|
||||
{name}
|
||||
</div>
|
||||
</Option>
|
||||
);
|
||||
|
||||
|
||||
return <Option key={address} value={address} name={name} title={address}>
|
||||
<CollateralItem
|
||||
reserve={reserve}
|
||||
userDeposit={userDeposits.find(dep => dep.reserve.pubkey.toBase58() === address)}
|
||||
mint={mint}
|
||||
name={name} />
|
||||
</Option>
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
|
|
|
@ -2,3 +2,4 @@ export * from "./ids";
|
|||
export * from "./labels";
|
||||
export * from "./math";
|
||||
export * from "./marks";
|
||||
export * from "./style";
|
|
@ -0,0 +1 @@
|
|||
export const GUTTER = [16, { xs: 8, sm: 16, md: 16, lg: 16 }] as any;
|
|
@ -2,28 +2,48 @@ import { PublicKey } from "@solana/web3.js";
|
|||
import { useMemo } from "react";
|
||||
import { useMidPriceInUSD } from "../contexts/market";
|
||||
import { useLendingMarket } from "./useLendingMarket";
|
||||
import { useLendingReserve } from "./useLendingReserves";
|
||||
import { getLendingReserves, useLendingReserve } from "./useLendingReserves";
|
||||
import { useUserDeposits } from "./useUserDeposits";
|
||||
|
||||
// TODO: add option to decrease buying power by overcollateralization factor
|
||||
|
||||
export function useBorrowingPower(reserveAddress: string | PublicKey, overcollateralize = true) {
|
||||
const key = useMemo(() => typeof reserveAddress === 'string' ? reserveAddress : reserveAddress.toBase58(), [reserveAddress]);
|
||||
const exclude = useMemo(() => new Set([key]), [key]);
|
||||
const { totalInQuote } = useUserDeposits(exclude)
|
||||
|
||||
const reserve = useLendingReserve(key);
|
||||
|
||||
const liquidityMint = reserve?.info.liquidityMint;
|
||||
const market = useLendingMarket(liquidityMint);
|
||||
const price = useMidPriceInUSD(liquidityMint.toBase58()).price;
|
||||
const liquidityMintAddress = liquidityMint?.toBase58();
|
||||
const market = useLendingMarket(reserve?.info.lendingMarket);
|
||||
|
||||
const quoteMintAddess = market?.info?.quoteMint?.toBase58();
|
||||
|
||||
// TODO: remove once cross-collateral is supported
|
||||
const onlyQuoteAllowed = liquidityMintAddress !==
|
||||
quoteMintAddess;
|
||||
|
||||
const exclude = useMemo(() => new Set(
|
||||
[key]),
|
||||
[key]);
|
||||
const inlcude = useMemo(() => {
|
||||
const quoteReserve = getLendingReserves()
|
||||
.find(r => r.info.liquidityMint.toBase58() === quoteMintAddess);
|
||||
return onlyQuoteAllowed && quoteReserve ?
|
||||
new Set([quoteReserve.pubkey.toBase58()]) :
|
||||
undefined;
|
||||
}, [onlyQuoteAllowed, quoteMintAddess]);
|
||||
|
||||
const { totalInQuote } = useUserDeposits(exclude, inlcude)
|
||||
|
||||
const price = useMidPriceInUSD(liquidityMintAddress).price;
|
||||
|
||||
// amounts already expressed as quite mint
|
||||
if(liquidityMint.toBase58() === market?.info.quoteMint?.toBase58()) {
|
||||
if (liquidityMintAddress === market?.info.quoteMint?.toBase58()) {
|
||||
return {
|
||||
borrowingPower: totalInQuote,
|
||||
totalInQuote,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
borrowingPower: totalInQuote / price,
|
||||
|
|
|
@ -50,6 +50,7 @@ export function useUserCollateralBalance(
|
|||
balanceInUSD,
|
||||
mint: reserve?.collateralMint,
|
||||
accounts,
|
||||
hasBalance: accounts.length > 0 && balance > 0,
|
||||
};
|
||||
}
|
||||
export function calculateCollateralBalance(
|
||||
|
|
|
@ -3,11 +3,11 @@ import { useEffect, useMemo, useState } from "react";
|
|||
import { LendingReserve, LendingReserveParser } from "../models/lending";
|
||||
import { cache, ParsedAccount } from "./../contexts/accounts";
|
||||
|
||||
const getLendingReserves = () => {
|
||||
export const getLendingReserves = () => {
|
||||
return cache
|
||||
.byParser(LendingReserveParser)
|
||||
.map((id) => cache.get(id))
|
||||
.filter((acc) => acc !== undefined) as any[];
|
||||
.filter((acc) => acc !== undefined) as ParsedAccount<LendingReserve>[];
|
||||
};
|
||||
|
||||
export function useLendingReserves() {
|
||||
|
|
|
@ -5,7 +5,8 @@ import { useMarkets } from "../contexts/market";
|
|||
import { fromLamports } from "../utils/utils";
|
||||
import { useUserAccounts } from "./useUserAccounts";
|
||||
|
||||
export function useUserBalance(mint?: PublicKey, account?: PublicKey) {
|
||||
export function useUserBalance(mintAddress?: PublicKey | string, account?: PublicKey) {
|
||||
const mint = useMemo(() => typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58(), [mintAddress]);
|
||||
const { userAccounts } = useUserAccounts();
|
||||
const [balanceInUSD, setBalanceInUSD] = useState(0);
|
||||
const { marketEmitter, midPriceInUSD } = useMarkets();
|
||||
|
@ -15,7 +16,7 @@ export function useUserBalance(mint?: PublicKey, account?: PublicKey) {
|
|||
return userAccounts
|
||||
.filter(
|
||||
(acc) =>
|
||||
mint?.equals(acc.info.mint) &&
|
||||
mint === acc.info.mint.toBase58() &&
|
||||
(!account || account.equals(acc.pubkey))
|
||||
)
|
||||
.sort((a, b) => b.info.amount.sub(a.info.amount).toNumber());
|
||||
|
@ -32,7 +33,7 @@ export function useUserBalance(mint?: PublicKey, account?: PublicKey) {
|
|||
|
||||
useEffect(() => {
|
||||
const updateBalance = () => {
|
||||
setBalanceInUSD(balance * midPriceInUSD(mint?.toBase58() || ''));
|
||||
setBalanceInUSD(balance * midPriceInUSD(mint || ''));
|
||||
}
|
||||
|
||||
const dispose = marketEmitter.onMarket((args) => {
|
||||
|
@ -51,5 +52,6 @@ export function useUserBalance(mint?: PublicKey, account?: PublicKey) {
|
|||
balanceLamports,
|
||||
balanceInUSD,
|
||||
accounts,
|
||||
hasBalance: accounts.length > 0 && balance > 0,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -148,7 +148,7 @@ const abbreviateNumber = (number: number, precision: number) => {
|
|||
return scaled.toFixed(precision) + suffix;
|
||||
};
|
||||
|
||||
const format = (val: number, precision: number, abbr: boolean) =>
|
||||
export const formatAmount = (val: number, precision: number = 6, abbr: boolean = true) =>
|
||||
abbr ? abbreviateNumber(val, precision) : val.toFixed(precision);
|
||||
|
||||
export function formatTokenAmount(
|
||||
|
@ -164,7 +164,7 @@ export function formatTokenAmount(
|
|||
return "";
|
||||
}
|
||||
|
||||
return `${[prefix]}${format(
|
||||
return `${[prefix]}${formatAmount(
|
||||
fromLamports(account, mint, rate),
|
||||
precision,
|
||||
abbr
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { useLendingReserve } from "../../hooks";
|
||||
import { useBorrowingPower, useLendingReserve, useUserObligations } from "../../hooks";
|
||||
import { useParams } from "react-router-dom";
|
||||
import "./style.less";
|
||||
|
||||
|
@ -8,28 +8,85 @@ import {
|
|||
SideReserveOverview,
|
||||
SideReserveOverviewMode,
|
||||
} from "../../components/SideReserveOverview";
|
||||
import { Card, Col, Row, Statistic } from "antd";
|
||||
import { BarChartStatistic } from "../../components/BarChartStatistic";
|
||||
import { GUTTER } from "../../constants";
|
||||
|
||||
export const BorrowReserveView = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const lendingReserve = useLendingReserve(id);
|
||||
const { userObligations, totalInQuote: loansValue } = useUserObligations();
|
||||
|
||||
const { totalInQuote: borrowingPower } = useBorrowingPower(id)
|
||||
|
||||
if (!lendingReserve) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numberOfLoans = userObligations
|
||||
.filter(ob =>
|
||||
// ob.obligation.info.borrowReserve.toBase58() === id &&
|
||||
ob.obligation.info.collateralInQuote > 0)
|
||||
.length;
|
||||
|
||||
return (
|
||||
<div className="borrow-reserve">
|
||||
<div className="borrow-reserve-container">
|
||||
<BorrowInput
|
||||
className="borrow-reserve-item borrow-reserve-item-left"
|
||||
reserve={lendingReserve}
|
||||
/>
|
||||
<SideReserveOverview
|
||||
className="borrow-reserve-item borrow-reserve-item-right"
|
||||
reserve={lendingReserve}
|
||||
mode={SideReserveOverviewMode.Borrow}
|
||||
/>
|
||||
</div>
|
||||
<Row gutter={GUTTER}>
|
||||
<Col xs={24} xl={5}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Your loans value"
|
||||
value={loansValue}
|
||||
precision={2}
|
||||
prefix="$"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} xl={5}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Number of loans"
|
||||
value={numberOfLoans}
|
||||
precision={0}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} xl={5}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Borrowing power"
|
||||
value={borrowingPower}
|
||||
valueStyle={{ color: "#3f8600" }}
|
||||
precision={2}
|
||||
prefix="$"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} xl={9}>
|
||||
<Card>
|
||||
<BarChartStatistic
|
||||
title="Your Loans"
|
||||
items={userObligations}
|
||||
getPct={(item) => item.obligation.info.borrowedInQuote / loansValue}
|
||||
name={(item) => item.obligation.info.repayName} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={GUTTER} className="flexColumn">
|
||||
<Col xs={24} xl={15}>
|
||||
<BorrowInput
|
||||
className="card-fill"
|
||||
reserve={lendingReserve}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} xl={9}>
|
||||
<SideReserveOverview
|
||||
className="card-fill"
|
||||
reserve={lendingReserve}
|
||||
mode={SideReserveOverviewMode.Borrow}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.borrow-reserve-item {
|
||||
margin: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.borrow-reserve-container {
|
||||
|
@ -13,18 +14,3 @@
|
|||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.borrow-reserve-item-left {
|
||||
flex: 60%;
|
||||
}
|
||||
|
||||
.borrow-reserve-item-right {
|
||||
flex: 30%;
|
||||
}
|
||||
|
||||
/* Responsive layout - makes a one column layout instead of a two-column layout */
|
||||
@media (max-width: 600px) {
|
||||
.borrow-reserve-item-right, .borrow-reserve-item-left {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { Col, Row } from "antd";
|
||||
import React from "react";
|
||||
import { LABELS } from "../../constants";
|
||||
import { GUTTER, LABELS } from "../../constants";
|
||||
import { useWallet } from "../../contexts/wallet";
|
||||
import { useUserDeposits, useUserObligations } from "./../../hooks";
|
||||
import { DashboardObligations } from "./obligation";
|
||||
|
@ -28,7 +28,7 @@ export const DashboardView = () => {
|
|||
{LABELS.NO_LOANS_NO_DEPOSITS}
|
||||
</div>
|
||||
)}
|
||||
{connected && <Row gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]} >
|
||||
{connected && <Row gutter={GUTTER} >
|
||||
{userDeposits.length >0 && (
|
||||
<Col md={24} xl={12} span={24}>
|
||||
<DashboardDeposits />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { MintInfo } from "@solana/spl-token";
|
||||
import { Card, Col, Row, Statistic } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { LABELS } from "../../constants";
|
||||
import { GUTTER, LABELS } from "../../constants";
|
||||
import { cache, ParsedAccount } from "../../contexts/accounts";
|
||||
import { useConnectionConfig } from "../../contexts/connection";
|
||||
import { useMarkets } from "../../contexts/market";
|
||||
|
@ -83,7 +83,7 @@ export const HomeView = () => {
|
|||
return (
|
||||
<div className="flexColumn">
|
||||
<Row
|
||||
gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]}
|
||||
gutter={GUTTER}
|
||||
className="home-info-row" >
|
||||
<Col xs={24} xl={5}>
|
||||
<Card>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { LABELS } from "../../constants";
|
||||
import { GUTTER, LABELS } from "../../constants";
|
||||
import { LiquidateItem } from "./item";
|
||||
import { useEnrichedLendingObligations } from "./../../hooks";
|
||||
import "./style.less";
|
||||
|
@ -30,17 +30,17 @@ export const LiquidateView = () => {
|
|||
<div className="liquidate-info">{LABELS.LIQUIDATE_NO_LOANS}</div>
|
||||
) : (
|
||||
<div className="flexColumn">
|
||||
<Row gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]}
|
||||
className="home-info-row" >
|
||||
<Card>
|
||||
<Typography>
|
||||
{LABELS.LIQUIDATION_INFO}
|
||||
</Typography>
|
||||
</Card>
|
||||
<Row gutter={GUTTER}>
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<Typography>
|
||||
{LABELS.LIQUIDATION_INFO}
|
||||
</Typography>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row
|
||||
gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]}
|
||||
className="home-info-row" >
|
||||
gutter={GUTTER}>
|
||||
<Col xs={24} xl={5}>
|
||||
<Card>
|
||||
<Statistic
|
||||
|
@ -81,23 +81,27 @@ export const LiquidateView = () => {
|
|||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card >
|
||||
<div className="liquidate-item liquidate-header">
|
||||
<div>{LABELS.TABLE_TITLE_ASSET}</div>
|
||||
<div>{LABELS.TABLE_TITLE_LOAN_BALANCE}</div>
|
||||
<div>{LABELS.TABLE_TITLE_COLLATERAL_BALANCE}</div>
|
||||
<div>{LABELS.TABLE_TITLE_APY}</div>
|
||||
<div>{LABELS.TABLE_TITLE_LTV}</div>
|
||||
<div>{LABELS.TABLE_TITLE_HEALTH}</div>
|
||||
<div>{LABELS.TABLE_TITLE_ACTION}</div>
|
||||
</div>
|
||||
{atRisk.map((item) => (
|
||||
<LiquidateItem
|
||||
key={item.account.pubkey.toBase58()}
|
||||
item={item}
|
||||
></LiquidateItem>
|
||||
))}
|
||||
</Card>
|
||||
<Row gutter={GUTTER}>
|
||||
<Col span={24}>
|
||||
<Card className="card-fill">
|
||||
<div className="liquidate-item liquidate-header">
|
||||
<div>{LABELS.TABLE_TITLE_ASSET}</div>
|
||||
<div>{LABELS.TABLE_TITLE_LOAN_BALANCE}</div>
|
||||
<div>{LABELS.TABLE_TITLE_COLLATERAL_BALANCE}</div>
|
||||
<div>{LABELS.TABLE_TITLE_APY}</div>
|
||||
<div>{LABELS.TABLE_TITLE_LTV}</div>
|
||||
<div>{LABELS.TABLE_TITLE_HEALTH}</div>
|
||||
<div>{LABELS.TABLE_TITLE_ACTION}</div>
|
||||
</div>
|
||||
{atRisk.map((item) => (
|
||||
<LiquidateItem
|
||||
key={item.account.pubkey.toBase58()}
|
||||
item={item}
|
||||
></LiquidateItem>
|
||||
))}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -44,4 +44,5 @@
|
|||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
}
|
Loading…
Reference in New Issue