feat: add liquidity chart

This commit is contained in:
bartosz-lipinski 2020-11-24 21:14:28 -06:00
parent 31020e7e86
commit f9adc64ca9
20 changed files with 258 additions and 112 deletions

View File

@ -20,4 +20,5 @@ Any content produced by Solana, or developer resources that Solana provides, are
- [] Add market size on front page
- [] Add github link
- [] Repay from reserve (add selection for obligation/loan)
- []
- [] Add support for token names in URL in addition to reserve address

View File

@ -76,8 +76,8 @@ export const repay = async (
signers
);
const loanRatio = amountLamports / wadToLamports(obligation.info.borrowAmountWad)
.toNumber();
const loanRatio =
amountLamports / wadToLamports(obligation.info.borrowAmountWad).toNumber();
console.log(loanRatio);
// create approval for transfer transactions

View File

@ -84,9 +84,7 @@ export const BorrowInput = (props: {
justifyContent: "space-around",
}}
>
<div className="borrow-input-title">
{LABELS.BORROW_QUESTION}
</div>
<div className="borrow-input-title">{LABELS.BORROW_QUESTION}</div>
<div className="token-input">
<TokenIcon mintAddress={borrowReserve?.liquidityMint} />
<NumericInput

View File

@ -1,13 +1,17 @@
import { Button } from "antd"
import { ButtonProps } from "antd/lib/button"
import React from "react"
import { Button } from "antd";
import { ButtonProps } from "antd/lib/button";
import React from "react";
import { useWallet } from "../../contexts/wallet";
import { LABELS } from './../../constants';
import { LABELS } from "./../../constants";
export const ConnectButton = (props: ButtonProps & React.RefAttributes<HTMLElement>) => {
export const ConnectButton = (
props: ButtonProps & React.RefAttributes<HTMLElement>
) => {
const { wallet, connected } = useWallet();
const { onClick, children, ...rest } = props;
return <Button {...rest} onClick={connected ? onClick : wallet.connect} >
{connected ? props.children : LABELS.CONNECT_LABEL}
</Button>
}
return (
<Button {...rest} onClick={connected ? onClick : wallet.connect}>
{connected ? props.children : LABELS.CONNECT_LABEL}
</Button>
);
};

View File

