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 React, { Suspense, useMemo } from 'react'
import HealthBar from 'components/Account/HealthBar'
import Card from 'components/Card'
import DisplayCurrency from 'components/DisplayCurrency' import DisplayCurrency from 'components/DisplayCurrency'
import { FormattedNumber } from 'components/FormattedNumber' import { FormattedNumber } from 'components/FormattedNumber'
import { Heart } from 'components/Icons' import Skeleton from 'components/Portfolio/SummarySkeleton'
import Loading from 'components/Loading'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import { MAX_AMOUNT_DECIMALS } from 'constants/math' import { MAX_AMOUNT_DECIMALS } from 'constants/math'
import { ORACLE_DENOM } from 'constants/oracle'
import useAccount from 'hooks/useAccount' import useAccount from 'hooks/useAccount'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData' import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useHealthComputer from 'hooks/useHealthComputer' import useHealthComputer from 'hooks/useHealthComputer'
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData' import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
import usePrices from 'hooks/usePrices' import usePrices from 'hooks/usePrices'
import { BNCoin } from 'types/classes/BNCoin' import { getAccountSummaryStats } from 'utils/accounts'
import { import { DEFAULT_PORTFOLIO_STATS } from 'utils/constants'
calculateAccountApr,
calculateAccountLeverage,
getAccountPositionValues,
} from 'utils/accounts'
interface Props { interface Props {
accountId: string accountId: string
@ -34,40 +24,27 @@ function Content(props: Props) {
const { allAssets: lendingAssets } = useLendingMarketAssetsTableData() const { allAssets: lendingAssets } = useLendingMarketAssetsTableData()
const stats = useMemo(() => { 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, debts, netWorth, apr, leverage } = getAccountSummaryStats(
const positionValue = deposits.plus(lends).plus(vaults) account,
const apr = calculateAccountApr(account, borrowAssets, lendingAssets, prices) prices,
const leverage = calculateAccountLeverage(account, prices) borrowAssets,
lendingAssets,
)
return [ return [
{ {
title: ( title: <DisplayCurrency className='text-xl' coin={positionValue} />,
<DisplayCurrency sub: DEFAULT_PORTFOLIO_STATS[0].sub,
className='text-xl'
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, positionValue)}
/>
),
sub: STATS[0].sub,
}, },
{ {
title: ( title: <DisplayCurrency className='text-xl' coin={debts} />,
<DisplayCurrency sub: DEFAULT_PORTFOLIO_STATS[1].sub,
className='text-xl'
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, debts)}
/>
),
sub: STATS[1].sub,
}, },
{ {
title: ( title: <DisplayCurrency className='text-xl' coin={netWorth} />,
<DisplayCurrency sub: DEFAULT_PORTFOLIO_STATS[2].sub,
className='text-xl'
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, positionValue.minus(debts))}
/>
),
sub: STATS[2].sub,
}, },
{ {
title: ( title: (
@ -81,7 +58,7 @@ function Content(props: Props) {
}} }}
/> />
), ),
sub: STATS[3].sub, sub: DEFAULT_PORTFOLIO_STATS[3].sub,
}, },
{ {
title: ( title: (
@ -91,56 +68,18 @@ function Content(props: Props) {
options={{ suffix: 'x' }} options={{ suffix: 'x' }}
/> />
), ),
sub: STATS[4].sub, sub: DEFAULT_PORTFOLIO_STATS[4].sub,
}, },
] ]
}, [account, borrowAssets, lendingAssets, prices]) }, [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) { export default function Summary(props: Props) {
return ( return (
<Suspense fallback={<Skeleton stats={STATS} health={0} {...props} />}> <Suspense fallback={<Skeleton health={0} title={`Credit account ${props.accountId}`} />}>
<Content {...props} /> <Content {...props} />
</Suspense> </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 classNames from 'classnames'
import { useCallback } from 'react' import React, { useCallback } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import AccountCreateFirst from 'components/Account/AccountCreateFirst' import AccountCreateFirst from 'components/Account/AccountCreateFirst'
@ -16,7 +16,7 @@ import useStore from 'store'
import { defaultFee } from 'utils/constants' import { defaultFee } from 'utils/constants'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
export default function Content() { export default function AccountSummary() {
const { address: urlAddress } = useParams() const { address: urlAddress } = useParams()
const walletAddress = useStore((s) => s.address) const walletAddress = useStore((s) => s.address)
const { data: accountIds, isLoading } = useAccountIds(urlAddress) const { data: accountIds, isLoading } = useAccountIds(urlAddress)
@ -65,12 +65,17 @@ export default function Content() {
} }
return ( return (
<div <div className='w-full mt-4'>
className={classNames('grid w-full grid-cols-1 gap-6', 'md:grid-cols-2', 'lg:grid-cols-3')} <Text size='2xl' className='mb-8'>
> Credit Accounts
{accountIds.map((accountId: string, index: number) => { </Text>
return <PortfolioCard key={accountId} accountId={accountId} /> <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> </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 MigrationBanner from 'components/MigrationBanner'
import AccountOverview from 'components/Portfolio/Overview' import AccountOverview from 'components/Portfolio/Overview'
import PortfolioSummary from 'components/Portfolio/Overview/Summary'
import PortfolioIntro from 'components/Portfolio/PortfolioIntro' import PortfolioIntro from 'components/Portfolio/PortfolioIntro'
export default function PortfolioPage() { export default function PortfolioPage() {
@ -7,6 +8,7 @@ export default function PortfolioPage() {
<div className='flex flex-wrap w-full gap-6'> <div className='flex flex-wrap w-full gap-6'>
<MigrationBanner /> <MigrationBanner />
<PortfolioIntro /> <PortfolioIntro />
<PortfolioSummary />
<AccountOverview /> <AccountOverview />
</div> </div>
) )

View File

@ -1,6 +1,7 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { BN_ZERO } from 'constants/math' import { BN_ZERO } from 'constants/math'
import { ORACLE_DENOM } from 'constants/oracle'
import { BNCoin } from 'types/classes/BNCoin' import { BNCoin } from 'types/classes/BNCoin'
import { import {
Positions, Positions,
@ -249,3 +250,23 @@ export function computeHealthGaugePercentage(health: number) {
return 100 - (health / ATTENTION_CUTOFF) * UNHEALTHY_BAR_SIZE 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 DEPOSIT_CAP_BUFFER = 0.999
export const VAULT_DEPOSIT_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' },
]