* first iteration

* finish implementation

* finish

* fix pr comments

* fix: added Card Title to Overview

---------

Co-authored-by: Linkie Link <linkielink.dev@gmail.com>
This commit is contained in:
Bob van der Helm 2023-09-19 15:39:14 +02:00 committed by GitHub
parent f87403eb4d
commit 50fd39e926
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 712 additions and 250 deletions

View File

@ -1,6 +1,7 @@
import { getAccountNftQueryClient } from 'api/cosmwasm-client'
export default async function getAccountIds(address: string): Promise<string[]> {
export default async function getAccountIds(address?: string): Promise<string[]> {
if (!address) return []
const accountNftQueryClient = await getAccountNftQueryClient()
const data = await accountNftQueryClient.tokens({ owner: address })

View File

@ -1,7 +1,8 @@
import getWalletAccountIds from 'api/wallets/getAccountIds'
import getAccount from 'api/accounts/getAccount'
import getWalletAccountIds from 'api/wallets/getAccountIds'
export default async function getAccounts(address: string): Promise<Account[]> {
export default async function getAccounts(address?: string): Promise<Account[]> {
if (!address) return []
const accountIds: string[] = await getWalletAccountIds(address)
const $accounts = accountIds.map((accountId) => getAccount(accountId))

View File

@ -54,7 +54,7 @@ export default function AccountComposition(props: Props) {
() => getAccountPositionValues(account, prices),
[account, prices],
)
const positionValue = depositsBalance.plus(lendsBalance).plus(vaultsBalance)
const totalBalance = depositsBalance.plus(lendsBalance).plus(vaultsBalance)
const [updatedPositionValue, updatedDebtsBalance] = useMemo(() => {
const [updatedDepositsBalance, updatedLendsBalance, updatedDebtsBalance, updatedVaultsBalance] =
@ -69,10 +69,7 @@ export default function AccountComposition(props: Props) {
return [updatedPositionValue, updatedDebtsBalance]
}, [updatedAccount, prices])
const totalBalance = useMemo(
() => calculateAccountBalanceValue(account, prices),
[account, prices],
)
const netWorth = useMemo(() => calculateAccountBalanceValue(account, prices), [account, prices])
const updatedTotalBalance = useMemo(
() => (updatedAccount ? calculateAccountBalanceValue(updatedAccount, prices) : BN_ZERO),
[updatedAccount, prices],
@ -93,9 +90,9 @@ export default function AccountComposition(props: Props) {
return (
<div className='flex-wrap w-full p-4 pb-1'>
<Item
title='Total Position Value'
current={positionValue}
change={hasChanged ? updatedPositionValue : positionValue}
title='Total Balance'
current={totalBalance}
change={hasChanged ? updatedPositionValue : totalBalance}
className='pb-3'
/>
<Item
@ -106,9 +103,9 @@ export default function AccountComposition(props: Props) {
isDecrease
/>
<Item
title='Total Balance'
current={totalBalance}
change={hasChanged ? updatedTotalBalance : totalBalance}
title='Net worth'
current={netWorth}
change={hasChanged ? updatedTotalBalance : netWorth}
className='py-3 font-bold border border-transparent border-y-white/20'
/>
<Item

View File

@ -4,8 +4,8 @@ import { FormattedNumber } from 'components/FormattedNumber'
import { ArrowRight } from 'components/Icons'
interface Props {
leverage: BigNumber
updatedLeverage: BigNumber | null
leverage: number
updatedLeverage: number | null
}
export default function AccountDetailsLeverage(props: Props) {
@ -15,7 +15,7 @@ export default function AccountDetailsLeverage(props: Props) {
return (
<FormattedNumber
className={'w-full text-center text-2xs'}
amount={isNaN(leverage.toNumber()) ? 0 : leverage.toNumber()}
amount={isNaN(leverage) ? 0 : leverage}
options={{
maxDecimals: 2,
minDecimals: 2,
@ -30,7 +30,7 @@ export default function AccountDetailsLeverage(props: Props) {
<div className='flex'>
<FormattedNumber
className={'w-full text-center text-2xs'}
amount={isNaN(leverage.toNumber()) ? 0 : leverage.toNumber()}
amount={isNaN(leverage) ? 1 : leverage}
options={{
maxDecimals: 1,
minDecimals: 1,
@ -42,10 +42,10 @@ export default function AccountDetailsLeverage(props: Props) {
<FormattedNumber
className={classNames(
'w-full text-center text-2xs',
updatedLeverage.gt(leverage) && 'text-loss',
updatedLeverage.lt(leverage) && 'text-profit',
updatedLeverage > leverage && 'text-loss',
updatedLeverage < leverage && 'text-profit',
)}
amount={isNaN(updatedLeverage.toNumber()) ? 0 : updatedLeverage.toNumber()}
amount={isNaN(updatedLeverage) ? 0 : updatedLeverage}
options={{ maxDecimals: 1, minDecimals: 1, rounded: true }}
animate
/>

View File

@ -106,18 +106,21 @@ function AccountDetails(props: Props) {
Account Health
</Text>
</div>
<div className='w-full py-4 border-t border-white/20'>
<Text size='2xs' className='mb-0.5 w-full text-center text-white/50'>
Leverage
</Text>
<AccountDetailsLeverage leverage={leverage} updatedLeverage={updatedLeverage} />
</div>
<div className='w-full py-4 border-t border-white/20'>
<Text size='2xs' className='mb-0.5 w-full text-center text-white/50 whitespace-nowrap'>
Net worth
</Text>
<DisplayCurrency coin={coin} className='w-full text-center truncate text-2xs ' />
</div>
<div className='w-full py-4 border-t border-white/20'>
<Text size='2xs' className='mb-0.5 w-full text-center text-white/50'>
Leverage
</Text>
<AccountDetailsLeverage
leverage={leverage.toNumber() || 1}
updatedLeverage={updatedLeverage?.toNumber() || null}
/>
</div>
<div className='w-full py-4 border-t border-white/20'>
<Text size='2xs' className='mb-0.5 w-full text-center text-white/50'>
APR

View File

@ -1,17 +1,17 @@
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import AccountFundContent from 'components/Account/AccountFund/AccountFundContent'
import Card from 'components/Card'
import { CircularProgress } from 'components/CircularProgress'
import FullOverlayContent from 'components/FullOverlayContent'
import useAccountId from 'hooks/useAccountId'
import useAccounts from 'hooks/useAccounts'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store'
export default function AccountFundFullPage() {
const address = useStore((s) => s.address)
const { accountId } = useParams()
const accountId = useAccountId()
const { data: accounts, isLoading } = useAccounts(address)
const currentAccount = useCurrentAccount()

View File

@ -1,5 +1,5 @@
import classNames from 'classnames'
import { useCallback, useEffect } from 'react'
import { useCallback } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import AccountCreateFirst from 'components/Account/AccountCreateFirst'
@ -13,6 +13,7 @@ import Text from 'components/Text'
import WalletBridges from 'components/Wallet/WalletBridges'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LEND_ASSETS_KEY } from 'constants/localStore'
import useAccountId from 'hooks/useAccountId'
import useAutoLend from 'hooks/useAutoLend'
import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance'
import useLocalStorage from 'hooks/useLocalStorage'
@ -33,7 +34,9 @@ interface Props {
export default function AccountMenuContent(props: Props) {
const navigate = useNavigate()
const { pathname } = useLocation()
const { accountId, address } = useParams()
const { address } = useParams()
const accountId = useAccountId()
const createAccount = useStore((s) => s.createAccount)
const baseCurrency = useStore((s) => s.baseCurrency)
const [showMenu, setShowMenu] = useToggle()
@ -97,10 +100,6 @@ export default function AccountMenuContent(props: Props) {
}
}, [checkHasFunds, hasCreditAccounts, setShowMenu, showMenu])
useEffect(() => {
useStore.setState({ accounts: props.accounts })
}, [props.accounts])
if (!address) return null
return (

View File

@ -1,148 +0,0 @@
import classNames from 'classnames'
import { Suspense, useCallback, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import AccountBalancesTable from 'components/Account/AccountBalancesTable'
import AccountComposition from 'components/Account/AccountComposition'
import AccountCreateFirst from 'components/Account/AccountCreateFirst'
import Button from 'components/Button'
import Card from 'components/Card'
import { PlusCircled } from 'components/Icons'
import Loading from 'components/Loading'
import Text from 'components/Text'
import WalletBridges from 'components/Wallet/WalletBridges'
import WalletConnectButton from 'components/Wallet/WalletConnectButton'
import useAccounts from 'hooks/useAccounts'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance'
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
import useStore from 'store'
import { defaultFee } from 'utils/constants'
import { BN } from 'utils/helpers'
function ConnectInfo() {
return (
<Card
className='w-full h-fit bg-white/5'
title='Portfolio'
contentClassName='px-4 py-6 flex justify-center flex-wrap'
>
<Text size='sm' className='w-full text-center'>
You need to be connected to view the porfolio page.
</Text>
<WalletConnectButton className='mt-4' />
</Card>
)
}
function Content() {
const { address: urlAddress } = useParams()
const { data: accounts, isLoading } = useAccounts(urlAddress ?? '')
const walletAddress = useStore((s) => s.address)
const baseCurrency = useStore((s) => s.baseCurrency)
const { availableAssets: borrowAvailableAssets, accountBorrowedAssets } =
useBorrowMarketAssetsTableData()
const { availableAssets: lendingAvailableAssets, accountLentAssets } =
useLendingMarketAssetsTableData()
const borrowAssetsData = useMemo(
() => [...borrowAvailableAssets, ...accountBorrowedAssets],
[borrowAvailableAssets, accountBorrowedAssets],
)
const lendingAssetsData = useMemo(
() => [...lendingAvailableAssets, ...accountLentAssets],
[lendingAvailableAssets, accountLentAssets],
)
const transactionFeeCoinBalance = useCurrentWalletBalance(baseCurrency.denom)
const checkHasFunds = useCallback(() => {
return (
transactionFeeCoinBalance &&
BN(transactionFeeCoinBalance.amount).isGreaterThan(defaultFee.amount[0].amount)
)
}, [transactionFeeCoinBalance])
const handleCreateAccountClick = useCallback(() => {
if (!checkHasFunds()) {
useStore.setState({ focusComponent: { component: <WalletBridges /> } })
return
}
useStore.setState({ focusComponent: { component: <AccountCreateFirst /> } })
}, [checkHasFunds])
if (isLoading) return <Fallback />
if (!walletAddress && !urlAddress) return <ConnectInfo />
if (!accounts || accounts.length === 0)
return (
<Card
className='w-full h-fit bg-white/5'
title='Portfolio'
contentClassName='px-4 py-6 flex justify-center flex-wrap'
>
<Text size='sm' className='w-full text-center'>
You need to create an Account first.
</Text>
<Button
className='mt-4'
onClick={handleCreateAccountClick}
leftIcon={<PlusCircled />}
color='primary'
>
Create Account
</Button>
</Card>
)
return (
<div
className={classNames('grid w-full grid-cols-1 gap-4', 'md:grid-cols-2', 'lg:grid-cols-3')}
>
{accounts.map((account: Account, index: number) => (
<Card
className='w-full h-fit bg-white/5'
title={`Credit Account ${account.id}`}
key={index}
>
<AccountComposition account={account} />
<Text className='w-full px-4 py-2 text-white bg-white/10'>Balances</Text>
<AccountBalancesTable
account={account}
borrowingData={borrowAssetsData}
lendingData={lendingAssetsData}
/>
</Card>
))}
</div>
)
}
function Fallback() {
const { address } = useParams()
const cardCount = 3
if (!address) return <ConnectInfo />
return (
<div
className={classNames('grid w-full grid-cols-1 gap-4', 'md:grid-cols-2', 'lg:grid-cols-3')}
>
{Array.from({ length: cardCount }, (_, i) => (
<Card key={i} className='w-full h-fit bg-white/5' title='Account' contentClassName='py-6'>
<div className='p-4'>
<Loading className='h-4 w-50' />
</div>
<Text className='w-full px-4 py-2 mt-3 text-white bg-white/10'>Balances</Text>
<Loading className='w-full h-4' />
</Card>
))}
</div>
)
}
export default function AccountOverview() {
return (
<Suspense fallback={<Fallback />}>
<Content />
</Suspense>
)
}

View File

@ -48,7 +48,7 @@ export default function HealthBar(props: Props) {
type='info'
className='flex items-center w-full'
>
<div className={classNames('flex max-w-[184px] max-h-1', props.className)}>
<div className={classNames('flex w-full', props.className)}>
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 184 4'>
<mask id='healthBarMask'>
<path fill='#FFFFFF' d='M0,2c0-1.1,0.9-2,2-2h41.6v4H2C0.9,4,0,3.1,0,2z' />

View File

@ -1,19 +1,21 @@
import { useCallback } from 'react'
import { useParams } from 'react-router-dom'
import AccountCreateFirst from 'components/Account/AccountCreateFirst'
import { ACCOUNT_MENU_BUTTON_ID } from 'components/Account/AccountMenuContent'
import Button from 'components/Button'
import { Account, PlusCircled } from 'components/Icons'
import WalletConnectButton from 'components/Wallet/WalletConnectButton'
import useAccountId from 'hooks/useAccountId'
import useAccountIds from 'hooks/useAccountIds'
import useStore from 'store'
export default function ActionButton(props: ButtonProps) {
const { className, color, variant, size } = props
const defaultProps = { className, color, variant, size }
const address = useStore((s) => s.address)
const accounts = useStore((s) => s.accounts)
const { accountId } = useParams()
const { data: accountIds } = useAccountIds(address || '')
const selectedAccountId = useAccountId()
const handleCreateAccountClick = useCallback(() => {
useStore.setState({ focusComponent: { component: <AccountCreateFirst /> } })
@ -21,7 +23,7 @@ export default function ActionButton(props: ButtonProps) {
if (!address) return <WalletConnectButton {...defaultProps} />
if (accounts && accounts.length === 0)
if (accountIds.length === 0) {
return (
<Button
onClick={handleCreateAccountClick}
@ -30,8 +32,9 @@ export default function ActionButton(props: ButtonProps) {
{...defaultProps}
/>
)
}
if (!accountId)
if (!selectedAccountId) {
return (
<Button
text='Select Account'
@ -41,6 +44,7 @@ export default function ActionButton(props: ButtonProps) {
{...defaultProps}
/>
)
}
return <Button {...props} />
}

View File

@ -1,13 +1,13 @@
import { Row } from '@tanstack/react-table'
import moment from 'moment'
import { useState } from 'react'
import { useParams } from 'react-router-dom'
import Button from 'components/Button'
import { AccountArrowDown, LockLocked, LockUnlocked, Plus } from 'components/Icons'
import { Tooltip } from 'components/Tooltip'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { SLIPPAGE_KEY } from 'constants/localStore'
import useAccountId from 'hooks/useAccountId'
import useLocalStorage from 'hooks/useLocalStorage'
import useStore from 'store'
import { VaultStatus } from 'types/enums/vault'
@ -19,7 +19,7 @@ interface Props {
export default function VaultExpanded(props: Props) {
const vault = props.row.original as DepositedVault
const { accountId } = useParams()
const accountId = useAccountId()
const [isConfirming, setIsConfirming] = useState(false)
const withdrawFromVaults = useStore((s) => s.withdrawFromVaults)
const [slippage] = useLocalStorage<number>(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)

View File

@ -1,11 +1,11 @@
import { useState } from 'react'
import { useParams } from 'react-router-dom'
import Button from 'components/Button'
import { ChevronRight } from 'components/Icons'
import NotificationBanner from 'components/NotificationBanner'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { SLIPPAGE_KEY } from 'constants/localStore'
import useAccountId from 'hooks/useAccountId'
import useLocalStorage from 'hooks/useLocalStorage'
import useStore from 'store'
@ -14,7 +14,7 @@ interface Props {
}
export default function VaultUnlockBanner(props: Props) {
const { accountId } = useParams()
const accountId = useAccountId()
const [isConfirming, setIsConfirming] = useState(false)
const withdrawFromVaults = useStore((s) => s.withdrawFromVaults)
const [slippage] = useLocalStorage<number>(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)

View File

@ -1,5 +1,4 @@
import { Suspense, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import Card from 'components/Card'
import { VaultTable } from 'components/Earn/Farm/VaultTable'
@ -7,6 +6,7 @@ import VaultUnlockBanner from 'components/Earn/Farm/VaultUnlockBanner'
import { IS_TESTNET } from 'constants/env'
import { BN_ZERO } from 'constants/math'
import { TESTNET_VAULTS_META_DATA, VAULTS_META_DATA } from 'constants/vaults'
import useAccountId from 'hooks/useAccountId'
import useDepositedVaults from 'hooks/useDepositedVaults'
import useVaults from 'hooks/useVaults'
import { VaultStatus } from 'types/enums/vault'
@ -16,7 +16,7 @@ interface Props {
}
function Content(props: Props) {
const { accountId } = useParams()
const accountId = useAccountId()
const { data: vaults } = useVaults()
const { data: depositedVaults } = useDepositedVaults(accountId || '')
const isAvailable = props.type === 'available'

View File

@ -1,5 +1,4 @@
import { useCallback } from 'react'
import { useParams } from 'react-router-dom'
import { ACCOUNT_MENU_BUTTON_ID } from 'components/Account/AccountMenuContent'
import Button from 'components/Button'
@ -8,6 +7,7 @@ import { ArrowDownLine, ArrowUpLine, Enter } from 'components/Icons'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import ConditionalWrapper from 'hocs/ConditionalWrapper'
import useAccountId from 'hooks/useAccountId'
import useAlertDialog from 'hooks/useAlertDialog'
import useAutoLend from 'hooks/useAutoLend'
import useCurrentAccountDeposits from 'hooks/useCurrentAccountDeposits'
@ -30,7 +30,7 @@ export default function LendingActionButtons(props: Props) {
const { isAutoLendEnabledForCurrentAccount } = useAutoLend()
const assetDepositAmount = accountDeposits.find(byDenom(asset.denom))?.amount
const address = useStore((s) => s.address)
const { accountId } = useParams()
const accountId = useAccountId()
const hasNoDeposit = !!(!assetDepositAmount && address && accountId)
const handleWithdraw = useCallback(() => {

View File

@ -1,8 +1,7 @@
import { useParams } from 'react-router-dom'
import Button from 'components/Button'
import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons'
import Text from 'components/Text'
import useAccountId from 'hooks/useAccountId'
import useStore from 'store'
interface Props {
@ -12,7 +11,7 @@ interface Props {
export default function UnlockModalContent(props: Props) {
const unlock = useStore((s) => s.unlock)
const { accountId } = useParams()
const accountId = useAccountId()
function onConfirm() {
if (!accountId) return

View File

@ -1,5 +1,3 @@
import { useParams } from 'react-router-dom'
import Button from 'components/Button'
import { CircularProgress } from 'components/CircularProgress'
import DisplayCurrency from 'components/DisplayCurrency'
@ -9,6 +7,7 @@ import Modal from 'components/Modal'
import Text from 'components/Text'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { SLIPPAGE_KEY } from 'constants/localStore'
import useAccountId from 'hooks/useAccountId'
import useLocalStorage from 'hooks/useLocalStorage'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
@ -17,7 +16,7 @@ import { demagnify } from 'utils/formatters'
export default function WithdrawFromVaultsModal() {
const modal = useStore((s) => s.withdrawFromVaultsModal)
const { accountId } = useParams()
const accountId = useAccountId()
const withdrawFromVaults = useStore((s) => s.withdrawFromVaults)
const baseCurrency = useStore((s) => s.baseCurrency)
const [slippage] = useLocalStorage<number>(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)

View File

@ -4,11 +4,14 @@ import { useParams } from 'react-router-dom'
import { menuTree } from 'components/Header/DesktopHeader'
import { Logo } from 'components/Icons'
import { NavLink } from 'components/Navigation/NavLink'
import useAccountId from 'hooks/useAccountId'
import useStore from 'store'
import { getRoute } from 'utils/route'
export default function DesktopNavigation() {
const { address, accountId } = useParams()
const { address } = useParams()
const accountId = useAccountId()
const focusComponent = useStore((s) => s.focusComponent)
function getIsActive(pages: string[]) {

View File

@ -0,0 +1,63 @@
import React, { Suspense } from 'react'
import AccountBalancesTable from 'components/Account/AccountBalancesTable'
import Card from 'components/Card'
import TableSkeleton from 'components/TableSkeleton'
import Text from 'components/Text'
import useAccount from 'hooks/useAccount'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
interface Props {
accountId: string
}
function Content(props: Props) {
const { data: account } = useAccount(props.accountId, true)
const { allAssets: borrowAssets } = useBorrowMarketAssetsTableData()
const { allAssets: lendingAssets } = useLendingMarketAssetsTableData()
if (!account || !borrowAssets.length || !lendingAssets.length) {
return <Skeleton />
}
return (
<Skeleton>
<AccountBalancesTable
account={account}
borrowingData={borrowAssets}
lendingData={lendingAssets}
/>
</Skeleton>
)
}
export default function Balances(props: Props) {
return (
<Suspense fallback={<Skeleton />}>
<Content {...props} />
</Suspense>
)
}
interface SkeletonProps {
children?: React.ReactNode
}
function Skeleton(props: SkeletonProps) {
return (
<div>
<Text size='2xl' className='mb-8'>
Balances
</Text>
<Card className='mb-4 h-fit w-full bg-white/5'>
{props.children ? (
props.children
) : (
<TableSkeleton labels={['Asset', 'Value', 'Size', 'APY']} rowCount={3} />
)}
</Card>
</div>
)
}

View File

@ -0,0 +1,26 @@
import React from 'react'
import { NavLink, useParams } from 'react-router-dom'
import { ArrowRight } from 'components/Icons'
import Text from 'components/Text'
import useAccountId from 'hooks/useAccountId'
import { getRoute } from 'utils/route'
interface Props {
accountId: string
}
export default function PortfolioAccountPageHeader(props: Props) {
const { address } = useParams()
const selectedAccountId = useAccountId()
return (
<div className='flex gap-2 items-center pt-4 pb-8 border-b border-white/20'>
<NavLink to={getRoute('portfolio', address, selectedAccountId)}>
<Text className='text-white/40'>Portfolio</Text>
</NavLink>
<ArrowRight className='h-3 text-white/60' />
<Text tag='span'>Credit Account {props.accountId}</Text>
</div>
)
}

View File

@ -0,0 +1,137 @@
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 { 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'
interface Props {
accountId: string
}
function Content(props: Props) {
const { data: account } = useAccount(props.accountId, true)
const { data: prices } = usePrices()
const { health } = useHealthComputer(account)
const { allAssets: borrowAssets } = useBorrowMarketAssetsTableData()
const { allAssets: lendingAssets } = useLendingMarketAssetsTableData()
const stats = useMemo(() => {
if (!account || !borrowAssets.length || !lendingAssets.length) return 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)
return [
{
title: (
<DisplayCurrency
className='text-xl'
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, positionValue)}
/>
),
sub: STATS[0].sub,
},
{
title: (
<DisplayCurrency
className='text-xl'
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, debts)}
/>
),
sub: STATS[1].sub,
},
{
title: (
<DisplayCurrency
className='text-xl'
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, positionValue.minus(debts))}
/>
),
sub: STATS[2].sub,
},
{
title: (
<FormattedNumber className='text-xl' amount={apr.toNumber()} options={{ suffix: '%' }} />
),
sub: STATS[3].sub,
},
{
title: (
<FormattedNumber
className='text-xl'
amount={leverage.toNumber()}
options={{ suffix: 'x' }}
/>
),
sub: STATS[4].sub,
},
]
}, [account, borrowAssets, lendingAssets, prices])
return <Skeleton stats={stats} health={health} {...props} />
}
export default function Summary(props: Props) {
return (
<Suspense fallback={<Skeleton stats={STATS} health={0} {...props} />}>
<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 py-8 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 bg-white/5 flex-grow-1 text-center'>
<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,37 @@
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'
interface Props {
stats: { title: React.ReactNode; sub: string }[]
health: number
isCurrent?: boolean
accountId?: string
}
export default function Skeleton(props: Props) {
return (
<Card className='bg-white/5 p-4'>
<div className='flex justify-between items-center'>
<Text>Credit account {props.accountId || <Loading />}</Text>
<Text size='xs' className='text-white/60'>
{props.isCurrent && '(current)'}
</Text>
</div>
<div className='flex gap-4 mt-6'>
{props.stats.map(({ title, sub }) => (
<TitleAndSubCell key={`${props.accountId}-${sub}`} title={title} sub={sub} />
))}
</div>
<div className='flex gap-1 mt-6'>
<Heart width={20} />
<HealthBar health={props.health} />
</div>
</Card>
)
}

View File

@ -0,0 +1,105 @@
import classNames from 'classnames'
import React, { ReactNode, useMemo } from 'react'
import { NavLink } from 'react-router-dom'
import { FormattedNumber } from 'components/FormattedNumber'
import Loading from 'components/Loading'
import Skeleton from 'components/Portfolio/Card/Skeleton'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import { BN_ZERO } from 'constants/math'
import useAccount from 'hooks/useAccount'
import useAccountId from 'hooks/useAccountId'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useHealthComputer from 'hooks/useHealthComputer'
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import {
calculateAccountApr,
calculateAccountLeverage,
getAccountPositionValues,
} from 'utils/accounts'
import { getRoute } from 'utils/route'
interface Props {
accountId: string
}
export default function PortfolioCard(props: Props) {
const { data: account } = useAccount(props.accountId)
const { health } = useHealthComputer(account)
const { data: prices } = usePrices()
const currentAccountId = useAccountId()
const { allAssets: lendingAssets } = useLendingMarketAssetsTableData()
const { allAssets: borrowAssets } = useBorrowMarketAssetsTableData()
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const address = useStore((s) => s.address)
const [deposits, lends, debts, vaults] = useMemo(() => {
if (!prices.length || !account) return Array(4).fill(BN_ZERO)
return getAccountPositionValues(account, prices)
}, [prices, account])
const leverage = useMemo(() => {
if (!prices.length || !account) return BN_ZERO
return calculateAccountLeverage(account, prices)
}, [account, prices])
const apr = useMemo(() => {
if (!lendingAssets.length || !borrowAssets.length || !prices.length || !account) return null
return calculateAccountApr(account, borrowAssets, lendingAssets, prices)
}, [lendingAssets, borrowAssets, prices, account])
const stats: { title: ReactNode; sub: string }[] = useMemo(() => {
const isLoaded = account && prices.length && apr !== null
return [
{
title: isLoaded ? (
<FormattedNumber
amount={deposits.plus(lends).plus(vaults).minus(debts).toNumber()}
options={{ prefix: '$' }}
/>
) : (
<Loading />
),
sub: 'Net worth',
},
{
title: isLoaded ? (
<FormattedNumber amount={leverage.toNumber() || 1} options={{ suffix: 'x' }} />
) : (
<Loading />
),
sub: 'Leverage',
},
{
title: isLoaded ? (
<FormattedNumber amount={apr.toNumber()} options={{ suffix: '%' }} />
) : (
<Loading />
),
sub: 'APR',
},
]
}, [account, prices.length, deposits, lends, vaults, debts, leverage, apr])
if (!account) {
return <Skeleton stats={stats} health={health} accountId={props.accountId} />
}
return (
<NavLink
to={getRoute(`portfolio/${props.accountId}` as Page, address, currentAccountId)}
className={classNames('w-full hover:bg-white/5', !reduceMotion && 'transition-all')}
>
<Skeleton
stats={stats}
health={health}
accountId={props.accountId}
isCurrent={props.accountId === currentAccountId}
/>
</NavLink>
)
}

View File

@ -0,0 +1,18 @@
import Card from 'components/Card'
import Text from 'components/Text'
import WalletConnectButton from 'components/Wallet/WalletConnectButton'
export default function ConnectInfo() {
return (
<Card
className='w-full h-fit bg-white/5'
title='Portfolio'
contentClassName='px-4 py-6 flex justify-center flex-wrap'
>
<Text size='sm' className='w-full text-center'>
You need to be connected to view the portfolio page.
</Text>
<WalletConnectButton className='mt-4' />
</Card>
)
}

View File

@ -0,0 +1,78 @@
import classNames from 'classnames'
import { useCallback } from 'react'
import { useParams } from 'react-router-dom'
import AccountCreateFirst from 'components/Account/AccountCreateFirst'
import Button from 'components/Button'
import Card from 'components/Card'
import { PlusCircled } from 'components/Icons'
import PortfolioCard from 'components/Portfolio/Card'
import ConnectInfo from 'components/Portfolio/Overview/ConnectInfo'
import Text from 'components/Text'
import WalletBridges from 'components/Wallet/WalletBridges'
import useAccountIds from 'hooks/useAccountIds'
import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance'
import useStore from 'store'
import { defaultFee } from 'utils/constants'
import { BN } from 'utils/helpers'
export default function Content() {
const { address: urlAddress } = useParams()
const walletAddress = useStore((s) => s.address)
const { data: accountIds, isLoading } = useAccountIds(urlAddress)
const baseCurrency = useStore((s) => s.baseCurrency)
const transactionFeeCoinBalance = useCurrentWalletBalance(baseCurrency.denom)
const checkHasFunds = useCallback(() => {
return (
transactionFeeCoinBalance &&
BN(transactionFeeCoinBalance.amount).isGreaterThan(defaultFee.amount[0].amount)
)
}, [transactionFeeCoinBalance])
const handleCreateAccountClick = useCallback(() => {
if (!checkHasFunds()) {
useStore.setState({ focusComponent: { component: <WalletBridges /> } })
return
}
useStore.setState({ focusComponent: { component: <AccountCreateFirst /> } })
}, [checkHasFunds])
if (!walletAddress && !urlAddress) return <ConnectInfo />
if (!isLoading && accountIds?.length === 0) {
return (
<Card
className='w-full h-fit bg-white/5'
title='Portfolio'
contentClassName='px-4 py-6 flex justify-center flex-wrap'
>
<Text size='sm' className='w-full text-center'>
You need to create an Account first.
</Text>
<Button
className='mt-4'
onClick={handleCreateAccountClick}
leftIcon={<PlusCircled />}
color='primary'
>
Create Account
</Button>
</Card>
)
}
return (
<Card title='Credit Accounts' contentClassName='p-6 pt-4'>
<div
className={classNames('grid w-full grid-cols-1 gap-4', 'md:grid-cols-2', 'lg:grid-cols-3')}
>
{accountIds.map((accountId: string, index: number) => {
return <PortfolioCard key={accountId} accountId={accountId} />
})}
</div>
</Card>
)
}

View File

@ -1,12 +1,13 @@
import { Outlet, Route, Routes as RoutesWrapper } from 'react-router-dom'
import { Navigate, Outlet, Route, Routes as RoutesWrapper } from 'react-router-dom'
import Layout from 'pages/_layout'
import BorrowPage from 'pages/BorrowPage'
import FarmPage from 'pages/FarmPage'
import LendPage from 'pages/LendPage'
import MobilePage from 'pages/MobilePage'
import PortfolioAccountPage from 'pages/PortfolioAccountPage'
import PortfolioPage from 'pages/PortfolioPage'
import TradePage from 'pages/TradePage'
import Layout from 'pages/_layout'
export default function Routes() {
return (
@ -26,21 +27,17 @@ export default function Routes() {
<Route path='/mobile' element={<MobilePage />} />
<Route path='/' element={<TradePage />} />
<Route path='/wallets/:address'>
<Route path='accounts/:accountId'>
<Route path='trade' element={<TradePage />} />
<Route path='farm' element={<FarmPage />} />
<Route path='lend' element={<LendPage />} />
<Route path='borrow' element={<BorrowPage />} />
<Route path='portfolio' element={<PortfolioPage />} />
<Route path='' element={<TradePage />} />
</Route>
<Route path='trade' element={<TradePage />} />
<Route path='farm' element={<FarmPage />} />
<Route path='lend' element={<LendPage />} />
<Route path='borrow' element={<BorrowPage />} />
<Route path='portfolio' element={<PortfolioPage />} />
<Route path='portfolio/:accountId'>
<Route path='' element={<PortfolioAccountPage />} />
</Route>
<Route path='' element={<TradePage />} />
</Route>
<Route path='*' element={<Navigate to='/' />} />
</Route>
</RoutesWrapper>
)

View File

@ -0,0 +1,77 @@
import classNames from 'classnames'
import React from 'react'
import { SortNone } from 'components/Icons'
import Loading from 'components/Loading'
import Text from 'components/Text'
interface Props {
labels: string[]
rowCount: number
}
export default function TableSkeleton(props: Props) {
return (
<table className='w-full'>
<thead className='border-b border-white/5'>
<tr>
{props.labels.map((label, index) => {
return (
<th
key={label}
className={classNames('p-3', index === props.labels.length - 1 && 'pr-4')}
>
<div
className={classNames(
'flex',
index === 0 ? 'justify-start' : 'justify-end',
'align-center',
)}
>
<span className='w-6 h-6 text-white'>
<SortNone />
</span>
<Text
tag='span'
size='sm'
className={classNames(
'flex font-normal text-white/70 items-center',
index !== 0 && 'justify-end',
)}
>
{label}
</Text>
</div>
</th>
)
})}
</tr>
</thead>
<tbody>
{Array(props.rowCount)
.fill(null)
.map((_, index) => {
return (
<tr key={index} className='pl-2'>
{props.labels.map((_, index2) => {
return (
<td
key={`${index}-${index2}`}
className={classNames(
index === 0 && 'justify-end',
index2 === 0 && 'pl-4',
index2 === props.labels.length - 1 && 'pr-4',
'p-2 text-right',
)}
>
<Loading className={classNames('w-20 h-3', index2 !== 0 && 'ml-auto')} />
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
)
}

View File

@ -15,7 +15,7 @@ export default function TitleAndSubCell(props: Props) {
<Text size='xs' className={props.className} tag='span'>
{props.title}
</Text>
<Text size='xs' className={classNames('text-white/50', props.className)} tag='span'>
<Text size='xs' className={classNames('text-white/40', props.className)} tag='span'>
{props.sub}
</Text>
</div>

View File

@ -5,7 +5,7 @@ import AccountCreateFirst from 'components/Account/AccountCreateFirst'
import { CircularProgress } from 'components/CircularProgress'
import FullOverlayContent from 'components/FullOverlayContent'
import WalletBridges from 'components/Wallet/WalletBridges'
import useAccounts from 'hooks/useAccounts'
import useAccountIds from 'hooks/useAccountIds'
import useWalletBalances from 'hooks/useWalletBalances'
import useStore from 'store'
import { byDenom } from 'utils/array'
@ -29,7 +29,7 @@ function Content() {
const address = useStore((s) => s.address)
const navigate = useNavigate()
const { pathname } = useLocation()
const { data: accounts, isLoading: isLoadingAccounts } = useAccounts(address)
const { data: accountIds, isLoading: isLoadingAccounts } = useAccountIds(address || '')
const { data: walletBalances, isLoading: isLoadingBalances } = useWalletBalances(address)
const baseAsset = getBaseAsset()
@ -40,17 +40,17 @@ function Content() {
useEffect(() => {
if (
accounts.length !== 0 &&
accountIds.length !== 0 &&
BN(baseBalance).isGreaterThanOrEqualTo(defaultFee.amount[0].amount)
) {
navigate(getRoute(getPage(pathname), address, accounts[0].id))
useStore.setState({ accounts: accounts, balances: walletBalances, focusComponent: null })
navigate(getRoute(getPage(pathname), address, accountIds[0]))
useStore.setState({ balances: walletBalances, focusComponent: null })
}
}, [accounts, baseBalance, navigate, pathname, address, walletBalances])
}, [accountIds, baseBalance, navigate, pathname, address, walletBalances])
if (isLoadingAccounts || isLoadingBalances) return <FetchLoading />
if (BN(baseBalance).isLessThan(defaultFee.amount[0].amount)) return <WalletBridges />
if (accounts.length === 0) return <AccountCreateFirst />
if (accountIds.length === 0) return <AccountCreateFirst />
return null
}

View File

@ -2,9 +2,9 @@ import useSWR from 'swr'
import getAccount from 'api/accounts/getAccount'
export default function useAccounts(accountId?: string) {
export default function useAccounts(accountId?: string, suspense?: boolean) {
return useSWR(`account${accountId}`, () => getAccount(accountId || ''), {
suspense: true,
suspense: suspense,
revalidateOnFocus: false,
})
}

View File

@ -0,0 +1,6 @@
import { useSearchParams } from 'react-router-dom'
export default function useAccountId() {
const [searchParams] = useSearchParams()
return searchParams.get('accountId')
}

View File

@ -0,0 +1,11 @@
import useSWR from 'swr'
import getAccountIds from 'api/wallets/getAccountIds'
export default function useAccountIds(address?: string) {
return useSWR(`wallets/${address}/account-ids`, () => getAccountIds(address), {
suspense: true,
fallback: [],
revalidateOnFocus: false,
})
}

View File

@ -1,11 +1,15 @@
import useSWR from 'swr'
import getAccounts from 'api/wallets/getAccounts'
import useStore from 'store'
export default function useAccounts(address?: string) {
return useSWR(`accounts${address}`, () => getAccounts(address || ''), {
return useSWR(`accounts${address}`, () => getAccounts(address), {
suspense: true,
fallbackData: [],
revalidateOnFocus: false,
onSuccess: (accounts) => {
useStore.setState({ accounts: accounts })
},
})
}

View File

@ -1,5 +1,6 @@
import { useMemo } from 'react'
import useCurrentAccountDebts from 'hooks/useCurrentAccountDebts'
import useDepositEnabledMarkets from 'hooks/useDepositEnabledMarkets'
import useMarketBorrowings from 'hooks/useMarketBorrowings'
import useMarketDeposits from 'hooks/useMarketDeposits'
@ -7,11 +8,11 @@ import useMarketLiquidities from 'hooks/useMarketLiquidities'
import { byDenom } from 'utils/array'
import { getAssetByDenom } from 'utils/assets'
import { BN } from 'utils/helpers'
import useCurrentAccountDebts from 'hooks/useCurrentAccountDebts'
export default function useBorrowMarketAssetsTableData(): {
accountBorrowedAssets: BorrowMarketTableData[]
availableAssets: BorrowMarketTableData[]
allAssets: BorrowMarketTableData[]
} {
const markets = useDepositEnabledMarkets()
const accountDebts = useCurrentAccountDebts()
@ -45,6 +46,10 @@ export default function useBorrowMarketAssetsTableData(): {
;(borrowMarketAsset.debt ? accountBorrowedAssets : availableAssets).push(borrowMarketAsset)
})
return { accountBorrowedAssets, availableAssets }
return {
accountBorrowedAssets,
availableAssets,
allAssets: [...accountBorrowedAssets, ...availableAssets],
}
}, [accountDebts, borrowData, markets, marketDeposits, marketLiquidities])
}

View File

@ -1,9 +1,9 @@
import { useParams } from 'react-router-dom'
import useAccountId from 'hooks/useAccountId'
import useStore from 'store'
export default function useCurrentAccount(): Account | undefined {
const { accountId } = useParams()
const accountId = useAccountId()
const accounts = useStore((s) => s.accounts)
return accounts?.find((account) => account.id === accountId)
}

View File

@ -12,6 +12,7 @@ import { BN } from 'utils/helpers'
function useLendingMarketAssetsTableData(): {
accountLentAssets: LendingMarketTableData[]
availableAssets: LendingMarketTableData[]
allAssets: LendingMarketTableData[]
} {
const markets = useDepositEnabledMarkets()
const accountLentAmounts = useCurrentAccountLends()
@ -52,7 +53,11 @@ function useLendingMarketAssetsTableData(): {
},
)
return { accountLentAssets, availableAssets }
return {
accountLentAssets,
availableAssets,
allAssets: [...accountLentAssets, ...availableAssets],
}
}, [markets, marketDeposits, marketLiquidities, accountLentAmounts, convertAmount])
}

View File

@ -0,0 +1,27 @@
import React from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import Balances from 'components/Portfolio/Account/Balances'
import BreadCrumbs from 'components/Portfolio/Account/BreadCrumbs'
import Summary from 'components/Portfolio/Account/Summary'
import useAccountId from 'hooks/useAccountId'
import { getRoute } from 'utils/route'
export default function PortfolioAccountPage() {
const selectedAccountId = useAccountId()
const { address, accountId } = useParams()
const navigate = useNavigate()
if (!accountId) {
navigate(getRoute('portfolio', address, selectedAccountId))
return null
}
return (
<div>
<BreadCrumbs accountId={accountId} />
<Summary accountId={accountId} />
<Balances accountId={accountId} />
</div>
)
}

View File

@ -1,4 +1,4 @@
import AccountOverview from 'components/Account/AccountOverview'
import AccountOverview from 'components/Portfolio/Overview'
import PortfolioIntro from 'components/Portfolio/PortfolioIntro'
export default function PortfolioPage() {

View File

@ -1 +1 @@
type Page = 'trade' | 'borrow' | 'farm' | 'lend' | 'portfolio' | 'council'
type Page = 'trade' | 'borrow' | 'farm' | 'lend' | 'portfolio' | 'portfolio/{accountId}'

View File

@ -1,26 +1,34 @@
export function getRoute(page: Page, address?: string, accountId?: string) {
export function getRoute(page: Page, address?: string, accountId?: string | null) {
let nextUrl = ''
if (address) {
nextUrl += `/wallets/${address}`
}
if (accountId) {
nextUrl += `/accounts/${accountId}`
nextUrl += `/${page}`
let url = new URL(nextUrl, 'https://app.marsprotocol.io')
if (accountId) {
url.searchParams.append('accountId', accountId)
}
return url.pathname + url.search
}
export function getPage(pathname: string): Page {
const pages: Page[] = ['trade', 'borrow', 'farm', 'lend', 'portfolio']
const segments = pathname.split('/')
const page = segments.find((segment) => pages.includes(segment as Page))
if (page) {
if (page === 'portfolio') {
const path = pathname.split('portfolio')[1]
return (page + path) as Page
}
return page as Page
}
return (nextUrl += `/${page}`)
}
export function getPage(pathname: string) {
const pages: Page[] = ['trade', 'borrow', 'farm', 'lend', 'portfolio', 'council']
const lastSegment = pathname.split('/').pop() as Page
if (!lastSegment) return 'trade'
if (pages.includes(lastSegment)) {
return lastSegment
}
return 'trade'
return 'trade' as Page
}