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;
}
.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;

View File

@ -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"
>
&nbsp;{" "}
{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>
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>
);

View File

@ -2,3 +2,4 @@ export * from "./ids";
export * from "./labels";
export * from "./math";
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,23 +2,43 @@ 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,

View File

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

View File

@ -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() {

View File

@ -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,
};
}

View File

@ -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

View File

@ -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">
<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="borrow-reserve-item borrow-reserve-item-left"
className="card-fill"
reserve={lendingReserve}
/>
</Col>
<Col xs={24} xl={9}>
<SideReserveOverview
className="borrow-reserve-item borrow-reserve-item-right"
className="card-fill"
reserve={lendingReserve}
mode={SideReserveOverviewMode.Borrow}
/>
</div>
</Col>
</Row>
</div>
);
};

View File

@ -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%;
}
}

View File

@ -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 />

View File

@ -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>

View File

@ -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" >
<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,7 +81,9 @@ export const LiquidateView = () => {
</Card>
</Col>
</Row>
<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>
@ -98,6 +100,8 @@ export const LiquidateView = () => {
></LiquidateItem>
))}
</Card>
</Col>
</Row>
</div>
)}
</div>

View File

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