added portfolio summary for all accounts (#512)

This commit is contained in:
Bob van der Helm 2023-09-30 09:56:11 +02:00 committed by GitHub
parent fac07787c5
commit 44196f1a10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 200 additions and 89 deletions

View File

@ -1,26 +1,16 @@
import React, { Suspense, useMemo } from 'react'
import HealthBar from 'components/Account/HealthBar'
import Card from 'components/Card'
import DisplayCurrency from 'components/DisplayCurrency'
import { FormattedNumber } from 'components/FormattedNumber'
import { Heart } from 'components/Icons'
import Loading from 'components/Loading'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import Skeleton from 'components/Portfolio/SummarySkeleton'
import { MAX_AMOUNT_DECIMALS } from 'constants/math'
import { ORACLE_DENOM } from 'constants/oracle'
import useAccount from 'hooks/useAccount'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useHealthComputer from 'hooks/useHealthComputer'
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
import usePrices from 'hooks/usePrices'
import { BNCoin } from 'types/classes/BNCoin'
import {
calculateAccountApr,
calculateAccountLeverage,
getAccountPositionValues,
} from 'utils/accounts'
import { getAccountSummaryStats } from 'utils/accounts'
import { DEFAULT_PORTFOLIO_STATS } from 'utils/constants'
interface Props {
accountId: string
@ -34,40 +24,27 @@ function Content(props: Props) {
const { allAssets: lendingAssets } = useLendingMarketAssetsTableData()
const stats = useMemo(() => {
if (!account || !borrowAssets.length || !lendingAssets.length) return STATS
if (!account || !borrowAssets.length || !lendingAssets.length) return DEFAULT_PORTFOLIO_STATS
const [deposits, lends, debts, vaults] = getAccountPositionValues(account, prices)
const positionValue = deposits.plus(lends).plus(vaults)
const apr = calculateAccountApr(account, borrowAssets, lendingAssets, prices)
const leverage = calculateAccountLeverage(account, prices)
const { positionValue, debts, netWorth, apr, leverage } = getAccountSummaryStats(
account,
prices,
borrowAssets,
lendingAssets,
)
return [
{
title: (
<DisplayCurrency
className='text-xl'
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, positionValue)}
/>
),
sub: STATS[0].sub,
title: <DisplayCurrency className='text-xl' coin={positionValue} />,
sub: DEFAULT_PORTFOLIO_STATS[0].sub,
},
{
title: (
<DisplayCurrency
className='text-xl'
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, debts)}
/>
),
sub: STATS[1].sub,
title: <DisplayCurrency className='text-xl' coin={debts} />,
sub: DEFAULT_PORTFOLIO_STATS[1].sub,
},
{
title: (
<DisplayCurrency
className='text-xl'
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, positionValue.minus(debts))}
/>
),
sub: STATS[2].sub,
title: <DisplayCurrency className='text-xl' coin={netWorth} />,
sub: DEFAULT_PORTFOLIO_STATS[2].sub,
},
{
title: (
@ -81,7 +58,7 @@ function Content(props: Props) {
}}
/>
),
sub: STATS[3].sub,
sub: DEFAULT_PORTFOLIO_STATS[3].sub,
},
{
title: (
@ -91,56 +68,18 @@ function Content(props: Props) {
options={{ suffix: 'x' }}
/>
),
sub: STATS[4].sub,
sub: DEFAULT_PORTFOLIO_STATS[4].sub,
},
]
}, [account, borrowAssets, lendingAssets, prices])
return <Skeleton stats={stats} health={health} {...props} />
return <Skeleton stats={stats} health={health} title={`Credit account ${props.accountId}`} />
}
export default function Summary(props: Props) {
return (
<Suspense fallback={<Skeleton stats={STATS} health={0} {...props} />}>
<Suspense fallback={<Skeleton health={0} title={`Credit account ${props.accountId}`} />}>
<Content {...props} />
</Suspense>
)
}
interface SkeletonProps extends Props {
stats: { title: React.ReactNode | null; sub: string }[]
health: number
}
function Skeleton(props: SkeletonProps) {
return (
<div className='flex flex-col w-full gap-8'>
<div className='flex justify-between'>
<Text size='2xl'>Credit Account {props.accountId}</Text>
<div className='flex gap-1 max-w-[300px] flex-grow'>
<Heart width={20} />
<HealthBar health={props.health} className='h-full' />
</div>
</div>
<div className='grid grid-cols-2 gap-4 lg:grid-cols-5 md:grid-cols-4 sm:grid-cols-3'>
{props.stats.map((stat) => (
<Card key={stat.sub} className='p-6 text-center bg-white/5 flex-grow-1'>
<TitleAndSubCell
title={stat.title || <Loading className='w-20 h-6 mx-auto mb-2' />}
sub={stat.sub}
className='mb-1'
/>
</Card>
))}
</div>
</div>
)
}
const STATS = [
{ title: null, sub: 'Total Balance' },
{ title: null, sub: 'Total Debt' },
{ title: null, sub: 'Net Worth' },
{ title: null, sub: 'APR' },
{ title: null, sub: 'Account Leverage' },
]

