Merge branch 'main' into main

This commit is contained in:
Bartosz Lipinski 2020-12-30 08:33:43 -06:00 committed by GitHub
commit 02ea379405
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 687 additions and 307 deletions

View File

@ -4,8 +4,13 @@ Any content produced by Solana, or developer resources that Solana provides, are
## TODO ## TODO
- [] Finish Reserve overview page (chart, and borrows information) * Add github link
- [] Deposit view calculate APY and health factor * Repay from reserve (add selection for obligation/loan)
- [] Add github link * Add support for token names in URL in addition to reserve address
- [] Repay from reserve (add selection for obligation/loan) * Repay select first loan when accessing from reserve overview
- [] Add support for token names in URL in addition to reserve address * convert deposit info to multiple cards
* add slider to borrow that shows risk/collateral usage
* integrate fees
* ephemeral keys
* merge margin PR and apply swap like style to borrow/repay
* add values to bar chart labels

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,12 +313,48 @@ 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;
text-align: right; 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) { @media only screen and (max-width: 600px) {
.exchange-card { .exchange-card {
width: 360px; width: 360px;

View File

@ -1,9 +1,14 @@
import { Account, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'; import {
import { sendTransaction } from '../contexts/connection'; Account,
import { notify } from '../utils/notifications'; Connection,
import { LendingReserve } from './../models/lending/reserve'; PublicKey,
import { AccountLayout, MintInfo, MintLayout, Token } from '@solana/spl-token'; TransactionInstruction,
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../utils/ids'; } from "@solana/web3.js";
import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import { LendingReserve } from "./../models/lending/reserve";
import { AccountLayout, MintInfo, MintLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import { import {
createTempMemoryAccount, createTempMemoryAccount,
createUninitializedAccount, createUninitializedAccount,
@ -20,8 +25,9 @@ import {
LendingMarket, LendingMarket,
BorrowAmountType, BorrowAmountType,
LendingObligation, LendingObligation,
} from '../models'; approve,
import { toLamports } from '../utils/utils'; } from "../models";
import { toLamports } from "../utils/utils";
export const borrow = async ( export const borrow = async (
connection: Connection, connection: Connection,
@ -141,8 +147,13 @@ export const borrow = async (
); );
// create approval for transfer transactions // create approval for transfer transactions
instructions.push( approve(
Token.createApproveInstruction(TOKEN_PROGRAM_ID, fromAccount, authority, wallet.publicKey, [], fromLamports) instructions,
cleanupInstructions,
fromAccount,
authority,
wallet.publicKey,
fromLamports
); );
const dexMarketAddress = borrowReserve.info.dexMarketOption const dexMarketAddress = borrowReserve.info.dexMarketOption

View File

@ -1,11 +1,24 @@
import { Account, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'; import {
import { sendTransaction } from '../contexts/connection'; Account,
import { notify } from '../utils/notifications'; Connection,
import { depositInstruction, initReserveInstruction, LendingReserve } from './../models/lending'; PublicKey,
import { AccountLayout, Token } from '@solana/spl-token'; TransactionInstruction,
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../utils/ids'; } from "@solana/web3.js";
import { createUninitializedAccount, ensureSplAccount, findOrCreateAccountByMint } from './account'; import { sendTransaction } from "../contexts/connection";
import { TokenAccount } from '../models'; import { notify } from "../utils/notifications";
import {
depositInstruction,
initReserveInstruction,
LendingReserve,
} from "./../models/lending";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import {
createUninitializedAccount,
ensureSplAccount,
findOrCreateAccountByMint,
} from "./account";
import { approve, TokenAccount } from "../models";
export const deposit = async ( export const deposit = async (
from: TokenAccount, from: TokenAccount,
@ -45,8 +58,13 @@ export const deposit = async (
); );
// create approval for transfer transactions // create approval for transfer transactions
instructions.push( approve(
Token.createApproveInstruction(TOKEN_PROGRAM_ID, fromAccount, authority, wallet.publicKey, [], amountLamports) instructions,
cleanupInstructions,
fromAccount,
authority,
wallet.publicKey,
amountLamports
); );
let toAccount: PublicKey; let toAccount: PublicKey;

View File

@ -1,13 +1,18 @@
import { Account, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'; import {
import { sendTransaction } from '../contexts/connection'; Account,
import { notify } from '../utils/notifications'; Connection,
import { LendingReserve } from './../models/lending/reserve'; PublicKey,
import { liquidateInstruction } from './../models/lending/liquidate'; TransactionInstruction,
import { AccountLayout, Token } from '@solana/spl-token'; } from "@solana/web3.js";
import { createTempMemoryAccount, ensureSplAccount, findOrCreateAccountByMint } from './account'; import { sendTransaction } from "../contexts/connection";
import { LendingMarket, LendingObligation, TokenAccount } from '../models'; import { notify } from "../utils/notifications";
import { cache, ParsedAccount } from '../contexts/accounts'; import { LendingReserve } from "./../models/lending/reserve";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../utils/ids'; import { liquidateInstruction } from "./../models/lending/liquidate";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import { createTempMemoryAccount, ensureSplAccount, findOrCreateAccountByMint } from "./account";
import { approve, LendingMarket, LendingObligation, TokenAccount } from "../models";
import { cache, ParsedAccount } from "../contexts/accounts";
export const liquidate = async ( export const liquidate = async (
connection: Connection, connection: Connection,
@ -50,8 +55,13 @@ export const liquidate = async (
); );
// create approval for transfer transactions // create approval for transfer transactions
instructions.push( approve(
Token.createApproveInstruction(TOKEN_PROGRAM_ID, fromAccount, authority, wallet.publicKey, [], amountLamports) instructions,
cleanupInstructions,
fromAccount,
authority,
wallet.publicKey,
amountLamports
); );
// get destination account // get destination account

View File

@ -1,13 +1,18 @@
import { Account, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'; import {
import { sendTransaction } from '../contexts/connection'; Account,
import { notify } from '../utils/notifications'; Connection,
import { LendingReserve } from './../models/lending/reserve'; PublicKey,
import { repayInstruction } from './../models/lending/repay'; TransactionInstruction,
import { AccountLayout, Token } from '@solana/spl-token'; } from "@solana/web3.js";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../utils/ids'; import { sendTransaction } from "../contexts/connection";
import { findOrCreateAccountByMint } from './account'; import { notify } from "../utils/notifications";
import { LendingObligation, TokenAccount } from '../models'; import { LendingReserve } from "./../models/lending/reserve";
import { ParsedAccount } from '../contexts/accounts'; import { repayInstruction } from "./../models/lending/repay";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import { findOrCreateAccountByMint } from "./account";
import { approve, LendingObligation, TokenAccount } from "../models";
import { ParsedAccount } from "../contexts/accounts";
export const repay = async ( export const repay = async (
from: TokenAccount, // CollateralAccount from: TokenAccount, // CollateralAccount
@ -46,8 +51,13 @@ export const repay = async (
const fromAccount = from.pubkey; const fromAccount = from.pubkey;
// create approval for transfer transactions // create approval for transfer transactions
instructions.push( approve(
Token.createApproveInstruction(TOKEN_PROGRAM_ID, fromAccount, authority, wallet.publicKey, [], amountLamports) instructions,
cleanupInstructions,
fromAccount,
authority,
wallet.publicKey,
amountLamports
); );
// get destination account // get destination account
@ -62,15 +72,13 @@ export const repay = async (
); );
// create approval for transfer transactions // create approval for transfer transactions
instructions.push( approve(
Token.createApproveInstruction( instructions,
TOKEN_PROGRAM_ID, cleanupInstructions,
obligationToken.pubkey, obligationToken.pubkey,
authority, authority,
wallet.publicKey, wallet.publicKey,
[], obligationToken.info.amount.toNumber()
obligationToken.info.amount.toNumber()
)
); );
// TODO: add obligation // TODO: add obligation

View File

@ -1,11 +1,16 @@
import { Account, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'; import {
import { sendTransaction } from '../contexts/connection'; Account,
import { notify } from '../utils/notifications'; Connection,
import { LendingReserve, withdrawInstruction } from './../models/lending'; PublicKey,
import { AccountLayout, Token } from '@solana/spl-token'; TransactionInstruction,
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../utils/ids'; } from "@solana/web3.js";
import { findOrCreateAccountByMint } from './account'; import { sendTransaction } from "../contexts/connection";
import { TokenAccount } from '../models'; import { notify } from "../utils/notifications";
import { LendingReserve, withdrawInstruction } from "./../models/lending";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import { findOrCreateAccountByMint } from "./account";
import { approve, TokenAccount } from "../models";
export const withdraw = async ( export const withdraw = async (
from: TokenAccount, // CollateralAccount from: TokenAccount, // CollateralAccount
@ -33,8 +38,13 @@ export const withdraw = async (
const fromAccount = from.pubkey; const fromAccount = from.pubkey;
// create approval for transfer transactions // create approval for transfer transactions
instructions.push( approve(
Token.createApproveInstruction(TOKEN_PROGRAM_ID, fromAccount, authority, wallet.publicKey, [], amountLamports) instructions,
cleanupInstructions,
fromAccount,
authority,
wallet.publicKey,
amountLamports
); );
// get destination account // get destination account

View File

@ -10,7 +10,7 @@ import {
LendingReserveParser, LendingReserveParser,
} from "../../models"; } from "../../models";
import { TokenIcon } from "../TokenIcon"; import { TokenIcon } from "../TokenIcon";
import { Button, Card } from "antd"; import { Card } from "antd";
import { cache, ParsedAccount } from "../../contexts/accounts"; import { cache, ParsedAccount } from "../../contexts/accounts";
import { NumericInput } from "../Input/numeric"; import { NumericInput } from "../Input/numeric";
import { useConnection } from "../../contexts/connection"; import { useConnection } from "../../contexts/connection";
@ -21,6 +21,7 @@ import "./style.less";
import { LABELS } from "../../constants"; import { LABELS } from "../../constants";
import { ActionConfirmation } from "./../ActionConfirmation"; import { ActionConfirmation } from "./../ActionConfirmation";
import { BackButton } from "./../BackButton"; import { BackButton } from "./../BackButton";
import { ConnectButton } from "../ConnectButton";
export const BorrowInput = (props: { export const BorrowInput = (props: {
className?: string; className?: string;
@ -150,14 +151,14 @@ export const BorrowInput = (props: {
/> />
<div>{name}</div> <div>{name}</div>
</div> </div>
<Button <ConnectButton
type="primary" type="primary"
onClick={onBorrow} onClick={onBorrow}
loading={pendingTx} loading={pendingTx}
disabled={fromAccounts.length === 0} disabled={fromAccounts.length === 0}
> >
{LABELS.BORROW_ACTION} {fromAccounts.length === 0 ? LABELS.NO_DEPOSITS : LABELS.BORROW_ACTION}
</Button> </ConnectButton>
<BackButton /> <BackButton />
</div> </div>
)} )}

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,11 +47,17 @@ 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<LendingMarket>; const market = cache.get(props.reserve?.lendingMarket) as ParsedAccount<
LendingMarket
>;
if (!market) return null; if (!market) return null;
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(market?.info?.quoteMint); const quoteMintAddress = market?.info?.quoteMint?.toBase58();
const onlyQuoteAllowed = props.reserve?.liquidityMint?.toBase58() !==
quoteMintAddress;
return ( return (
<Select <Select
@ -46,14 +82,14 @@ 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}> return <Option key={address} value={address} name={name} title={address}>
<div key={address} style={{ display: 'flex', alignItems: 'center' }}> <CollateralItem
<TokenIcon mintAddress={mint} /> reserve={reserve}
{name} userDeposit={userDeposits.find(dep => dep.reserve.pubkey.toBase58() === address)}
</div> mint={mint}
</Option> name={name} />
); </Option>
})} })}
</Select> </Select>
); );

View File

@ -8,9 +8,9 @@ export const ConnectButton = (
props: ButtonProps & React.RefAttributes<HTMLElement> props: ButtonProps & React.RefAttributes<HTMLElement>
) => { ) => {
const { wallet, connected } = useWallet(); const { wallet, connected } = useWallet();
const { onClick, children, ...rest } = props; const { onClick, children, disabled, ...rest } = props;
return ( return (
<Button {...rest} onClick={connected ? onClick : wallet.connect}> <Button {...rest} onClick={connected ? onClick : wallet.connect} disabled={connected && disabled}>
{connected ? props.children : LABELS.CONNECT_LABEL} {connected ? props.children : LABELS.CONNECT_LABEL}
</Button> </Button>
); );

View File

@ -7,7 +7,7 @@ import {
} from "../../hooks"; } from "../../hooks";
import { LendingReserve } from "../../models/lending"; import { LendingReserve } from "../../models/lending";
import { TokenIcon } from "../TokenIcon"; import { TokenIcon } from "../TokenIcon";
import { Button, Card, Slider } from "antd"; import { Card, Slider } from "antd";
import { NumericInput } from "../Input/numeric"; import { NumericInput } from "../Input/numeric";
import { useConnection } from "../../contexts/connection"; import { useConnection } from "../../contexts/connection";
import { useWallet } from "../../contexts/wallet"; import { useWallet } from "../../contexts/wallet";
@ -16,6 +16,7 @@ import { PublicKey } from "@solana/web3.js";
import "./style.less"; import "./style.less";
import { ActionConfirmation } from "./../ActionConfirmation"; import { ActionConfirmation } from "./../ActionConfirmation";
import { LABELS, marks } from "../../constants"; import { LABELS, marks } from "../../constants";
import { ConnectButton } from "../ConnectButton";
export const DepositInput = (props: { export const DepositInput = (props: {
className?: string; className?: string;
@ -126,14 +127,14 @@ export const DepositInput = (props: {
<Slider marks={marks} value={pct} onChange={setPct} /> <Slider marks={marks} value={pct} onChange={setPct} />
<Button <ConnectButton
type="primary" type="primary"
onClick={onDeposit} onClick={onDeposit}
loading={pendingTx} loading={pendingTx}
disabled={fromAccounts.length === 0} disabled={fromAccounts.length === 0}
> >
{LABELS.DEPOSIT_ACTION} {LABELS.DEPOSIT_ACTION}
</Button> </ConnectButton>
</div> </div>
)} )}
</Card> </Card>

View File

@ -9,7 +9,7 @@ import {
} from "../../hooks"; } from "../../hooks";
import { LendingReserve } from "../../models"; import { LendingReserve } from "../../models";
import { TokenIcon } from "../TokenIcon"; import { TokenIcon } from "../TokenIcon";
import { Button, Card, Slider } from "antd"; import { Card, Slider } from "antd";
import { ParsedAccount, useMint } from "../../contexts/accounts"; import { ParsedAccount, useMint } from "../../contexts/accounts";
import { NumericInput } from "../Input/numeric"; import { NumericInput } from "../Input/numeric";
import { useConnection } from "../../contexts/connection"; import { useConnection } from "../../contexts/connection";
@ -21,6 +21,7 @@ import { LABELS, marks } from "../../constants";
import { ActionConfirmation } from "./../ActionConfirmation"; import { ActionConfirmation } from "./../ActionConfirmation";
import { fromLamports, wadToLamports } from "../../utils/utils"; import { fromLamports, wadToLamports } from "../../utils/utils";
import { notify } from "../../utils/notifications"; import { notify } from "../../utils/notifications";
import { ConnectButton } from "../ConnectButton";
export const RepayInput = (props: { export const RepayInput = (props: {
className?: string; className?: string;
@ -169,14 +170,14 @@ export const RepayInput = (props: {
disabled={true} disabled={true}
/> />
<Button <ConnectButton
type="primary" type="primary"
onClick={onRepay} onClick={onRepay}
loading={pendingTx} loading={pendingTx}
disabled={fromAccounts.length === 0} disabled={fromAccounts.length === 0}
> >
{LABELS.REPAY_ACTION} {LABELS.REPAY_ACTION}
</Button> </ConnectButton>
</div> </div>
)} )}
</Card> </Card>

View File

@ -1,10 +1,15 @@
import React from "react"; import React from "react";
import { LendingReserve } from "../../models/lending"; import { calculateDepositAPY, LendingReserve } from "../../models/lending";
import { Card } from "antd"; import { Card, Col, Row, Statistic } from "antd";
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from "@solana/web3.js";
import "./style.less"; import "./style.less";
import { LABELS } from "../../constants"; import { GUTTER, LABELS } from "../../constants";
import { ReserveUtilizationChart } from "./../../components/ReserveUtilizationChart"; 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: { export const ReserveStatus = (props: {
className?: string; className?: string;
@ -17,20 +22,113 @@ export const ReserveStatus = (props: {
alignItems: "center", 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 ( return (
<Card <Card
className={props.className} 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} bodyStyle={bodyStyle}
> >
<div <div className="flexColumn">
style={{ <Row gutter={GUTTER}>
display: "flex", <Col span={12}>
flexDirection: "column", <Statistic
justifyContent: "space-around", title="Available Liquidity"
}} value={availableLiquidity}
> valueRender={(node) => <div>
<ReserveUtilizationChart reserve={props.reserve} /> {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> </div>
</Card> </Card>
); );

View File

@ -3,9 +3,11 @@ import { LendingReserve } from "../../models/lending";
import { fromLamports, wadToLamports } from "../../utils/utils"; import { fromLamports, wadToLamports } from "../../utils/utils";
import { useMint } from "../../contexts/accounts"; import { useMint } from "../../contexts/accounts";
import { WaterWave } from "./../WaterWave"; import { WaterWave } from "./../WaterWave";
import { Statistic } from "antd";
export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => { 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( const availableLiquidity = fromLamports(
props.reserve.availableLiquidity.toNumber(), props.reserve.availableLiquidity.toNumber(),
liquidityMint liquidityMint
@ -20,10 +22,20 @@ export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
[props.reserve, liquidityMint] [props.reserve, liquidityMint]
); );
const percent = (availableLiquidity * 100) / (availableLiquidity + totalBorrows);
return ( return (
<WaterWave <WaterWave
style={{ height: 300 }} style={{ height: 300 }}
percent={(availableLiquidity * 100) / (availableLiquidity + totalBorrows)} showPercent={false}
title={
<Statistic
title="Utilization"
value={percent}
suffix="%"
precision={2} />
}
percent={percent}
/> />
); );
}; };

View File

@ -7,19 +7,22 @@ import { PublicKey } from "@solana/web3.js";
export const TokenIcon = (props: { export const TokenIcon = (props: {
mintAddress?: string | PublicKey; mintAddress?: string | PublicKey;
style?: React.CSSProperties; style?: React.CSSProperties;
size?: number;
className?: string; className?: string;
}) => { }) => {
const { tokenMap } = useConnectionConfig(); const { tokenMap } = useConnectionConfig();
const icon = getTokenIcon(tokenMap, props.mintAddress); const icon = getTokenIcon(tokenMap, props.mintAddress);
const size = props.size || 20;
if (icon) { if (icon) {
return ( return (
<img <img
alt="Token icon" alt="Token icon"
className={props.className} className={props.className}
key={icon} key={icon}
width={props.style?.width || "20"} width={props.style?.width || size.toString()}
height={props.style?.height || "20"} height={props.style?.height || size.toString()}
src={icon} src={icon}
style={{ style={{
marginRight: "0.5rem", marginRight: "0.5rem",
@ -38,8 +41,8 @@ export const TokenIcon = (props: {
address={props.mintAddress} address={props.mintAddress}
style={{ style={{
marginRight: "0.5rem", marginRight: "0.5rem",
width: 20, width: size,
height: 20, height: size,
marginTop: 2, marginTop: 2,
...props.style, ...props.style,
}} }}

View File

@ -4,6 +4,7 @@ import {
useTokenName, useTokenName,
useUserBalance, useUserBalance,
useBorrowedAmount, useBorrowedAmount,
useBorrowingPower,
} from "./../../hooks"; } from "./../../hooks";
import { LendingReserve } from "../../models/lending"; import { LendingReserve } from "../../models/lending";
import { formatNumber } from "../../utils/utils"; import { formatNumber } from "../../utils/utils";
@ -29,9 +30,7 @@ export const UserLendingCard = (props: {
); );
const { borrowed: totalBorrowed, borrowedInUSD, ltv, health } = useBorrowedAmount(address); const { borrowed: totalBorrowed, borrowedInUSD, ltv, health } = useBorrowedAmount(address);
const { totalInQuote: borrowingPowerInUSD, borrowingPower } = useBorrowingPower(address);
// TODO: calculate
const available = 0; // use all available deposits and convert using market rate
return ( return (
<Card <Card
@ -82,7 +81,10 @@ export const UserLendingCard = (props: {
Available to you: Available to you:
</Text> </Text>
<div className="card-cell "> <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>
</div> </div>

View File

@ -4,13 +4,15 @@
display: inline-block; display: inline-block;
position: relative; position: relative;
transform-origin: left; transform-origin: left;
display: flex;
align-items: center;
justify-content: center;
.text { .text {
position: absolute; position: absolute;
left: 5px;
top: calc(50% - 15px);
text-align: center; text-align: center;
width: 100%; width: 100%;
span { .title {
color: @text-color-secondary; color: @text-color-secondary;
font-size: 14px; font-size: 14px;
line-height: 22px; line-height: 22px;

View File

@ -5,7 +5,7 @@ export const WaterWave = (props: any) => {
const node = useRef<HTMLCanvasElement>(); const node = useRef<HTMLCanvasElement>();
const root = useRef<HTMLDivElement>(); const root = useRef<HTMLDivElement>();
const [radio, setRadio] = useState(1); const [radio, setRadio] = useState(1);
const { percent, title, style, color } = props; const { percent, title, style, color, showPercent } = props;
const { height } = style; const { height } = style;
const resize = useCallback(() => { const resize = useCallback(() => {
@ -54,8 +54,8 @@ export const WaterWave = (props: any) => {
/> />
</div> </div>
<div className="text" style={{ width: height }}> <div className="text" style={{ width: height }}>
{title && <span>{title}</span>} {title}
<h4>{percent.toFixed(2)}%</h4> <h4>{showPercent && `${percent.toFixed(2)}%`}</h4>
</div> </div>
</div> </div>
); );

View File

@ -8,7 +8,7 @@ import {
} from "../../hooks"; } from "../../hooks";
import { LendingReserve } from "../../models/lending"; import { LendingReserve } from "../../models/lending";
import { TokenIcon } from "../TokenIcon"; import { TokenIcon } from "../TokenIcon";
import { Button, Card, Slider } from "antd"; import { Card, Slider } from "antd";
import { NumericInput } from "../Input/numeric"; import { NumericInput } from "../Input/numeric";
import { useConnection } from "../../contexts/connection"; import { useConnection } from "../../contexts/connection";
import { useWallet } from "../../contexts/wallet"; import { useWallet } from "../../contexts/wallet";
@ -17,6 +17,7 @@ import { PublicKey } from "@solana/web3.js";
import "./style.less"; import "./style.less";
import { LABELS, marks } from "../../constants"; import { LABELS, marks } from "../../constants";
import { ActionConfirmation } from "./../ActionConfirmation"; import { ActionConfirmation } from "./../ActionConfirmation";
import { ConnectButton } from "../ConnectButton";
export const WithdrawInput = (props: { export const WithdrawInput = (props: {
className?: string; className?: string;
@ -134,14 +135,14 @@ export const WithdrawInput = (props: {
<Slider marks={marks} value={pct} onChange={setPct} /> <Slider marks={marks} value={pct} onChange={setPct} />
<Button <ConnectButton
type="primary" type="primary"
onClick={onWithdraw} onClick={onWithdraw}
loading={pendingTx} loading={pendingTx}
disabled={fromAccounts.length === 0} disabled={fromAccounts.length === 0}
> >
{LABELS.WITHDRAW_ACTION} {fromAccounts.length === 0 ? LABELS.NO_DEPOSITS : LABELS.WITHDRAW_ACTION}
</Button> </ConnectButton>
</div> </div>
)} )}
</Card> </Card>

View File

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

View File

@ -1,57 +1,62 @@
export const LABELS = { export const LABELS = {
CONNECT_LABEL: 'Connect Wallet', CONNECT_LABEL: "Connect Wallet",
GIVE_SOL: 'Give me SOL', BORROWING_POWER_USED: "Borrowing Power Used",
FAUCET_INFO: 'This faucet will help you fund your accounts outside of Solana main network.', BORROWING_POWER_VALUE: "Borrowing Power",
ACCOUNT_FUNDED: 'Account funded.', BORROWED_VALUE: "You borrowed",
REPAY_QUESTION: 'How much would you like to repay?', GIVE_SOL: "Give me SOL",
REPAY_ACTION: 'Repay', 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. ",
RESERVE_STATUS_TITLE: 'Reserve Status & Configuration', FAUCET_INFO:
"This faucet will help you fund your accounts outside of Solana main network.",
ACCOUNT_FUNDED: "Account funded.",
REPAY_QUESTION: "How much would you like to repay?",
REPAY_ACTION: "Repay",
RESERVE_STATUS_TITLE: "Reserve Status & Configuration",
AUDIT_WARNING: AUDIT_WARNING:
'Oyster is an unaudited software project used for internal purposes at the Solana Foundation. This app is not for public use.', 'Oyster is an unaudited software project used for internal purposes at the Solana Foundation. This app is not for public use.',
FOOTER: FOOTER:
'This page was produced by the Solana Foundation ("SF") for internal educational and inspiration purposes only. SF does not encourage, induce or sanction the deployment, integration or use of Oyster or any similar application (including its code) in violation of applicable laws or regulations and hereby prohibits any such deployment, integration or use. Anyone using this code or a derivation thereof must comply with applicable laws and regulations when releasing related software.', 'This page was produced by the Solana Foundation ("SF") for internal educational and inspiration purposes only. SF does not encourage, induce or sanction the deployment, integration or use of Oyster or any similar application (including its code) in violation of applicable laws or regulations and hereby prohibits any such deployment, integration or use. Anyone using this code or a derivation thereof must comply with applicable laws and regulations when releasing related software.',
MENU_HOME: 'Home', MENU_HOME: "Home",
MENU_DASHBOARD: 'Dashboard', MENU_DASHBOARD: "Dashboard",
DASHBOARD_INFO: 'Connect to a wallet to view your deposits/loans.', DASHBOARD_INFO: "Connect to a wallet to view your deposits/loans.",
NO_LOANS_NO_DEPOSITS: 'No loans or deposits.', NO_LOANS_NO_DEPOSITS: "No loans or deposits.",
MENU_DEPOSIT: 'Deposit', MENU_DEPOSIT: "Deposit",
MENU_BORROW: 'Borrow', MENU_BORROW: "Borrow",
MENU_LIQUIDATE: 'Liquidate', MENU_LIQUIDATE: "Liquidate",
MENU_FAUCET: 'Faucet', MENU_FAUCET: "Faucet",
MARGIN_TRADING: 'Margin Trading', APP_TITLE: "Oyster Lending",
APP_TITLE: 'Oyster Lending', CONNECT_BUTTON: "Connect",
CONNECT_BUTTON: 'Connect', WALLET_TOOLTIP: "Wallet public key",
WALLET_TOOLTIP: 'Wallet public key', SETTINGS_TOOLTIP: "Settings",
SETTINGS_TOOLTIP: 'Settings', SELECT_COLLATERAL: "Select collateral",
SELECT_COLLATERAL: 'Select collateral', COLLATERAL: "Collateral",
COLLATERAL: 'Collateral', BORROW_QUESTION: "How much would you like to borrow?",
BORROW_QUESTION: 'How much would you like to borrow?', BORROW_ACTION: "Borrow",
BORROW_ACTION: 'Borrow', NO_DEPOSITS: "No collateral",
LIQUIDATE_ACTION: 'Liquidate', LIQUIDATE_ACTION: "Liquidate",
LIQUIDATE_NO_LOANS: 'There are no loans to liquidate.', LIQUIDATE_NO_LOANS: "There are no loans to liquidate.",
TABLE_TITLE_ASSET: 'Asset', TABLE_TITLE_ASSET: "Asset",
TABLE_TITLE_YOUR_LOAN_BALANCE: 'Loan balance', TABLE_TITLE_YOUR_LOAN_BALANCE: "Loan balance",
TABLE_TITLE_LOAN_BALANCE: 'Loan balance', TABLE_TITLE_LOAN_BALANCE: "Loan balance",
TABLE_TITLE_COLLATERAL_BALANCE: 'Collateral', TABLE_TITLE_COLLATERAL_BALANCE: "Collateral",
TABLE_TITLE_DEPOSIT_BALANCE: 'Your deposits', TABLE_TITLE_DEPOSIT_BALANCE: "Your deposits",
TABLE_TITLE_APY: 'APY', TABLE_TITLE_APY: "APY",
TABLE_TITLE_LTV: 'LTV', TABLE_TITLE_LTV: "LTV",
TABLE_TITLE_HEALTH: 'Health Factor', TABLE_TITLE_HEALTH: "Health Factor",
TABLE_TITLE_BORROW_APY: 'Borrow APY', TABLE_TITLE_BORROW_APY: "Borrow APY",
TABLE_TITLE_DEPOSIT_APY: 'Deposit APY', TABLE_TITLE_DEPOSIT_APY: "Deposit APY",
TABLE_TITLE_TOTAL_BORROWED: 'Total Borrowed', TABLE_TITLE_TOTAL_BORROWED: "Total Borrowed",
TABLE_TITLE_MARKET_SIZE: 'Market Size', TABLE_TITLE_MARKET_SIZE: "Market Size",
TABLE_TITLE_ACTION: 'Action', TABLE_TITLE_ACTION: "Action",
TABLE_TITLE_MAX_BORROW: 'Available for you', TABLE_TITLE_MAX_BORROW: "Available for you",
DASHBOARD_TITLE_LOANS: 'Loans', DASHBOARD_TITLE_LOANS: "Loans",
DASHBOARD_TITLE_DEPOSITS: 'Deposits', DASHBOARD_TITLE_DEPOSITS: "Deposits",
DEPOSIT_QUESTION: 'How much would you like to deposit?', DEPOSIT_QUESTION: "How much would you like to deposit?",
WITHDRAW_ACTION: 'Withdraw', WITHDRAW_ACTION: "Withdraw",
WITHDRAW_QUESTION: 'How much would you like to withdraw?', WITHDRAW_QUESTION: "How much would you like to withdraw?",
DASHBOARD_ACTION: 'Go to dashboard', DASHBOARD_ACTION: "Go to dashboard",
GO_BACK_ACTION: 'Go back', GO_BACK_ACTION: "Go back",
DEPOSIT_ACTION: 'Deposit', DEPOSIT_ACTION: "Deposit",
TOTAL_TITLE: 'Total', TOTAL_TITLE: "Total",
TRADING_TABLE_TITLE_MY_COLLATERAL: 'Chosen Collateral', TRADING_TABLE_TITLE_MY_COLLATERAL: 'Chosen Collateral',
TRADING_TABLE_TITLE_DESIRED_ASSET: 'Desired Asset', TRADING_TABLE_TITLE_DESIRED_ASSET: 'Desired Asset',
TRADING_TABLE_TITLE_MULTIPLIER: 'Leverage', TRADING_TABLE_TITLE_MULTIPLIER: 'Leverage',

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

@ -0,0 +1,5 @@
export const GUTTER = [16, { xs: 8, sm: 16, md: 16, lg: 16 }] as any;
export const SMALL_STATISTIC: React.CSSProperties = {
fontSize: 10,
};

View File

@ -55,7 +55,7 @@ export function useBorrowedAmount(address?: string | PublicKey) {
borrowedInUSD: 0, borrowedInUSD: 0,
colateralInUSD: 0, colateralInUSD: 0,
ltv: 0, ltv: 0,
health: 0, health: 0,
}; };
let liquidationThreshold = 0; let liquidationThreshold = 0;
@ -88,7 +88,6 @@ export function useBorrowedAmount(address?: string | PublicKey) {
result.health = Number.isFinite(result.health) ? result.health : 0; result.health = Number.isFinite(result.health) ? result.health : 0;
} }
setBorrowedInfo(result); setBorrowedInfo(result);
})(); })();
}, [connection, userObligationsByReserve, setBorrowedInfo]); }, [connection, userObligationsByReserve, setBorrowedInfo]);

View File

@ -2,29 +2,60 @@ 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";
import { useUserObligations } from "./useUserObligations";
export function useBorrowingPower(reserveAddress: string | PublicKey) { // TODO: add option to decrease buying power by overcollateralization factor
// TODO: add support for balance in the wallet
export function useBorrowingPower(reserveAddress: string | PublicKey, includeWallet = false, 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;
const { totalInQuote: loansValue } = useUserObligations();
const totalDeposits = loansValue + totalInQuote;
const utilization = totalDeposits === 0 ? 0 : loansValue / totalDeposits;
// amounts already expressed as quite mint // amounts already expressed as quite mint
if(liquidityMint.toBase58() === market?.info.quoteMint?.toBase58()) { if (liquidityMintAddress === quoteMintAddess) {
return { return {
borrowingPower: totalInQuote, borrowingPower: totalInQuote,
totalInQuote, totalInQuote,
utilization,
}; };
} }
return { return {
borrowingPower: totalInQuote / price, borrowingPower: totalInQuote / price,
totalInQuote, totalInQuote,
utilization
}; };
} }

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

@ -25,7 +25,7 @@ export const useSliderInput = (
pct, pct,
setPct: useCallback( setPct: useCallback(
(val: number) => { (val: number) => {
setType(InputType.AbsoluteValue); setType(InputType.Percent);
setPct(val); setPct(val);
setValue(convert(val) as string); setValue(convert(val) as string);
}, },

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

@ -1,9 +1,39 @@
import { AccountInfo, PublicKey } from "@solana/web3.js"; import { AccountInfo, PublicKey, TransactionInstruction } from "@solana/web3.js";
import { AccountInfo as TokenAccountInfo } from "@solana/spl-token"; import { AccountInfo as TokenAccountInfo, Token } from "@solana/spl-token";
import { TOKEN_PROGRAM_ID } from "../constants";
export interface TokenAccount { export interface TokenAccount {
pubkey: PublicKey; pubkey: PublicKey;
account: AccountInfo<Buffer>; account: AccountInfo<Buffer>;
info: TokenAccountInfo; info: TokenAccountInfo;
} }
export function approve(
instructions: TransactionInstruction[],
cleanupInstructions: TransactionInstruction[],
account: PublicKey,
delegate: PublicKey,
owner: PublicKey,
amount: number,
): void {
const tokenProgram = TOKEN_PROGRAM_ID;
instructions.push(
Token.createApproveInstruction(
tokenProgram,
account,
delegate,
owner,
[],
amount
)
);
cleanupInstructions.push(
Token.createRevokeInstruction(
tokenProgram,
account,
owner,
[]),
);
}

View File

@ -133,7 +133,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(
@ -149,7 +149,11 @@ export function formatTokenAmount(
return ''; return '';
} }
return `${[prefix]}${format(fromLamports(account, mint, rate), precision, abbr)}${suffix}`; return `${[prefix]}${formatAmount(
fromLamports(account, mint, rate),
precision,
abbr
)}${suffix}`;
} }
export const formatUSD = new Intl.NumberFormat('en-US', { export const formatUSD = new Intl.NumberFormat('en-US', {

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,10 +8,16 @@ 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, LABELS } 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, utilization } = useBorrowingPower(id)
if (!lendingReserve) { if (!lendingReserve) {
return null; return null;
@ -19,17 +25,63 @@ export const BorrowReserveView = () => {
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={LABELS.BORROWED_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={LABELS.BORROWING_POWER_USED}
value={utilization * 100}
precision={2}
suffix="%"
/>
</Card>
</Col>
<Col xs={24} xl={5}>
<Card>
<Statistic
title={LABELS.BORROWING_POWER_VALUE}
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";
@ -44,14 +44,17 @@ export const HomeView = () => {
return; return;
} }
const price = midPriceInUSD(liquidityMint?.pubkey.toBase58());
let leaf = { let leaf = {
key: item.pubkey.toBase58(), key: item.pubkey.toBase58(),
marketSize: fromLamports(marketCapLamports, liquidityMint?.info) * marketSize: fromLamports(marketCapLamports, liquidityMint?.info) *
midPriceInUSD(liquidityMint?.pubkey.toBase58()), price,
borrowed: fromLamports( borrowed: fromLamports(
wadToLamports(item.info?.borrowedLiquidityWad).toNumber(), wadToLamports(item.info?.borrowedLiquidityWad).toNumber(),
liquidityMint.info liquidityMint.info
), ) *
price,
name: getTokenName(tokenMap, item.info.liquidityMint.toBase58()) name: getTokenName(tokenMap, item.info.liquidityMint.toBase58())
} }
@ -83,7 +86,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>
@ -128,16 +131,16 @@ export const HomeView = () => {
</Row> </Row>
<Card> <Card>
<div className="home-item home-header"> <div className="home-item home-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div> <div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_MARKET_SIZE}</div> <div>{LABELS.TABLE_TITLE_MARKET_SIZE}</div>
<div>{LABELS.TABLE_TITLE_TOTAL_BORROWED}</div> <div>{LABELS.TABLE_TITLE_TOTAL_BORROWED}</div>
<div>{LABELS.TABLE_TITLE_DEPOSIT_APY}</div> <div>{LABELS.TABLE_TITLE_DEPOSIT_APY}</div>
<div>{LABELS.TABLE_TITLE_BORROW_APY}</div> <div>{LABELS.TABLE_TITLE_BORROW_APY}</div>
</div> </div>
{reserveAccounts.map((account) => ( {reserveAccounts.map((account) => (
<LendingReserveItem reserve={account.info} address={account.pubkey} item={totals.items.find(item => item.key === account.pubkey.toBase58())} /> <LendingReserveItem reserve={account.info} address={account.pubkey} item={totals.items.find(item => item.key === account.pubkey.toBase58())} />
))} ))}
</Card> </Card>
</div> </div>
); );

View File

@ -1,9 +1,9 @@
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";
import { Card, Col, Row, Statistic } from "antd"; import { Card, Col, Row, Statistic, Typography } from "antd";
import { BarChartStatistic } from "../../components/BarChartStatistic"; import { BarChartStatistic } from "../../components/BarChartStatistic";
export const LiquidateView = () => { export const LiquidateView = () => {
@ -30,9 +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={GUTTER}>
<Col span={24}>
<Card>
<Typography>
{LABELS.LIQUIDATION_INFO}
</Typography>
</Card>
</Col>
</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
@ -73,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;
} }

View File

@ -5,6 +5,8 @@ import "./style.less";
import { UserLendingCard } from "./../../components/UserLendingCard"; import { UserLendingCard } from "./../../components/UserLendingCard";
import { ReserveStatus } from "./../../components/ReserveStatus"; import { ReserveStatus } from "./../../components/ReserveStatus";
import { Col, Row } from "antd";
import { GUTTER } from "../../constants";
export const ReserveView = () => { export const ReserveView = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@ -16,19 +18,21 @@ export const ReserveView = () => {
} }
return ( return (
<div className="reserve-overview"> <div className="flexColumn">
<div className="reserve-overview-container"> <Row gutter={GUTTER}>
<ReserveStatus <Col sm={24} md={12} lg={14} xl={15} xxl={18}>
className="reserve-overview-item reserve-overview-item-left" <ReserveStatus
reserve={reserve} reserve={reserve}
address={lendingReserve.pubkey} address={lendingReserve.pubkey} />
/> </Col>
<UserLendingCard <Col sm={24} md={12} lg={10} xl={9} xxl={6}>
className="reserve-overview-item reserve-overview-item-right" <UserLendingCard
reserve={reserve} className="user-lending-card"
address={lendingReserve.pubkey} reserve={reserve}
/> address={lendingReserve.pubkey}
</div> />
</Col>
</Row>
</div> </div>
); );
}; };

View File

@ -4,27 +4,6 @@
flex: 1; flex: 1;
} }
.reserve-overview-item { .user-lending-card {
margin: 4px; height: 100%;
}
.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%;
}
} }