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