feat: add borrow page overview

This commit is contained in:
bartosz-lipinski 2020-12-28 11:45:23 -06:00
parent c5d1e51963
commit 9805849b85
15 changed files with 211 additions and 90 deletions

View File

@ -231,6 +231,10 @@ em {
flex: 1; flex: 1;
} }
.card-fill {
height: 100%;
}
.card-row { .card-row {
box-sizing: border-box; box-sizing: border-box;
margin: 5px 0px; margin: 5px 0px;
@ -295,6 +299,12 @@ em {
font-weight: bold; font-weight: bold;
} }
.ant-select-selection-item {
.token-balance {
display: none;
};
};
.token-input { .token-input {
display: flex; display: flex;
align-items: center; align-items: center;
@ -303,6 +313,16 @@ em {
margin: 5px 0px; 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 { .dashboard-amount-quote {
font-size: 10px; font-size: 10px;
font-style: normal; font-style: normal;

View File

@ -1,14 +1,44 @@
import React from "react"; import React from "react";
import { useLendingReserves } from "../../hooks"; import { useLendingReserves, UserDeposit, useUserDeposits } from "../../hooks";
import { LendingMarket, LendingReserve } from "../../models"; import { LendingMarket, LendingReserve } from "../../models";
import { TokenIcon } from "../TokenIcon"; import { TokenIcon } from "../TokenIcon";
import { getTokenName } from "../../utils/utils"; import { formatAmount, getTokenName } from "../../utils/utils";
import { Select } from "antd"; import { Select } from "antd";
import { useConnectionConfig } from "../../contexts/connection"; import { useConnectionConfig } from "../../contexts/connection";
import { cache, ParsedAccount } from "../../contexts/accounts"; import { cache, ParsedAccount } from "../../contexts/accounts";
const { Option } = Select; 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"
>
&nbsp;{" "}
{userDeposit ? formatAmount(userDeposit.info.amount): '--'}
</span>
</div>
</>
);
}
export const CollateralSelector = (props: { export const CollateralSelector = (props: {
reserve: LendingReserve; reserve: LendingReserve;
collateralReserve?: string; collateralReserve?: string;
@ -17,13 +47,13 @@ export const CollateralSelector = (props: {
}) => { }) => {
const { reserveAccounts } = useLendingReserves(); const { reserveAccounts } = useLendingReserves();
const { tokenMap } = useConnectionConfig(); 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 LendingMarket
>; >;
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals( const onlyQuoteAllowed = props.reserve?.liquidityMint?.toBase58() !==
market?.info?.quoteMint market?.info?.quoteMint?.toBase58();
);
return ( return (
<Select <Select
@ -54,17 +84,15 @@ export const CollateralSelector = (props: {
const mint = reserve.info.liquidityMint.toBase58(); const mint = reserve.info.liquidityMint.toBase58();
const address = reserve.pubkey.toBase58(); const address = reserve.pubkey.toBase58();
const name = getTokenName(tokenMap, mint); const name = getTokenName(tokenMap, mint);
return (
<Option key={address} value={address} name={name} title={address}>
<div return <Option key={address} value={address} name={name} title={address}>
key={address} <CollateralItem
style={{ display: "flex", alignItems: "center" }} reserve={reserve}
> userDeposit={userDeposits.find(dep => dep.reserve.pubkey.toBase58() === address)}
<TokenIcon mintAddress={mint} /> mint={mint}
{name} name={name} />
</div> </Option>
</Option>
);
})} })}
</Select> </Select>
); );

View File

@ -2,3 +2,4 @@ export * from "./ids";
export * from "./labels"; export * from "./labels";
export * from "./math"; export * from "./math";
export * from "./marks"; export * from "./marks";
export * from "./style";

1
src/constants/style.tsx Normal file
View File

@ -0,0 +1 @@
export const GUTTER = [16, { xs: 8, sm: 16, md: 16, lg: 16 }] as any;

View File

@ -2,28 +2,48 @@ import { PublicKey } from "@solana/web3.js";
import { useMemo } from "react"; import { useMemo } from "react";
import { useMidPriceInUSD } from "../contexts/market"; import { useMidPriceInUSD } from "../contexts/market";
import { useLendingMarket } from "./useLendingMarket"; import { useLendingMarket } from "./useLendingMarket";
import { useLendingReserve } from "./useLendingReserves"; import { getLendingReserves, useLendingReserve } from "./useLendingReserves";
import { useUserDeposits } from "./useUserDeposits"; import { useUserDeposits } from "./useUserDeposits";
// TODO: add option to decrease buying power by overcollateralization factor // TODO: add option to decrease buying power by overcollateralization factor
export function useBorrowingPower(reserveAddress: string | PublicKey, overcollateralize = true) { export function useBorrowingPower(reserveAddress: string | PublicKey, overcollateralize = true) {
const key = useMemo(() => typeof reserveAddress === 'string' ? reserveAddress : reserveAddress.toBase58(), [reserveAddress]); 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 reserve = useLendingReserve(key);
const liquidityMint = reserve?.info.liquidityMint; const liquidityMint = reserve?.info.liquidityMint;
const market = useLendingMarket(liquidityMint); const liquidityMintAddress = liquidityMint?.toBase58();
const price = useMidPriceInUSD(liquidityMint.toBase58()).price; 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 // amounts already expressed as quite mint
if(liquidityMint.toBase58() === market?.info.quoteMint?.toBase58()) { if (liquidityMintAddress === market?.info.quoteMint?.toBase58()) {
return { return {
borrowingPower: totalInQuote, borrowingPower: totalInQuote,
totalInQuote, totalInQuote,
}; };
} }
return { return {
borrowingPower: totalInQuote / price, borrowingPower: totalInQuote / price,

View File

@ -50,6 +50,7 @@ export function useUserCollateralBalance(
balanceInUSD, balanceInUSD,
mint: reserve?.collateralMint, mint: reserve?.collateralMint,
accounts, accounts,
hasBalance: accounts.length > 0 && balance > 0,
}; };
} }
export function calculateCollateralBalance( export function calculateCollateralBalance(

View File

@ -3,11 +3,11 @@ import { useEffect, useMemo, useState } from "react";
import { LendingReserve, LendingReserveParser } from "../models/lending"; import { LendingReserve, LendingReserveParser } from "../models/lending";
import { cache, ParsedAccount } from "./../contexts/accounts"; import { cache, ParsedAccount } from "./../contexts/accounts";
const getLendingReserves = () => { export const getLendingReserves = () => {
return cache return cache
.byParser(LendingReserveParser) .byParser(LendingReserveParser)
.map((id) => cache.get(id)) .map((id) => cache.get(id))
.filter((acc) => acc !== undefined) as any[]; .filter((acc) => acc !== undefined) as ParsedAccount<LendingReserve>[];
}; };
export function useLendingReserves() { export function useLendingReserves() {

View File

@ -5,7 +5,8 @@ import { useMarkets } from "../contexts/market";
import { fromLamports } from "../utils/utils"; import { fromLamports } from "../utils/utils";
import { useUserAccounts } from "./useUserAccounts"; 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 { userAccounts } = useUserAccounts();
const [balanceInUSD, setBalanceInUSD] = useState(0); const [balanceInUSD, setBalanceInUSD] = useState(0);
const { marketEmitter, midPriceInUSD } = useMarkets(); const { marketEmitter, midPriceInUSD } = useMarkets();
@ -15,7 +16,7 @@ export function useUserBalance(mint?: PublicKey, account?: PublicKey) {
return userAccounts return userAccounts
.filter( .filter(
(acc) => (acc) =>
mint?.equals(acc.info.mint) && mint === acc.info.mint.toBase58() &&
(!account || account.equals(acc.pubkey)) (!account || account.equals(acc.pubkey))
) )
.sort((a, b) => b.info.amount.sub(a.info.amount).toNumber()); .sort((a, b) => b.info.amount.sub(a.info.amount).toNumber());
@ -32,7 +33,7 @@ export function useUserBalance(mint?: PublicKey, account?: PublicKey) {
useEffect(() => { useEffect(() => {
const updateBalance = () => { const updateBalance = () => {
setBalanceInUSD(balance * midPriceInUSD(mint?.toBase58() || '')); setBalanceInUSD(balance * midPriceInUSD(mint || ''));
} }
const dispose = marketEmitter.onMarket((args) => { const dispose = marketEmitter.onMarket((args) => {
@ -51,5 +52,6 @@ export function useUserBalance(mint?: PublicKey, account?: PublicKey) {
balanceLamports, balanceLamports,
balanceInUSD, balanceInUSD,
accounts, accounts,
hasBalance: accounts.length > 0 && balance > 0,
}; };
} }

View File

@ -148,7 +148,7 @@ const abbreviateNumber = (number: number, precision: number) => {
return scaled.toFixed(precision) + suffix; 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); abbr ? abbreviateNumber(val, precision) : val.toFixed(precision);
export function formatTokenAmount( export function formatTokenAmount(
@ -164,7 +164,7 @@ export function formatTokenAmount(
return ""; return "";
} }
return `${[prefix]}${format( return `${[prefix]}${formatAmount(
fromLamports(account, mint, rate), fromLamports(account, mint, rate),
precision, precision,
abbr abbr

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { useLendingReserve } from "../../hooks"; import { useBorrowingPower, useLendingReserve, useUserObligations } from "../../hooks";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import "./style.less"; import "./style.less";
@ -8,28 +8,85 @@ import {
SideReserveOverview, SideReserveOverview,
SideReserveOverviewMode, SideReserveOverviewMode,
} from "../../components/SideReserveOverview"; } from "../../components/SideReserveOverview";
import { Card, Col, Row, Statistic } from "antd";
import { BarChartStatistic } from "../../components/BarChartStatistic";
import { GUTTER } from "../../constants";
export const BorrowReserveView = () => { export const BorrowReserveView = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const lendingReserve = useLendingReserve(id); const lendingReserve = useLendingReserve(id);
const { userObligations, totalInQuote: loansValue } = useUserObligations();
const { totalInQuote: borrowingPower } = useBorrowingPower(id)
if (!lendingReserve) { if (!lendingReserve) {
return null; return null;
} }
const numberOfLoans = userObligations
.filter(ob =>
// ob.obligation.info.borrowReserve.toBase58() === id &&
ob.obligation.info.collateralInQuote > 0)
.length;
return ( return (
<div className="borrow-reserve"> <div className="borrow-reserve">
<div className="borrow-reserve-container"> <Row gutter={GUTTER}>
<BorrowInput <Col xs={24} xl={5}>
className="borrow-reserve-item borrow-reserve-item-left" <Card>
reserve={lendingReserve} <Statistic
/> title="Your loans value"
<SideReserveOverview value={loansValue}
className="borrow-reserve-item borrow-reserve-item-right" precision={2}
reserve={lendingReserve} prefix="$"
mode={SideReserveOverviewMode.Borrow} />
/> </Card>
</div> </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> </div>
); );
}; };

View File

@ -2,10 +2,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
overflow-x: hidden;
} }
.borrow-reserve-item { .borrow-reserve-item {
margin: 4px; height: 100%;
} }
.borrow-reserve-container { .borrow-reserve-container {
@ -13,18 +14,3 @@
flex-wrap: wrap; flex-wrap: wrap;
flex: 1; 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%;
}
}

View File

@ -1,6 +1,6 @@
import { Col, Row } from "antd"; import { Col, Row } from "antd";
import React from "react"; import React from "react";
import { LABELS } from "../../constants"; import { GUTTER, LABELS } from "../../constants";
import { useWallet } from "../../contexts/wallet"; import { useWallet } from "../../contexts/wallet";
import { useUserDeposits, useUserObligations } from "./../../hooks"; import { useUserDeposits, useUserObligations } from "./../../hooks";
import { DashboardObligations } from "./obligation"; import { DashboardObligations } from "./obligation";
@ -28,7 +28,7 @@ export const DashboardView = () => {
{LABELS.NO_LOANS_NO_DEPOSITS} {LABELS.NO_LOANS_NO_DEPOSITS}
</div> </div>
)} )}
{connected && <Row gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]} > {connected && <Row gutter={GUTTER} >
{userDeposits.length >0 && ( {userDeposits.length >0 && (
<Col md={24} xl={12} span={24}> <Col md={24} xl={12} span={24}>
<DashboardDeposits /> <DashboardDeposits />

View File

@ -1,7 +1,7 @@
import { MintInfo } from "@solana/spl-token"; import { MintInfo } from "@solana/spl-token";
import { Card, Col, Row, Statistic } from "antd"; import { Card, Col, Row, Statistic } from "antd";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { LABELS } from "../../constants"; import { GUTTER, LABELS } from "../../constants";
import { cache, ParsedAccount } from "../../contexts/accounts"; import { cache, ParsedAccount } from "../../contexts/accounts";
import { useConnectionConfig } from "../../contexts/connection"; import { useConnectionConfig } from "../../contexts/connection";
import { useMarkets } from "../../contexts/market"; import { useMarkets } from "../../contexts/market";
@ -83,7 +83,7 @@ export const HomeView = () => {
return ( return (
<div className="flexColumn"> <div className="flexColumn">
<Row <Row
gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]} gutter={GUTTER}
className="home-info-row" > className="home-info-row" >
<Col xs={24} xl={5}> <Col xs={24} xl={5}>
<Card> <Card>

View File

@ -1,5 +1,5 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { LABELS } from "../../constants"; import { GUTTER, LABELS } from "../../constants";
import { LiquidateItem } from "./item"; import { LiquidateItem } from "./item";
import { useEnrichedLendingObligations } from "./../../hooks"; import { useEnrichedLendingObligations } from "./../../hooks";
import "./style.less"; import "./style.less";
@ -30,17 +30,17 @@ export const LiquidateView = () => {
<div className="liquidate-info">{LABELS.LIQUIDATE_NO_LOANS}</div> <div className="liquidate-info">{LABELS.LIQUIDATE_NO_LOANS}</div>
) : ( ) : (
<div className="flexColumn"> <div className="flexColumn">
<Row gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]} <Row gutter={GUTTER}>
className="home-info-row" > <Col span={24}>
<Card> <Card>
<Typography> <Typography>
{LABELS.LIQUIDATION_INFO} {LABELS.LIQUIDATION_INFO}
</Typography> </Typography>
</Card> </Card>
</Col>
</Row> </Row>
<Row <Row
gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]} gutter={GUTTER}>
className="home-info-row" >
<Col xs={24} xl={5}> <Col xs={24} xl={5}>
<Card> <Card>
<Statistic <Statistic
@ -81,23 +81,27 @@ export const LiquidateView = () => {
</Card> </Card>
</Col> </Col>
</Row> </Row>
<Card > <Row gutter={GUTTER}>
<div className="liquidate-item liquidate-header"> <Col span={24}>
<div>{LABELS.TABLE_TITLE_ASSET}</div> <Card className="card-fill">
<div>{LABELS.TABLE_TITLE_LOAN_BALANCE}</div> <div className="liquidate-item liquidate-header">
<div>{LABELS.TABLE_TITLE_COLLATERAL_BALANCE}</div> <div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_APY}</div> <div>{LABELS.TABLE_TITLE_LOAN_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_LTV}</div> <div>{LABELS.TABLE_TITLE_COLLATERAL_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_HEALTH}</div> <div>{LABELS.TABLE_TITLE_APY}</div>
<div>{LABELS.TABLE_TITLE_ACTION}</div> <div>{LABELS.TABLE_TITLE_LTV}</div>
</div> <div>{LABELS.TABLE_TITLE_HEALTH}</div>
{atRisk.map((item) => ( <div>{LABELS.TABLE_TITLE_ACTION}</div>
<LiquidateItem </div>
key={item.account.pubkey.toBase58()} {atRisk.map((item) => (
item={item} <LiquidateItem
></LiquidateItem> key={item.account.pubkey.toBase58()}
))} item={item}
</Card> ></LiquidateItem>
))}
</Card>
</Col>
</Row>
</div> </div>
)} )}
</div> </div>

View File

@ -44,4 +44,5 @@
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
flex: 1; flex: 1;
overflow-x: hidden;
} }