View File

@ -0,0 +1,93 @@
import React, { useMemo } from 'react'
import { useParams } from 'react-router-dom'
import DisplayCurrency from 'components/DisplayCurrency'
import { FormattedNumber } from 'components/FormattedNumber'
import SummarySkeleton from 'components/Portfolio/SummarySkeleton'
import { MAX_AMOUNT_DECIMALS } from 'constants/math'
import useAccounts from 'hooks/useAccounts'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import { getAccountSummaryStats } from 'utils/accounts'
import { DEFAULT_PORTFOLIO_STATS } from 'utils/constants'
export default function PortfolioSummary() {
const { address: urlAddress } = useParams()
const walletAddress = useStore((s) => s.address)
const { data: prices } = usePrices()
const { allAssets: borrowAssets } = useBorrowMarketAssetsTableData()
const { allAssets: lendingAssets } = useLendingMarketAssetsTableData()
const { data: accounts } = useAccounts(urlAddress || walletAddress)
const stats = useMemo(() => {
if (!accounts?.length) return
const combinedAccount = accounts.reduce(
(combinedAccount, account) => {
combinedAccount.debts = combinedAccount.debts.concat(account.debts)
combinedAccount.deposits = combinedAccount.deposits.concat(account.deposits)
combinedAccount.lends = combinedAccount.lends.concat(account.lends)
combinedAccount.vaults = combinedAccount.vaults.concat(account.vaults)
return combinedAccount
},
{
id: '1',
deposits: [],
lends: [],
debts: [],
vaults: [],
} as Account,
)
const { positionValue, debts, netWorth, apr, leverage } = getAccountSummaryStats(
combinedAccount,
prices,
borrowAssets,
lendingAssets,
)
return [
{
title: <DisplayCurrency className='text-xl' coin={positionValue} />,
sub: DEFAULT_PORTFOLIO_STATS[0].sub,
},
{
title: <DisplayCurrency className='text-xl' coin={debts} />,
sub: DEFAULT_PORTFOLIO_STATS[1].sub,
},
{
title: <DisplayCurrency className='text-xl' coin={netWorth} />,
sub: DEFAULT_PORTFOLIO_STATS[2].sub,
},
{
title: (
<FormattedNumber
className='text-xl'
amount={apr.toNumber()}
options={{
suffix: '%',
maxDecimals: apr.abs().isLessThan(0.1) ? MAX_AMOUNT_DECIMALS : 2,
minDecimals: 2,
}}
/>
),
sub: 'Combined APR',
},
{
title: (
<FormattedNumber
className='text-xl'
amount={leverage.toNumber()}
options={{ suffix: 'x' }}
/>
),
sub: 'Combined leverage',
},
]
}, [accounts, borrowAssets, lendingAssets, prices])
if (!walletAddress && !urlAddress) return null
return <SummarySkeleton title='Portfolio Summary' stats={stats} />
}

View File

