feat: add LTV, and health factor calculations

This commit is contained in:
bartosz-lipinski 2020-12-22 00:00:00 -06:00
parent 60f873911e
commit befb9b9851
11 changed files with 156 additions and 63 deletions

View File

@ -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) {

View File

@ -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)}

View File

@ -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>

View File

@ -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,
};
}

View File

@ -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,
};

View File

@ -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;

View File

@ -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,
};
}

View File

@ -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;

View File

@ -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]);

View File

@ -21,12 +21,6 @@
flex: 200px
}
em {
font-weight: bold;
font-style: normal;
text-decoration: none;
}
border-bottom: 1px solid #eee;
}

View File

@ -16,12 +16,6 @@
flex: 80px
}
em {
font-weight: bold;
font-style: normal;
text-decoration: none;
}
border-bottom: 1px solid #eee;
}