@ -36,9 +36,7 @@ export const AppLayout = (props: any) => {
return (
<div className="App">
<div className="Banner">
<div className="Banner-description">
{LABELS.AUDIT_WARNING}
</div>
<div className="Banner-description">{LABELS.AUDIT_WARNING}</div>
</div>
<BasicLayout
title={LABELS.APP_TITLE}

View File

@ -14,7 +14,12 @@ import { useWallet } from "../../contexts/wallet";
import { repay } from "../../actions";
import { CollateralSelector } from "./../CollateralSelector";
import "./style.less";
import { wadToLamports, formatNumber, fromLamports, toLamports } from "../../utils/utils";
import {
wadToLamports,
formatNumber,
fromLamports,
toLamports,
} from "../../utils/utils";
import { LABELS } from "../../constants";
export const RepayInput = (props: {
@ -50,9 +55,14 @@ export const RepayInput = (props: {
const obligationAccount = useAccountByMint(obligation?.info.tokenMint);
const lamports = useMemo(() => toLamports(parseFloat(value), repayLiquidityMint), [value, repayLiquidityMint]);
const lamports = useMemo(
() => toLamports(parseFloat(value), repayLiquidityMint),
[value, repayLiquidityMint]
);
const mark = wadToLamports(obligation?.info.borrowAmountWad).toNumber() / lamports * 100;
const mark =
(wadToLamports(obligation?.info.borrowAmountWad).toNumber() / lamports) *
100;
const onRepay = useCallback(() => {
if (
@ -103,8 +113,8 @@ export const RepayInput = (props: {
}}
>
<div className="repay-input-title">
{LABELS.REPAY_QUESTION} (Currently:{" "})
{formatNumber.format(balance)} {name})
{LABELS.REPAY_QUESTION} (Currently: ){formatNumber.format(balance)}{" "}
{name})
</div>
<div className="token-input">
<TokenIcon mintAddress={repayReserve?.info.liquidityMint} />
@ -124,10 +134,22 @@ export const RepayInput = (props: {
/>
<div>{name}</div>
</div>
<Slider marks={marks}
value={mark}
onChange={(val: number) =>
setValue((fromLamports(wadToLamports(obligation?.info.borrowAmountWad).toNumber(), repayLiquidityMint) * val / 100).toFixed(2))} />
<Slider
marks={marks}
value={mark}
onChange={(val: number) =>
setValue(
(
(fromLamports(
wadToLamports(obligation?.info.borrowAmountWad).toNumber(),
repayLiquidityMint
) *
val) /
100
).toFixed(2)
)
}
/>
<div className="repay-input-title">{LABELS.SELECT_COLLATERAL}</div>
<CollateralSelector
reserve={repayReserve.info}
@ -148,9 +170,9 @@ export const RepayInput = (props: {
};
const marks = {
0: '0%',
25: '25%',
50: '50%',
75: '75%',
100: '100%'
0: "0%",
25: "25%",
50: "50%",
75: "75%",
100: "100%",
};

View File

@ -1,9 +1,115 @@
import React from "react";
import React, { useEffect, useMemo, useRef } from "react";
import { LendingReserve } from "../../models/lending";
import { Card } from "antd";
import { PublicKey } from "@solana/web3.js";
import "./style.less";
import { LABELS } from "../../constants";
import echarts from "echarts";
import { formatNumber, formatUSD, fromLamports, wadToLamports } from "../../utils/utils";
import { useMint } from "../../contexts/accounts";
export const ReserveUtilizationChart = (props: {
reserve: LendingReserve;
}) => {
const chartDiv = useRef<HTMLDivElement>(null);
// dispose chart
useEffect(() => {
const div = chartDiv.current;
return () => {
let instance = div && echarts.getInstanceByDom(div);
instance && instance.dispose();
};
}, []);
const liquidityMint = useMint(props.reserve.liquidityMint);
const avilableLiquidity = fromLamports(
props.reserve.totalLiquidity.toNumber(),
liquidityMint
);
const totalBorrows = useMemo(
() =>
fromLamports(wadToLamports(props.reserve.totalBorrowsWad), liquidityMint),
[props.reserve, liquidityMint]
);
useEffect(() => {
if (!chartDiv.current) {
return;
}
let instance = echarts.getInstanceByDom(chartDiv.current);
if (!instance) {
instance = echarts.init(chartDiv.current as any);
}
const data = [
{
name: 'Available Liquidity',
value: avilableLiquidity,
tokens: avilableLiquidity,
},
{
name: 'Total Borrowed',
value: totalBorrows,
tokens: totalBorrows,
},
];
instance.setOption({
tooltip: {
trigger: "item",
formatter: function (params: any) {
var val = formatUSD.format(params.value);
var tokenAmount = formatNumber.format(params.data.tokens);
return `${params.name}: \n${val}\n(${tokenAmount})`;
},
},
series: [
{
name: "Liquidity",
type: "pie",
radius: ['50%', '70%'],
top: 0,
bottom: 0,
left: 0,
right: 0,
animation: false,
label: {
fontSize: 14,
show: true,
formatter: function (params: any) {
var val = formatUSD.format(params.value);
var tokenAmount = formatNumber.format(params.data.tokens);
return `{c|${params.name}}\n{r|${tokenAmount}}\n{r|${val}}`;
},
rich: {
c: {
color: "#999",
lineHeight: 22,
align: "center",
},
r: {
color: "#999",
align: "right",
},
},
color: "rgba(255, 255, 255, 0.5)",
},
itemStyle: {
normal: {
borderColor: "#000",
},
},
data,
},
],
});
}, [totalBorrows, avilableLiquidity]);
return <div ref={chartDiv} style={{ height: 300, width: 400 }} />
}
export const ReserveStatus = (props: {
className?: string;
@ -29,7 +135,7 @@ export const ReserveStatus = (props: {
justifyContent: "space-around",
}}
>
TODO: Reserve Status - add chart
<ReserveUtilizationChart reserve={props.reserve} />
</div>
</Card>
);

View File

@ -6,12 +6,10 @@ import {
useBorrowedAmount,
} from "./../../hooks";
import { LendingReserve } from "../../models/lending";
import { formatNumber, wadToLamports } from "../../utils/utils";
import { formatNumber } from "../../utils/utils";
import { Button, Card, Typography } from "antd";
import { Link } from "react-router-dom";
import { PublicKey } from "@solana/web3.js";
import { cache, ParsedAccount } from "../../contexts/accounts";
import { MintInfo } from "@solana/spl-token";
const { Text } = Typography;

View File

@ -76,9 +76,7 @@ export const WithdrawInput = (props: {
justifyContent: "space-around",
}}
>
<div className="withdraw-input-title">
{LABELS.WITHDRAW_QUESTION}
</div>
<div className="withdraw-input-title">{LABELS.WITHDRAW_QUESTION}</div>
<div className="token-input">
<TokenIcon mintAddress={reserve?.liquidityMint} />
<NumericInput

View File

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

View File

@ -1,11 +1,12 @@
export const LABELS = {
CONNECT_LABEL: "Connect Wallet",
GIVE_SOL: "Give me SOL",
FAUCET_INFO: "This faucet will help you fund your accounts outside of Solana main network.",
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 &amp; Configuration",
RESERVE_STATUS_TITLE: "Reserve Status & Configuration",
AUDIT_WARNING: "Oyster Lending is unaudited software. Use at your own risk.",
MENU_HOME: "Home",
MENU_DASHBOARD: "Dashboard",
@ -27,4 +28,4 @@ export const LABELS = {
DASHBOARD_TITLE_DEPOSITS: "Deposts",
WITHDRAW_ACTION: "Withdraw",
WITHDRAW_QUESTION: "How much would you like to withdraw?",
}
};

View File

@ -3,4 +3,4 @@ import BN from "bn.js";
export const TEN = new BN(10);
export const WAD = TEN.pow(new BN(18));
export const RAY = TEN.pow(new BN(27));
export const ZERO = new BN(0);
export const ZERO = new BN(0);

View File

@ -1,11 +1,14 @@
import { useEffect, useMemo, useState } from "react";
import { useUserAccounts } from "./useUserAccounts";
import { useLendingObligations } from "./useLendingObligations";
import { TokenAccount } from "../models";
import { useEffect, useState } from "react";
import { PublicKey } from "@solana/web3.js";
import { useUserObligationByReserve } from "./useUserObligationByReserve";
import { fromLamports, wadToLamports } from "../utils/utils";
import { cache, getMultipleAccounts, MintParser, ParsedAccount, useMint } from "../contexts/accounts";
import {
cache,
getMultipleAccounts,
MintParser,
ParsedAccount,
useMint,
} from "../contexts/accounts";
import { useConnection } from "../contexts/connection";
import { MintInfo } from "@solana/spl-token";
import { useLendingReserve } from "./useLendingReserves";
@ -24,30 +27,40 @@ export function useBorrowedAmount(address?: string | PublicKey) {
// precache obligation mints
const { keys, array } = await getMultipleAccounts(
connection,
userObligationsByReserve
.map(item => item.obligation.info.tokenMint.toBase58()),
"single");
userObligationsByReserve.map((item) =>
item.obligation.info.tokenMint.toBase58()
),
"single"
);
array.forEach((item, index) => {
const address = keys[index];
cache.add(new PublicKey(address), item, MintParser);
});
setBorrowedLamports(userObligationsByReserve.reduce((result, item) => {
setBorrowedLamports(
userObligationsByReserve.reduce((result, item) => {
const borrowed = wadToLamports(
item.obligation.info.borrowAmountWad
).toNumber();
const borrowed = wadToLamports(item.obligation.info.borrowAmountWad).toNumber();
const owned = item.userAccounts.reduce(
(amount, acc) => (amount += acc.info.amount.toNumber()),
0
);
const obligationMint = cache.get(
item.obligation.info.tokenMint
) as ParsedAccount<MintInfo>;
const owned = item.userAccounts.reduce((amount, acc) => amount += acc.info.amount.toNumber(), 0);
const obligationMint = cache.get(item.obligation.info.tokenMint) as ParsedAccount<MintInfo>;
result += borrowed * owned / obligationMint?.info.supply.toNumber();
return result
}, 0));
result += (borrowed * owned) / obligationMint?.info.supply.toNumber();
return result;
}, 0)
);
})();
}, [connection, userObligationsByReserve]);
}, [userObligationsByReserve]);
return { borrowed: fromLamports(borrowedLamports, liquidityMint), borrowedLamports };
return {
borrowed: fromLamports(borrowedLamports, liquidityMint),
borrowedLamports,
};
}

View File

@ -41,7 +41,7 @@ export function useLendingReserve(address?: string | PublicKey) {
>();
useEffect(() => {
setReserveAccount(cache.get(id || ''));
setReserveAccount(cache.get(id || ""));
const dispose = cache.emitter.onCache((args) => {
if (args.id === id) {

View File

@ -5,14 +5,12 @@ import { PublicKey } from "@solana/web3.js";
export function useUserObligationByReserve(reserve?: string | PublicKey) {
const { userObligations } = useUserObligations();
const userObligationsByReserve = useMemo(
() => {
const id = typeof reserve === 'string' ? reserve : reserve?.toBase58();
return userObligations.filter((item) =>
item.obligation.info.borrowReserve.toBase58() === id
)
}, [reserve, userObligations]
);
const userObligationsByReserve = useMemo(() => {
const id = typeof reserve === "string" ? reserve : reserve?.toBase58();
return userObligations.filter(
(item) => item.obligation.info.borrowReserve.toBase58() === id
);
}, [reserve, userObligations]);
return {
userObligationsByReserve,

View File

@ -262,21 +262,26 @@ export const withdrawInstruction = (
};
export const calculateBorrowAPY = (reserve: LendingReserve) => {
const totalBorrows = reserve.totalBorrowsWad.div(WAD).toNumber()
const currentUtilization = totalBorrows / (reserve.totalLiquidity.toNumber() + totalBorrows)
const optimalUtilization = reserve.config.optimalUtilizationRate
const totalBorrows = reserve.totalBorrowsWad.div(WAD).toNumber();
const currentUtilization =
totalBorrows / (reserve.totalLiquidity.toNumber() + totalBorrows);
const optimalUtilization = reserve.config.optimalUtilizationRate;
let borrowAPY;
if (currentUtilization < optimalUtilization) {
const normalized_factor = currentUtilization / optimalUtilization;
const optimalBorrowRate = reserve.config.optimalBorrowRate / 100;
const minBorrowRate = reserve.config.minBorrowRate / 100;
borrowAPY = normalized_factor * (optimalBorrowRate - minBorrowRate) + minBorrowRate;
borrowAPY =
normalized_factor * (optimalBorrowRate - minBorrowRate) + minBorrowRate;
} else {
const normalized_factor = (currentUtilization - optimalUtilization) / (100 - optimalUtilization);
const normalized_factor =
(currentUtilization - optimalUtilization) / (100 - optimalUtilization);
const optimalBorrowRate = reserve.config.optimalBorrowRate / 100;
const maxBorrowRate = reserve.config.maxBorrowRate / 100;
borrowAPY = normalized_factor * (maxBorrowRate - optimalBorrowRate) + optimalBorrowRate;
borrowAPY =
normalized_factor * (maxBorrowRate - optimalBorrowRate) +
optimalBorrowRate;
}
return borrowAPY;
}
};

View File

@ -2,7 +2,7 @@ import React from "react";
import { LABELS } from "../../constants";
import { useUserObligations } from "./../../hooks";
import { ObligationItem } from "./obligationItem";
import "./style.less"
import "./style.less";
export const DashboardView = () => {
const { userObligations } = useUserObligations();
@ -14,12 +14,14 @@ export const DashboardView = () => {
</div>
<div>
<span>{LABELS.DASHBOARD_TITLE_LOANS}</span>
{userObligations.length > 0 && <div className="dashboard-item dashboard-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_LOAN_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_APY}</div>
<div>{LABELS.TABLE_TITLE_ACTION}</div>
</div>}
{userObligations.length > 0 && (
<div className="dashboard-item dashboard-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_LOAN_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_APY}</div>
<div>{LABELS.TABLE_TITLE_ACTION}</div>
</div>
)}
{userObligations.map((item) => {
return <ObligationItem obligation={item.obligation} />;
})}

View File

@ -2,11 +2,7 @@ import React from "react";
import { useTokenName } from "../../hooks";
import { LendingObligation, LendingReserve } from "../../models/lending";
import { TokenIcon } from "../../components/TokenIcon";
import {
wadToLamports,
formatNumber,
fromLamports,
} from "../../utils/utils";
import { wadToLamports, formatNumber, fromLamports } from "../../utils/utils";
import { Button, Card } from "antd";
import { Link } from "react-router-dom";
import { cache, ParsedAccount, useMint } from "../../contexts/accounts";
@ -30,9 +26,7 @@ export const ObligationItem = (props: {
);
return (
<Link
to={`/repay/loan/${obligation.pubkey.toBase58()}`}
>
<Link to={`/repay/loan/${obligation.pubkey.toBase58()}`}>
<Card>
<div className="dashboard-item">
<span style={{ display: "flex" }}>

View File

@ -1,13 +1,17 @@
import React from "react";
import React, { useMemo } from "react";
import { useTokenName } from "../../hooks";
import { calculateBorrowAPY, LendingReserve } from "../../models/lending";
import { TokenIcon } from "../../components/TokenIcon";
import { wadToLamports, formatNumber, fromLamports } from "../../utils/utils";
import {
wadToLamports,
formatNumber,
fromLamports,
formatPct,
} from "../../utils/utils";
import { Card } from "antd";
import { Link } from "react-router-dom";
import { PublicKey } from "@solana/web3.js";
import { useMint } from "../../contexts/accounts";
import { WAD } from "../../constants";
export const LendingReserveItem = (props: {
reserve: LendingReserve;
@ -22,10 +26,15 @@ export const LendingReserveItem = (props: {
liquidityMint
);
console.log(props.reserve.totalBorrowsWad.toString());
const totalBorrows = fromLamports(wadToLamports(props.reserve.totalBorrowsWad), liquidityMint);
const totalBorrows = useMemo(
() =>
fromLamports(wadToLamports(props.reserve.totalBorrowsWad), liquidityMint),
[props.reserve, liquidityMint]
);
console.log(liquidityMint);
const borrowAPY = useMemo(() => calculateBorrowAPY(props.reserve), [
props.reserve,
]);
return (
<Link to={`/reserve/${props.address.toBase58()}`}>
@ -36,18 +45,15 @@ export const LendingReserveItem = (props: {
{name}
</span>
<div>
{formatNumber.format(totalLiquidity)} {name}
{formatNumber.format(totalLiquidity+totalBorrows)} {name}
</div>
<div>
{formatNumber.format(totalBorrows)} {name}
</div>
<div>--</div>
<div>{calculateBorrowAPY(props.reserve)}</div>
<div>{formatPct.format(borrowAPY)}</div>
</div>
</Card>
</Link>
);
};

View File

@ -15,9 +15,11 @@ export const RepayReserveView = () => {
reserve?: string;
obligation?: string;
}>();
const lendingObligation = useLendingObligation(obligationId);
const lendingReserve = useLendingReserve(obligationId ? lendingObligation?.info.borrowReserve : reserveId);
const lendingReserve = useLendingReserve(
obligationId ? lendingObligation?.info.borrowReserve : reserveId
);
const reserve = lendingReserve?.info;
console.log([reserveId, obligationId]);