@ -1,5 +1,5 @@
import classNames from 'classnames'
import { useCallback } from 'react'
import React, { useCallback } from 'react'
import { useParams } from 'react-router-dom'
import AccountCreateFirst from 'components/Account/AccountCreateFirst'
@ -16,7 +16,7 @@ import useStore from 'store'
import { defaultFee } from 'utils/constants'
import { BN } from 'utils/helpers'
export default function Content() {
export default function AccountSummary() {
const { address: urlAddress } = useParams()
const walletAddress = useStore((s) => s.address)
const { data: accountIds, isLoading } = useAccountIds(urlAddress)
@ -65,12 +65,17 @@ export default function Content() {
}
return (
<div
className={classNames('grid w-full grid-cols-1 gap-6', 'md:grid-cols-2', 'lg:grid-cols-3')}
>
{accountIds.map((accountId: string, index: number) => {
return <PortfolioCard key={accountId} accountId={accountId} />
})}
<div className='w-full mt-4'>
<Text size='2xl' className='mb-8'>
Credit Accounts
</Text>
<div
className={classNames('grid w-full grid-cols-1 gap-6', 'md:grid-cols-2', 'lg:grid-cols-3')}
>
{accountIds.map((accountId: string, index: number) => {
return <PortfolioCard key={accountId} accountId={accountId} />
})}
</div>
</div>
)
}

View File

@ -0,0 +1,43 @@
import React from 'react'
import HealthBar from 'components/Account/HealthBar'
import Card from 'components/Card'
import { Heart } from 'components/Icons'
import Loading from 'components/Loading'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import { DEFAULT_PORTFOLIO_STATS } from 'utils/constants'
interface Props {
stats?: { title: React.ReactNode | null; sub: string }[]
health?: number
title: string
}
export default function SummarySkeleton(props: Props) {
const stats = props.stats || DEFAULT_PORTFOLIO_STATS
return (
<div className='flex flex-col w-full gap-8'>
<div className='flex justify-between'>
<Text size='2xl'>{props.title}</Text>
{props.health !== undefined && (
<div className='flex gap-1 max-w-[300px] flex-grow'>
<Heart width={20} />
<HealthBar health={props.health} className='h-full' />
</div>
)}
</div>
<div className='grid grid-cols-2 gap-4 lg:grid-cols-5 md:grid-cols-4 sm:grid-cols-3'>
{stats.map((stat) => (
<Card key={stat.sub} className='p-6 text-center bg-white/5 flex-grow-1'>
<TitleAndSubCell
title={stat.title || <Loading className='w-20 h-6 mx-auto mb-2' />}
sub={stat.sub}
className='mb-1'
/>
</Card>
))}
</div>
</div>
)
}

View File

@ -1,5 +1,6 @@
import MigrationBanner from 'components/MigrationBanner'
import AccountOverview from 'components/Portfolio/Overview'
import PortfolioSummary from 'components/Portfolio/Overview/Summary'
import PortfolioIntro from 'components/Portfolio/PortfolioIntro'
export default function PortfolioPage() {
@ -7,6 +8,7 @@ export default function PortfolioPage() {
<div className='flex flex-wrap w-full gap-6'>
<MigrationBanner />
<PortfolioIntro />
<PortfolioSummary />
<AccountOverview />
</div>
)

View File

@ -1,6 +1,7 @@
import BigNumber from 'bignumber.js'
import { BN_ZERO } from 'constants/math'
import { ORACLE_DENOM } from 'constants/oracle'
import { BNCoin } from 'types/classes/BNCoin'
import {
Positions,
@ -249,3 +250,23 @@ export function computeHealthGaugePercentage(health: number) {
return 100 - (health / ATTENTION_CUTOFF) * UNHEALTHY_BAR_SIZE
}
export function getAccountSummaryStats(
account: Account,
prices: BNCoin[],
borrowAssets: BorrowMarketTableData[],
lendingAssets: LendingMarketTableData[],
) {
const [deposits, lends, debts, vaults] = getAccountPositionValues(account, prices)
const positionValue = deposits.plus(lends).plus(vaults)
const apr = calculateAccountApr(account, borrowAssets, lendingAssets, prices)
const leverage = calculateAccountLeverage(account, prices)
return {
positionValue: BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, positionValue),
debts: BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, debts),
netWorth: BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, positionValue.minus(debts)),
apr,
leverage,
}
}

View File

@ -14,3 +14,11 @@ export const LTV_BUFFER = 0.99
export const DEPOSIT_CAP_BUFFER = 0.999
export const VAULT_DEPOSIT_BUFFER = 0.999
export const DEFAULT_PORTFOLIO_STATS = [
{ title: null, sub: 'Total Balance' },
{ title: null, sub: 'Total Debt' },
{ title: null, sub: 'Net Worth' },
{ title: null, sub: 'APR' },
{ title: null, sub: 'Account Leverage' },
]