feat: add LTV, and health factor calculations
This commit is contained in:
parent
60f873911e
commit
befb9b9851
|
@ -130,6 +130,12 @@ body {
|
||||||
height: 27px;
|
height: 27px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.telegram:hover {
|
.telegram:hover {
|
||||||
color: #2789de !important;
|
color: #2789de !important;
|
||||||
}
|
}
|
||||||
|
@ -247,7 +253,6 @@ body {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
|
@ -301,6 +306,7 @@ body {
|
||||||
.dashboard-amount-quote {
|
.dashboard-amount-quote {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
|
|
|
@ -23,9 +23,9 @@ export const BarChartStatistic = <T, >(props: {
|
||||||
<Statistic
|
<Statistic
|
||||||
title={props.title}
|
title={props.title}
|
||||||
valueRender={() =>
|
valueRender={() =>
|
||||||
<div style={{ width: '100%', height: 40, display: 'flex', backgroundColor: 'lightgrey',
|
<div style={{ width: '100%', height: 37, display: 'flex', backgroundColor: 'lightgrey',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineHeight: '40px', }}>
|
lineHeight: '37px', }}>
|
||||||
{props.items.map((item, i) =>
|
{props.items.map((item, i) =>
|
||||||
<div key={props.name(item)}
|
<div key={props.name(item)}
|
||||||
title={props.name(item)}
|
title={props.name(item)}
|
||||||
|
|
|
@ -23,17 +23,15 @@ export const UserLendingCard = (props: {
|
||||||
|
|
||||||
const name = useTokenName(reserve?.liquidityMint);
|
const name = useTokenName(reserve?.liquidityMint);
|
||||||
|
|
||||||
const { balance: tokenBalance } = useUserBalance(props.reserve.liquidityMint);
|
const { balance: tokenBalance, balanceInUSD: tokenBalanceInUSD } = useUserBalance(props.reserve.liquidityMint);
|
||||||
const { balance: collateralBalance } = useUserCollateralBalance(
|
const { balance: collateralBalance, balanceInUSD: collateralBalanceInUSD } = useUserCollateralBalance(
|
||||||
props.reserve
|
props.reserve
|
||||||
);
|
);
|
||||||
|
|
||||||
const { borrowed: totalBorrowed } = useBorrowedAmount(address);
|
const { borrowed: totalBorrowed, borrowedInUSD, ltv, health } = useBorrowedAmount(address);
|
||||||
|
|
||||||
// TODO: calculate
|
// TODO: calculate
|
||||||
const healthFactor = "--";
|
const available = 0; // use all available deposits and convert using market rate
|
||||||
const ltv = 0;
|
|
||||||
const available = 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
@ -58,7 +56,10 @@ export const UserLendingCard = (props: {
|
||||||
Borrowed
|
Borrowed
|
||||||
</Text>
|
</Text>
|
||||||
<div className="card-cell ">
|
<div className="card-cell ">
|
||||||
{formatNumber.format(totalBorrowed)} {name}
|
<div>
|
||||||
|
<div><em>{formatNumber.format(totalBorrowed)}</em> {name}</div>
|
||||||
|
<div className="dashboard-amount-quote">${formatNumber.format(borrowedInUSD)}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -66,7 +67,7 @@ export const UserLendingCard = (props: {
|
||||||
<Text type="secondary" className="card-cell ">
|
<Text type="secondary" className="card-cell ">
|
||||||
Health factor:
|
Health factor:
|
||||||
</Text>
|
</Text>
|
||||||
<div className="card-cell ">{healthFactor}</div>
|
<div className="card-cell ">{health.toFixed(2)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card-row">
|
<div className="card-row">
|
||||||
|
@ -92,7 +93,10 @@ export const UserLendingCard = (props: {
|
||||||
Wallet balance:
|
Wallet balance:
|
||||||
</Text>
|
</Text>
|
||||||
<div className="card-cell ">
|
<div className="card-cell ">
|
||||||
{formatNumber.format(tokenBalance)} {name}
|
<div>
|
||||||
|
<div><em>{formatNumber.format(tokenBalance)}</em> {name}</div>
|
||||||
|
<div className="dashboard-amount-quote">${formatNumber.format(tokenBalanceInUSD)}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -101,7 +105,10 @@ export const UserLendingCard = (props: {
|
||||||
You already deposited:
|
You already deposited:
|
||||||
</Text>
|
</Text>
|
||||||
<div className="card-cell ">
|
<div className="card-cell ">
|
||||||
{formatNumber.format(collateralBalance)} {name}
|
<div>
|
||||||
|
<div><em>{formatNumber.format(collateralBalance)}</em> {name}</div>
|
||||||
|
<div className="dashboard-amount-quote">${formatNumber.format(collateralBalanceInUSD)}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,24 @@ import { useLendingReserve } from "./useLendingReserves";
|
||||||
export function useBorrowedAmount(address?: string | PublicKey) {
|
export function useBorrowedAmount(address?: string | PublicKey) {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
const { userObligationsByReserve } = useUserObligationByReserve(address);
|
const { userObligationsByReserve } = useUserObligationByReserve(address);
|
||||||
const [borrowedLamports, setBorrowedLamports] = useState(0);
|
const [borrowedInfo, setBorrowedInfo] = useState({
|
||||||
|
borrowedLamports: 0,
|
||||||
|
borrowedInUSD: 0,
|
||||||
|
colateralInUSD: 0,
|
||||||
|
ltv: 0,
|
||||||
|
health: 0,
|
||||||
|
});
|
||||||
const reserve = useLendingReserve(address);
|
const reserve = useLendingReserve(address);
|
||||||
const liquidityMint = useMint(reserve?.info.liquidityMint);
|
const liquidityMint = useMint(reserve?.info.liquidityMint);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBorrowedLamports(0);
|
setBorrowedInfo({
|
||||||
|
borrowedLamports: 0,
|
||||||
|
borrowedInUSD: 0,
|
||||||
|
colateralInUSD: 0,
|
||||||
|
ltv: 0,
|
||||||
|
health: 0,
|
||||||
|
});
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
// precache obligation mints
|
// precache obligation mints
|
||||||
|
@ -38,8 +50,17 @@ export function useBorrowedAmount(address?: string | PublicKey) {
|
||||||
cache.add(new PublicKey(address), item, MintParser);
|
cache.add(new PublicKey(address), item, MintParser);
|
||||||
});
|
});
|
||||||
|
|
||||||
setBorrowedLamports(
|
const result = {
|
||||||
userObligationsByReserve.reduce((result, item) => {
|
borrowedLamports: 0,
|
||||||
|
borrowedInUSD: 0,
|
||||||
|
colateralInUSD: 0,
|
||||||
|
ltv: 0,
|
||||||
|
health: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let liquidationThreshold = 0;
|
||||||
|
|
||||||
|
userObligationsByReserve.forEach((item) => {
|
||||||
const borrowed = wadToLamports(
|
const borrowed = wadToLamports(
|
||||||
item.obligation.info.borrowAmountWad
|
item.obligation.info.borrowAmountWad
|
||||||
).toNumber();
|
).toNumber();
|
||||||
|
@ -52,15 +73,27 @@ export function useBorrowedAmount(address?: string | PublicKey) {
|
||||||
item.obligation.info.tokenMint
|
item.obligation.info.tokenMint
|
||||||
) as ParsedAccount<MintInfo>;
|
) as ParsedAccount<MintInfo>;
|
||||||
|
|
||||||
result += (borrowed * owned) / obligationMint?.info.supply.toNumber();
|
result.borrowedLamports += borrowed * (owned / obligationMint?.info.supply.toNumber());
|
||||||
return result;
|
result.borrowedInUSD += item.obligation.info.borrowedInQuote;
|
||||||
}, 0)
|
result.colateralInUSD += item.obligation.info.collateralInQuote;
|
||||||
);
|
liquidationThreshold = item.obligation.info.liquidationThreshold;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (userObligationsByReserve.length === 1) {
|
||||||
|
result.ltv = userObligationsByReserve[0].obligation.info.ltv;
|
||||||
|
result.health = userObligationsByReserve[0].obligation.info.health;
|
||||||
|
} else {
|
||||||
|
result.ltv = 100 * result.borrowedInUSD / result.colateralInUSD;
|
||||||
|
result.health = result.colateralInUSD * liquidationThreshold / 100 / result.borrowedInUSD;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setBorrowedInfo(result);
|
||||||
})();
|
})();
|
||||||
}, [connection, userObligationsByReserve]);
|
}, [connection, userObligationsByReserve, setBorrowedInfo]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
borrowed: fromLamports(borrowedLamports, liquidityMint),
|
borrowed: fromLamports(borrowedInfo.borrowedLamports, liquidityMint),
|
||||||
borrowedLamports,
|
...borrowedInfo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useMint } from "../contexts/accounts";
|
import { useMint } from "../contexts/accounts";
|
||||||
|
import { useMarkets } from "../contexts/market";
|
||||||
import { LendingReserve, reserveMarketCap } from "../models/lending";
|
import { LendingReserve, reserveMarketCap } from "../models/lending";
|
||||||
import { fromLamports } from "../utils/utils";
|
import { fromLamports } from "../utils/utils";
|
||||||
import { useUserBalance } from "./useUserBalance";
|
import { useUserBalance } from "./useUserBalance";
|
||||||
|
@ -9,17 +11,43 @@ export function useUserCollateralBalance(
|
||||||
account?: PublicKey
|
account?: PublicKey
|
||||||
) {
|
) {
|
||||||
const mint = useMint(reserve?.collateralMint);
|
const mint = useMint(reserve?.collateralMint);
|
||||||
const { balanceLamports, accounts } = useUserBalance(
|
const { balanceLamports: userBalance, accounts } = useUserBalance(
|
||||||
reserve?.collateralMint,
|
reserve?.collateralMint,
|
||||||
account
|
account
|
||||||
);
|
);
|
||||||
|
|
||||||
const collateralBalance = reserve &&
|
const [balanceInUSD, setBalanceInUSD] = useState(0);
|
||||||
calculateCollateralBalance(reserve, balanceLamports);
|
const { marketEmitter, midPriceInUSD } = useMarkets();
|
||||||
|
|
||||||
|
const balanceLamports = useMemo(() => reserve &&
|
||||||
|
calculateCollateralBalance(reserve, userBalance),
|
||||||
|
[userBalance, reserve]);
|
||||||
|
|
||||||
|
const balance = useMemo(() => fromLamports(balanceLamports, mint),
|
||||||
|
[balanceLamports, mint]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateBalance = () => {
|
||||||
|
setBalanceInUSD(balance * midPriceInUSD(reserve?.liquidityMint?.toBase58() || ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispose = marketEmitter.onMarket((args) => {
|
||||||
|
if(args.ids.has(reserve?.dexMarket.toBase58() || '')) {
|
||||||
|
updateBalance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateBalance();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispose();
|
||||||
|
};
|
||||||
|
}, [balance, midPriceInUSD, marketEmitter, mint, setBalanceInUSD, reserve]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
balance: fromLamports(collateralBalance, mint),
|
balance,
|
||||||
balanceLamports: collateralBalance,
|
balanceLamports,
|
||||||
|
balanceInUSD,
|
||||||
mint: reserve?.collateralMint,
|
mint: reserve?.collateralMint,
|
||||||
accounts,
|
accounts,
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,6 +18,7 @@ interface EnrichedLendingObligationInfo extends LendingObligation {
|
||||||
health: number;
|
health: number;
|
||||||
borrowedInQuote: number;
|
borrowedInQuote: number;
|
||||||
collateralInQuote: number;
|
collateralInQuote: number;
|
||||||
|
liquidationThreshold: number;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +117,7 @@ export function useEnrichedLendingObligations() {
|
||||||
health,
|
health,
|
||||||
borrowedInQuote,
|
borrowedInQuote,
|
||||||
collateralInQuote,
|
collateralInQuote,
|
||||||
|
liquidationThreshold: item.reserve.info.config.liquidationThreshold,
|
||||||
name: getTokenName(tokenMap, reserve.liquidityMint)
|
name: getTokenName(tokenMap, reserve.liquidityMint)
|
||||||
},
|
},
|
||||||
} as EnrichedLendingObligation;
|
} as EnrichedLendingObligation;
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useMint } from "../contexts/accounts";
|
import { useMint } from "../contexts/accounts";
|
||||||
|
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(mint?: PublicKey, account?: PublicKey) {
|
||||||
const { userAccounts } = useUserAccounts();
|
const { userAccounts } = useUserAccounts();
|
||||||
|
const [balanceInUSD, setBalanceInUSD] = useState(0);
|
||||||
|
const { marketEmitter, midPriceInUSD } = useMarkets();
|
||||||
|
|
||||||
const mintInfo = useMint(mint);
|
const mintInfo = useMint(mint);
|
||||||
const accounts = useMemo(() => {
|
const accounts = useMemo(() => {
|
||||||
return userAccounts
|
return userAccounts
|
||||||
|
@ -24,9 +28,28 @@ export function useUserBalance(mint?: PublicKey, account?: PublicKey) {
|
||||||
);
|
);
|
||||||
}, [accounts]);
|
}, [accounts]);
|
||||||
|
|
||||||
|
const balance = useMemo(() => fromLamports(balanceLamports, mintInfo), [mintInfo, balanceLamports]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateBalance = () => {
|
||||||
|
setBalanceInUSD(balance * midPriceInUSD(mint?.toBase58() || ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispose = marketEmitter.onMarket((args) => {
|
||||||
|
updateBalance();
|
||||||
|
});
|
||||||
|
|
||||||
|
updateBalance();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispose();
|
||||||
|
};
|
||||||
|
}, [balance, midPriceInUSD, marketEmitter, mint, setBalanceInUSD]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
balance: fromLamports(balanceLamports, mintInfo),
|
balance,
|
||||||
balanceLamports,
|
balanceLamports,
|
||||||
|
balanceInUSD,
|
||||||
accounts,
|
accounts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export interface UserDeposit {
|
||||||
reserve: ParsedAccount<LendingReserve>;
|
reserve: ParsedAccount<LendingReserve>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUserDeposits() {
|
export function useUserDeposits(reserveAddress?: string) {
|
||||||
const { userAccounts } = useUserAccounts();
|
const { userAccounts } = useUserAccounts();
|
||||||
const { reserveAccounts } = useLendingReserves();
|
const { reserveAccounts } = useLendingReserves();
|
||||||
const [userDeposits, setUserDeposits] = useState<UserDeposit[]>([]);
|
const [userDeposits, setUserDeposits] = useState<UserDeposit[]>([]);
|
||||||
|
@ -31,26 +31,29 @@ export function useUserDeposits() {
|
||||||
|
|
||||||
const reservesByCollateralMint = useMemo(() => {
|
const reservesByCollateralMint = useMemo(() => {
|
||||||
return reserveAccounts.reduce((result, item) => {
|
return reserveAccounts.reduce((result, item) => {
|
||||||
|
if(!reserveAddress || item.pubkey.toBase58() === reserveAddress) {
|
||||||
result.set(item.info.collateralMint.toBase58(), item);
|
result.set(item.info.collateralMint.toBase58(), item);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, new Map<string, ParsedAccount<LendingReserve>>());
|
}, new Map<string, ParsedAccount<LendingReserve>>());
|
||||||
}, [reserveAccounts]);
|
}, [reserveAccounts, reserveAddress]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeMarkets = new Set(reserveAccounts.map(r => r.info.dexMarket.toBase58()));
|
const activeMarkets = new Set(reserveAccounts.map(r => r.info.dexMarket.toBase58()));
|
||||||
|
|
||||||
const userDepositsFactory = () => {
|
const userDepositsFactory = () => {
|
||||||
return userAccounts
|
return userAccounts
|
||||||
.filter((acc) => reservesByCollateralMint.has(acc.info.mint.toBase58()))
|
.filter((acc) => reservesByCollateralMint.has(acc?.info.mint.toBase58()))
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const reserve = reservesByCollateralMint.get(
|
const reserve = reservesByCollateralMint.get(
|
||||||
item.info.mint.toBase58()
|
item?.info.mint.toBase58()
|
||||||
) as ParsedAccount<LendingReserve>;
|
) as ParsedAccount<LendingReserve>;
|
||||||
|
|
||||||
let collateralMint = cache.get(reserve.info.collateralMint) as ParsedAccount<MintInfo>;
|
let collateralMint = cache.get(reserve.info.collateralMint) as ParsedAccount<MintInfo>;
|
||||||
|
|
||||||
const amountLamports = calculateCollateralBalance(reserve.info, item.info.amount.toNumber());
|
const amountLamports = calculateCollateralBalance(reserve.info, item?.info.amount.toNumber());
|
||||||
const amount = fromLamports(amountLamports, collateralMint.info);
|
const amount = fromLamports(amountLamports, collateralMint?.info);
|
||||||
const price = midPriceInUSD(reserve.info.liquidityMint.toBase58());
|
const price = midPriceInUSD(reserve.info.liquidityMint.toBase58());
|
||||||
const amountInQuote = price * amount;
|
const amountInQuote = price * amount;
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,11 @@ export function useUserObligationByReserve(
|
||||||
: collateralReserve?.toBase58();
|
: collateralReserve?.toBase58();
|
||||||
return userObligations.filter(
|
return userObligations.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
|
borrowId && collateralId ?
|
||||||
item.obligation.info.borrowReserve.toBase58() === borrowId &&
|
item.obligation.info.borrowReserve.toBase58() === borrowId &&
|
||||||
item.obligation.info.collateralReserve.toBase58() === collateralId
|
item.obligation.info.collateralReserve.toBase58() === collateralId :
|
||||||
|
(borrowId && item.obligation.info.borrowReserve.toBase58() === borrowId) ||
|
||||||
|
(collateralId && item.obligation.info.collateralReserve.toBase58() === collateralId)
|
||||||
);
|
);
|
||||||
}, [borrowReserve, collateralReserve, userObligations]);
|
}, [borrowReserve, collateralReserve, userObligations]);
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,6 @@
|
||||||
flex: 200px
|
flex: 200px
|
||||||
}
|
}
|
||||||
|
|
||||||
em {
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: normal;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,6 @@
|
||||||
flex: 80px
|
flex: 80px
|
||||||
}
|
}
|
||||||
|
|
||||||
em {
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: normal;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue