feat: reserve utilization
This commit is contained in:
parent
f9adc64ca9
commit
49324447ba
|
@ -10,7 +10,7 @@ import {
|
||||||
depositInstruction,
|
depositInstruction,
|
||||||
initReserveInstruction,
|
initReserveInstruction,
|
||||||
LendingReserve,
|
LendingReserve,
|
||||||
} from "./../models/lending/reserve";
|
} from "./../models/lending";
|
||||||
import { AccountLayout, MintInfo, Token } from "@solana/spl-token";
|
import { AccountLayout, MintInfo, Token } from "@solana/spl-token";
|
||||||
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids";
|
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids";
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { notify } from "../utils/notifications";
|
||||||
import {
|
import {
|
||||||
LendingReserve,
|
LendingReserve,
|
||||||
withdrawInstruction,
|
withdrawInstruction,
|
||||||
} from "./../models/lending/reserve";
|
} from "./../models/lending";
|
||||||
import { AccountLayout, Token } from "@solana/spl-token";
|
import { AccountLayout, Token } from "@solana/spl-token";
|
||||||
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids";
|
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids";
|
||||||
import { findOrCreateAccountByMint } from "./account";
|
import { findOrCreateAccountByMint } from "./account";
|
||||||
|
|
|
@ -1,115 +1,10 @@
|
||||||
import React, { useEffect, useMemo, useRef } from "react";
|
import React, { } from "react";
|
||||||
import { LendingReserve } from "../../models/lending";
|
import { LendingReserve } from "../../models/lending";
|
||||||
import { Card } from "antd";
|
import { Card } 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 { LABELS } from "../../constants";
|
||||||
import echarts from "echarts";
|
import { ReserveUtilizationChart } from "./../../components/ReserveUtilizationChart";
|
||||||
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: {
|
export const ReserveStatus = (props: {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
import React, { useEffect, useMemo, useRef } from "react";
|
||||||
|
import { LendingReserve } from "../../models/lending";
|
||||||
|
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 }} />
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import { formatNumber, formatPct, fromLamports } from "../../utils/utils";
|
||||||
import { Card, Typography } from "antd";
|
import { Card, Typography } from "antd";
|
||||||
import { useMint } from "../../contexts/accounts";
|
import { useMint } from "../../contexts/accounts";
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
@ -101,11 +102,13 @@ export const SideReserveOverview = (props: {
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TokenIcon
|
<Link to={`/reserve/${props.address}`}>
|
||||||
mintAddress={reserve?.liquidityMint}
|
<TokenIcon
|
||||||
style={{ width: 30, height: 30 }}
|
mintAddress={reserve?.liquidityMint}
|
||||||
/>{" "}
|
style={{ width: 30, height: 30 }}
|
||||||
{name} Reserve Overview
|
/>{" "}
|
||||||
|
{name} Reserve Overview
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -23,7 +23,9 @@ export const LABELS = {
|
||||||
TABLE_TITLE_ASSET: "Asset",
|
TABLE_TITLE_ASSET: "Asset",
|
||||||
TABLE_TITLE_LOAN_BALANCE: "Your loan balan",
|
TABLE_TITLE_LOAN_BALANCE: "Your loan balan",
|
||||||
TABLE_TITLE_APY: "APY",
|
TABLE_TITLE_APY: "APY",
|
||||||
|
TABLE_TITLE_APR: "APR",
|
||||||
TABLE_TITLE_ACTION: "Action",
|
TABLE_TITLE_ACTION: "Action",
|
||||||
|
TABLE_TITLE_MAX_BORROW: "Available fro you",
|
||||||
DASHBOARD_TITLE_LOANS: "Loans",
|
DASHBOARD_TITLE_LOANS: "Loans",
|
||||||
DASHBOARD_TITLE_DEPOSITS: "Deposts",
|
DASHBOARD_TITLE_DEPOSITS: "Deposts",
|
||||||
WITHDRAW_ACTION: "Withdraw",
|
WITHDRAW_ACTION: "Withdraw",
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from "./accounts";
|
} from "./accounts";
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
import { DexMarketParser } from "../models/dex";
|
import { DexMarketParser } from "../models/dex";
|
||||||
|
import { usePrecacheMarket } from "./market";
|
||||||
|
|
||||||
export interface LendingContextState {}
|
export interface LendingContextState {}
|
||||||
|
|
||||||
|
@ -39,14 +40,19 @@ export function LendingProvider({ children = null as any }) {
|
||||||
export const useLending = () => {
|
export const useLending = () => {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
const [lendingAccounts, setLendingAccounts] = useState<any[]>([]);
|
const [lendingAccounts, setLendingAccounts] = useState<any[]>([]);
|
||||||
|
const precacheMarkets = usePrecacheMarket()
|
||||||
|
|
||||||
|
// TODO: query for all the dex from reserves
|
||||||
|
|
||||||
const processAccount = useCallback((item) => {
|
const processAccount = useCallback((item) => {
|
||||||
if (isLendingReserve(item.account)) {
|
if (isLendingReserve(item.account)) {
|
||||||
return cache.add(
|
const reserve = cache.add(
|
||||||
item.pubkey.toBase58(),
|
item.pubkey.toBase58(),
|
||||||
item.account,
|
item.account,
|
||||||
LendingReserveParser
|
LendingReserveParser
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return reserve;
|
||||||
} else if (isLendingMarket(item.account)) {
|
} else if (isLendingMarket(item.account)) {
|
||||||
return cache.add(
|
return cache.add(
|
||||||
item.pubkey.toBase58(),
|
item.pubkey.toBase58(),
|
||||||
|
@ -71,12 +77,14 @@ export const useLending = () => {
|
||||||
.map(processAccount)
|
.map(processAccount)
|
||||||
.filter((item) => item !== undefined);
|
.filter((item) => item !== undefined);
|
||||||
|
|
||||||
|
const lendingReserves = accounts
|
||||||
|
.filter(
|
||||||
|
(acc) => (acc?.info as LendingReserve).lendingMarket !== undefined
|
||||||
|
)
|
||||||
|
.map((acc) => acc as ParsedAccount<LendingReserve>);
|
||||||
|
|
||||||
const toQuery = [
|
const toQuery = [
|
||||||
...accounts
|
...lendingReserves
|
||||||
.filter(
|
|
||||||
(acc) => (acc?.info as LendingReserve).lendingMarket !== undefined
|
|
||||||
)
|
|
||||||
.map((acc) => acc as ParsedAccount<LendingReserve>)
|
|
||||||
.map((acc) => {
|
.map((acc) => {
|
||||||
const result = [
|
const result = [
|
||||||
cache.registerParser(
|
cache.registerParser(
|
||||||
|
|
|
@ -17,6 +17,8 @@ export interface MarketsContextState {
|
||||||
marketByMint: Map<string, SerumMarket>;
|
marketByMint: Map<string, SerumMarket>;
|
||||||
|
|
||||||
subscribeToMarket: (mint: string) => () => void;
|
subscribeToMarket: (mint: string) => () => void;
|
||||||
|
|
||||||
|
precacheMarkets: (mints: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const REFRESH_INTERVAL = 30_000;
|
const REFRESH_INTERVAL = 30_000;
|
||||||
|
@ -28,17 +30,15 @@ const marketEmitter = new EventEmitter();
|
||||||
export function MarketProvider({ children = null as any }) {
|
export function MarketProvider({ children = null as any }) {
|
||||||
const { endpoint } = useConnectionConfig();
|
const { endpoint } = useConnectionConfig();
|
||||||
const accountsToObserve = useMemo(() => new Map<string, number>(), []);
|
const accountsToObserve = useMemo(() => new Map<string, number>(), []);
|
||||||
|
const [marketMints, setMarketMints] = useState<string[]>();
|
||||||
|
|
||||||
const connection = useMemo(() => new Connection(endpoint, "recent"), [
|
const connection = useMemo(() => new Connection(endpoint, "recent"), [
|
||||||
endpoint,
|
endpoint,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// TODO: identify which markets to query ...
|
|
||||||
const mints = useMemo(() => [] as PublicKey[], []);
|
|
||||||
|
|
||||||
const marketByMint = useMemo(() => {
|
const marketByMint = useMemo(() => {
|
||||||
return [...new Set(mints).values()].reduce((acc, key) => {
|
return [...new Set(marketMints).values()].reduce((acc, key) => {
|
||||||
const mintAddress = key.toBase58();
|
const mintAddress = key;
|
||||||
|
|
||||||
const SERUM_TOKEN = TOKEN_MINTS.find(
|
const SERUM_TOKEN = TOKEN_MINTS.find(
|
||||||
(a) => a.address.toBase58() === mintAddress
|
(a) => a.address.toBase58() === mintAddress
|
||||||
|
@ -58,7 +58,7 @@ export function MarketProvider({ children = null as any }) {
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, new Map<string, SerumMarket>()) as Map<string, SerumMarket>;
|
}, new Map<string, SerumMarket>()) as Map<string, SerumMarket>;
|
||||||
}, [mints]);
|
}, [marketMints]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer = 0;
|
let timer = 0;
|
||||||
|
@ -185,6 +185,10 @@ export function MarketProvider({ children = null as any }) {
|
||||||
[marketByMint, accountsToObserve]
|
[marketByMint, accountsToObserve]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const precacheMarkets = useCallback((mints: string[]) => {
|
||||||
|
setMarketMints([...new Set([...marketMints, ...mints]).values()])
|
||||||
|
}, [setMarketMints, marketMints]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MarketsContext.Provider
|
<MarketsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -193,6 +197,7 @@ export function MarketProvider({ children = null as any }) {
|
||||||
accountsToObserve,
|
accountsToObserve,
|
||||||
marketByMint,
|
marketByMint,
|
||||||
subscribeToMarket,
|
subscribeToMarket,
|
||||||
|
precacheMarkets,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -231,6 +236,11 @@ export const useMidPriceInUSD = (mint: string) => {
|
||||||
return { price, isBase: price === 1.0 };
|
return { price, isBase: price === 1.0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const usePrecacheMarket = () => {
|
||||||
|
const context = useMarkets();
|
||||||
|
return context.precacheMarkets;
|
||||||
|
}
|
||||||
|
|
||||||
const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
|
const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
|
||||||
const SERUM_TOKEN = TOKEN_MINTS.find(
|
const SERUM_TOKEN = TOKEN_MINTS.find(
|
||||||
(a) => a.address.toBase58() === mintAddress
|
(a) => a.address.toBase58() === mintAddress
|
||||||
|
|
|
@ -7,8 +7,10 @@ import {
|
||||||
import BN from "bn.js";
|
import BN from "bn.js";
|
||||||
import * as BufferLayout from "buffer-layout";
|
import * as BufferLayout from "buffer-layout";
|
||||||
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
|
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
|
||||||
|
import { WAD } from "../../constants/math";
|
||||||
import * as Layout from "./../../utils/layout";
|
import * as Layout from "./../../utils/layout";
|
||||||
import { LendingInstruction } from "./lending";
|
import { LendingInstruction } from "./lending";
|
||||||
|
import { LendingReserve } from "./reserve";
|
||||||
|
|
||||||
/// Borrow tokens from a reserve by depositing collateral tokens. The number of borrowed tokens
|
/// Borrow tokens from a reserve by depositing collateral tokens. The number of borrowed tokens
|
||||||
/// is calculated by market price. The debt obligation is tokenized.
|
/// is calculated by market price. The debt obligation is tokenized.
|
||||||
|
@ -98,3 +100,30 @@ export const borrowInstruction = (
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// deposit APY utilization currentUtilizationRate * borrowAPY
|
||||||
|
|
||||||
|
export const calculateBorrowAPY = (reserve: LendingReserve) => {
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return borrowAPY;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import {
|
||||||
|
PublicKey,
|
||||||
|
SYSVAR_CLOCK_PUBKEY,
|
||||||
|
TransactionInstruction,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import BN from "bn.js";
|
||||||
|
import * as BufferLayout from "buffer-layout";
|
||||||
|
import { WAD } from "../../constants";
|
||||||
|
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
|
||||||
|
import * as Layout from "./../../utils/layout";
|
||||||
|
import { calculateBorrowAPY } from "./borrow";
|
||||||
|
import { LendingInstruction } from "./lending";
|
||||||
|
import { LendingReserve } from "./reserve";
|
||||||
|
|
||||||
|
/// Deposit liquidity into a reserve. The output is a collateral token representing ownership
|
||||||
|
/// of the reserve liquidity pool.
|
||||||
|
///
|
||||||
|
/// 0. `[writable]` Liquidity input SPL Token account. $authority can transfer $liquidity_amount
|
||||||
|
/// 1. `[writable]` Collateral output SPL Token account,
|
||||||
|
/// 2. `[writable]` Reserve account.
|
||||||
|
/// 3. `[writable]` Reserve liquidity supply SPL Token account.
|
||||||
|
/// 4. `[writable]` Reserve collateral SPL Token mint.
|
||||||
|
/// 5. `[]` Derived lending market authority ($authority).
|
||||||
|
/// 6. `[]` Clock sysvar
|
||||||
|
/// 7. '[]` Token program id
|
||||||
|
export const depositInstruction = (
|
||||||
|
liquidityAmount: number | BN,
|
||||||
|
from: PublicKey, // Liquidity input SPL Token account. $authority can transfer $liquidity_amount
|
||||||
|
to: PublicKey, // Collateral output SPL Token account,
|
||||||
|
reserveAuthority: PublicKey,
|
||||||
|
reserveAccount: PublicKey,
|
||||||
|
reserveSupply: PublicKey,
|
||||||
|
collateralMint: PublicKey
|
||||||
|
): TransactionInstruction => {
|
||||||
|
const dataLayout = BufferLayout.struct([
|
||||||
|
BufferLayout.u8("instruction"),
|
||||||
|
Layout.uint64("liquidityAmount"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const data = Buffer.alloc(dataLayout.span);
|
||||||
|
dataLayout.encode(
|
||||||
|
{
|
||||||
|
instruction: LendingInstruction.DepositReserveLiquidity,
|
||||||
|
liquidityAmount: new BN(liquidityAmount),
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
const keys = [
|
||||||
|
{ pubkey: from, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: to, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: reserveAccount, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: reserveSupply, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: collateralMint, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: reserveAuthority, isSigner: false, isWritable: false },
|
||||||
|
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
|
||||||
|
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
||||||
|
];
|
||||||
|
return new TransactionInstruction({
|
||||||
|
keys,
|
||||||
|
programId: LENDING_PROGRAM_ID,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateDepositAPY = (reserve: LendingReserve) => {
|
||||||
|
const totalBorrows = reserve.totalBorrowsWad.div(WAD).toNumber();
|
||||||
|
const currentUtilization =
|
||||||
|
totalBorrows / (reserve.totalLiquidity.toNumber() + totalBorrows);
|
||||||
|
|
||||||
|
const borrowAPY = calculateBorrowAPY(reserve);
|
||||||
|
return currentUtilization * borrowAPY;
|
||||||
|
};
|
|
@ -3,3 +3,5 @@ export * from "./reserve";
|
||||||
export * from "./obligation";
|
export * from "./obligation";
|
||||||
export * from "./lending";
|
export * from "./lending";
|
||||||
export * from "./borrow";
|
export * from "./borrow";
|
||||||
|
export * from "./deposit";
|
||||||
|
export * from "./withdraw";
|
|
@ -7,7 +7,6 @@ import {
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import BN from "bn.js";
|
import BN from "bn.js";
|
||||||
import * as BufferLayout from "buffer-layout";
|
import * as BufferLayout from "buffer-layout";
|
||||||
import { WAD } from "../../constants";
|
|
||||||
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
|
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
|
||||||
import * as Layout from "./../../utils/layout";
|
import * as Layout from "./../../utils/layout";
|
||||||
import { LendingInstruction } from "./lending";
|
import { LendingInstruction } from "./lending";
|
||||||
|
@ -169,119 +168,3 @@ export const initReserveInstruction = (
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Deposit liquidity into a reserve. The output is a collateral token representing ownership
|
|
||||||
/// of the reserve liquidity pool.
|
|
||||||
///
|
|
||||||
/// 0. `[writable]` Liquidity input SPL Token account. $authority can transfer $liquidity_amount
|
|
||||||
/// 1. `[writable]` Collateral output SPL Token account,
|
|
||||||
/// 2. `[writable]` Reserve account.
|
|
||||||
/// 3. `[writable]` Reserve liquidity supply SPL Token account.
|
|
||||||
/// 4. `[writable]` Reserve collateral SPL Token mint.
|
|
||||||
/// 5. `[]` Derived lending market authority ($authority).
|
|
||||||
/// 6. `[]` Clock sysvar
|
|
||||||
/// 7. '[]` Token program id
|
|
||||||
export const depositInstruction = (
|
|
||||||
liquidityAmount: number | BN,
|
|
||||||
from: PublicKey, // Liquidity input SPL Token account. $authority can transfer $liquidity_amount
|
|
||||||
to: PublicKey, // Collateral output SPL Token account,
|
|
||||||
reserveAuthority: PublicKey,
|
|
||||||
reserveAccount: PublicKey,
|
|
||||||
reserveSupply: PublicKey,
|
|
||||||
collateralMint: PublicKey
|
|
||||||
): TransactionInstruction => {
|
|
||||||
const dataLayout = BufferLayout.struct([
|
|
||||||
BufferLayout.u8("instruction"),
|
|
||||||
Layout.uint64("liquidityAmount"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const data = Buffer.alloc(dataLayout.span);
|
|
||||||
dataLayout.encode(
|
|
||||||
{
|
|
||||||
instruction: LendingInstruction.DepositReserveLiquidity,
|
|
||||||
liquidityAmount: new BN(liquidityAmount),
|
|
||||||
},
|
|
||||||
data
|
|
||||||
);
|
|
||||||
|
|
||||||
const keys = [
|
|
||||||
{ pubkey: from, isSigner: false, isWritable: true },
|
|
||||||
{ pubkey: to, isSigner: false, isWritable: true },
|
|
||||||
{ pubkey: reserveAccount, isSigner: false, isWritable: true },
|
|
||||||
{ pubkey: reserveSupply, isSigner: false, isWritable: true },
|
|
||||||
{ pubkey: collateralMint, isSigner: false, isWritable: true },
|
|
||||||
{ pubkey: reserveAuthority, isSigner: false, isWritable: false },
|
|
||||||
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
|
|
||||||
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
||||||
];
|
|
||||||
return new TransactionInstruction({
|
|
||||||
keys,
|
|
||||||
programId: LENDING_PROGRAM_ID,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const withdrawInstruction = (
|
|
||||||
collateralAmount: number | BN,
|
|
||||||
from: PublicKey, // Collateral input SPL Token account. $authority can transfer $liquidity_amount
|
|
||||||
to: PublicKey, // Liquidity output SPL Token account,
|
|
||||||
reserveAccount: PublicKey,
|
|
||||||
collateralMint: PublicKey,
|
|
||||||
reserveSupply: PublicKey,
|
|
||||||
authority: PublicKey
|
|
||||||
): TransactionInstruction => {
|
|
||||||
const dataLayout = BufferLayout.struct([
|
|
||||||
BufferLayout.u8("instruction"),
|
|
||||||
Layout.uint64("collateralAmount"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const data = Buffer.alloc(dataLayout.span);
|
|
||||||
dataLayout.encode(
|
|
||||||
{
|
|
||||||
instruction: LendingInstruction.WithdrawReserveLiquidity,
|
|
||||||
collateralAmount: new BN(collateralAmount),
|
|
||||||
},
|
|
||||||
data
|
|
||||||
);
|
|
||||||
|
|
||||||
const keys = [
|
|
||||||
{ pubkey: from, isSigner: false, isWritable: true },
|
|
||||||
{ pubkey: to, isSigner: false, isWritable: true },
|
|
||||||
{ pubkey: reserveAccount, isSigner: false, isWritable: true },
|
|
||||||
{ pubkey: collateralMint, isSigner: false, isWritable: true },
|
|
||||||
{ pubkey: reserveSupply, isSigner: false, isWritable: true },
|
|
||||||
{ pubkey: authority, isSigner: false, isWritable: false },
|
|
||||||
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
|
|
||||||
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
||||||
];
|
|
||||||
return new TransactionInstruction({
|
|
||||||
keys,
|
|
||||||
programId: LENDING_PROGRAM_ID,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const calculateBorrowAPY = (reserve: LendingReserve) => {
|
|
||||||
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;
|
|
||||||
} else {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return borrowAPY;
|
|
||||||
};
|
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import {
|
||||||
|
PublicKey,
|
||||||
|
SYSVAR_CLOCK_PUBKEY,
|
||||||
|
TransactionInstruction,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import BN from "bn.js";
|
||||||
|
import * as BufferLayout from "buffer-layout";
|
||||||
|
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
|
||||||
|
import * as Layout from "./../../utils/layout";
|
||||||
|
import { LendingInstruction } from "./lending";
|
||||||
|
|
||||||
|
export const withdrawInstruction = (
|
||||||
|
collateralAmount: number | BN,
|
||||||
|
from: PublicKey, // Collateral input SPL Token account. $authority can transfer $liquidity_amount
|
||||||
|
to: PublicKey, // Liquidity output SPL Token account,
|
||||||
|
reserveAccount: PublicKey,
|
||||||
|
collateralMint: PublicKey,
|
||||||
|
reserveSupply: PublicKey,
|
||||||
|
authority: PublicKey
|
||||||
|
): TransactionInstruction => {
|
||||||
|
const dataLayout = BufferLayout.struct([
|
||||||
|
BufferLayout.u8("instruction"),
|
||||||
|
Layout.uint64("collateralAmount"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const data = Buffer.alloc(dataLayout.span);
|
||||||
|
dataLayout.encode(
|
||||||
|
{
|
||||||
|
instruction: LendingInstruction.WithdrawReserveLiquidity,
|
||||||
|
collateralAmount: new BN(collateralAmount),
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
const keys = [
|
||||||
|
{ pubkey: from, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: to, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: reserveAccount, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: collateralMint, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: reserveSupply, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: authority, isSigner: false, isWritable: false },
|
||||||
|
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
|
||||||
|
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
||||||
|
];
|
||||||
|
return new TransactionInstruction({
|
||||||
|
keys,
|
||||||
|
programId: LENDING_PROGRAM_ID,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { LABELS } from "../../constants";
|
||||||
import { useLendingReserves } from "../../hooks";
|
import { useLendingReserves } from "../../hooks";
|
||||||
import { BorrowItem } from "./item";
|
import { BorrowItem } from "./item";
|
||||||
import "./itemStyle.less";
|
import "./itemStyle.less";
|
||||||
|
@ -8,10 +9,10 @@ export const BorrowView = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flexColumn">
|
<div className="flexColumn">
|
||||||
<div className="borrow-item deposit-header">
|
<div className="borrow-item deposit-header">
|
||||||
<div>Asset</div>
|
<div>{LABELS.TABLE_TITLE_ASSET}</div>
|
||||||
<div>Available fro you</div>
|
<div>{LABELS.TABLE_TITLE_MAX_BORROW}</div>
|
||||||
<div>APY</div>
|
<div>{LABELS.TABLE_TITLE_APR}</div>
|
||||||
<div>Action</div>
|
<div>{LABELS.TABLE_TITLE_ACTION}</div>
|
||||||
</div>
|
</div>
|
||||||
{reserveAccounts.map((account) => (
|
{reserveAccounts.map((account) => (
|
||||||
<BorrowItem reserve={account.info} address={account.pubkey} />
|
<BorrowItem reserve={account.info} address={account.pubkey} />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useTokenName } from "../../hooks";
|
import { useTokenName } from "../../hooks";
|
||||||
import { calculateBorrowAPY, LendingReserve } from "../../models/lending";
|
import { calculateBorrowAPY, calculateDepositAPY, LendingReserve } from "../../models/lending";
|
||||||
import { TokenIcon } from "../../components/TokenIcon";
|
import { TokenIcon } from "../../components/TokenIcon";
|
||||||
import {
|
import {
|
||||||
wadToLamports,
|
wadToLamports,
|
||||||
|
@ -36,6 +36,12 @@ export const LendingReserveItem = (props: {
|
||||||
props.reserve,
|
props.reserve,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const depositAPY = useMemo(() => calculateDepositAPY(props.reserve), [
|
||||||
|
props.reserve,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const marketSize = totalLiquidity+totalBorrows;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/reserve/${props.address.toBase58()}`}>
|
<Link to={`/reserve/${props.address.toBase58()}`}>
|
||||||
<Card>
|
<Card>
|
||||||
|
@ -44,14 +50,18 @@ export const LendingReserveItem = (props: {
|
||||||
<TokenIcon mintAddress={props.reserve.liquidityMint} />
|
<TokenIcon mintAddress={props.reserve.liquidityMint} />
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div title={marketSize.toString()}>
|
||||||
{formatNumber.format(totalLiquidity+totalBorrows)} {name}
|
{formatNumber.format(marketSize)} {name}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div title={totalBorrows.toString()}>
|
||||||
{formatNumber.format(totalBorrows)} {name}
|
{formatNumber.format(totalBorrows)} {name}
|
||||||
</div>
|
</div>
|
||||||
<div>--</div>
|
<div title={depositAPY.toString()}>
|
||||||
<div>{formatPct.format(borrowAPY)}</div>
|
{formatPct.format(depositAPY)}
|
||||||
|
</div>
|
||||||
|
<div title={borrowAPY.toString()}>
|
||||||
|
{formatPct.format(borrowAPY)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
Loading…
Reference in New Issue