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