✨ added portfolio summary for all accounts (#512)
This commit is contained in:
parent
fac07787c5
commit
44196f1a10
@ -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' },
|
|
||||||
]
|
|
||||||
|
93
src/components/Portfolio/Overview/Summary.tsx
Normal file
93
src/components/Portfolio/Overview/Summary.tsx
Normal 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} />
|
||||||
|
}
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
43
src/components/Portfolio/SummarySkeleton.tsx
Normal file
43
src/components/Portfolio/SummarySkeleton.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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' },
|
||||||
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user