Skip to content

Commit f9d3349

Browse files
authored
Merge pull request #1652 from mars-protocol/develop
2 parents fdaabd9 + 95d836a commit f9d3349

File tree

16 files changed

+356
-31
lines changed

16 files changed

+356
-31
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mars-v2-frontend",
3-
"version": "2.11.5",
3+
"version": "2.11.6",
44
"homepage": "./",
55
"private": false,
66
"license": "SEE LICENSE IN LICENSE FILE",
@@ -46,9 +46,9 @@
4646
"mobx": "^6.13.7",
4747
"moment": "^2.30.1",
4848
"next": "^15.5.4",
49-
"react": "19.1.1",
49+
"react": "19.2.0",
5050
"react-device-detect": "^2.2.3",
51-
"react-dom": "^19.1.1",
51+
"react-dom": "^19.2.0",
5252
"react-draggable": "^4.5.0",
5353
"react-helmet-async": "^2.0.5",
5454
"react-qr-code": "^2.0.18",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { FETCH_TIMEOUT } from 'constants/query'
2+
import { fetchWithTimeout } from 'utils/fetch'
3+
import { getUrl } from 'utils/url'
4+
5+
interface ManagedVaultDepositorsApiResponse {
6+
denom_owners: ManagedVaultDepositor[]
7+
pagination: {
8+
next_key: string | null
9+
total: string
10+
}
11+
}
12+
13+
export default async function getManagedVaultDepositors(vaultTokensDenom: string) {
14+
try {
15+
const baseUrl = `https://neutron-rest.cosmos-apis.com/cosmos/bank/v1beta1/denom_owners_by_query?denom=${vaultTokensDenom}`
16+
17+
const url = getUrl(baseUrl, '')
18+
const response = await fetchWithTimeout(url, FETCH_TIMEOUT)
19+
20+
if (!response.ok) return []
21+
const data: ManagedVaultDepositorsApiResponse = await response.json()
22+
23+
return data.denom_owners
24+
} catch (error) {
25+
console.error('Could not fetch managed vault depositors.', error)
26+
return []
27+
}
28+
}

src/components/managedVaults/vaultDetails/common/VaultStats.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ interface Props {
1414
export default function VaultStats(props: Props) {
1515
const { stats } = props
1616
return (
17-
<div className='p-3 space-y-3 text-white/60'>
17+
<div className='p-3 space-y-4 text-white/60'>
1818
{stats.map((stat, index) => (
1919
<React.Fragment key={index}>
2020
<div className='flex justify-between items-center'>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import useVaultDepositorsColumns from 'components/managedVaults/vaultDetails/overview/DepositorTable/useVaultDepositorsColumns'
2+
import Table from 'components/common/Table'
3+
import useManagedVaultDepositors from 'hooks/managedVaults/useManagedVaultDepositors'
4+
import { CircularProgress } from 'components/common/CircularProgress'
5+
import Text from 'components/common/Text'
6+
7+
interface Props {
8+
vaultTokensDenom: string
9+
vaultAddress: string
10+
baseTokensDenom: string
11+
vaultTokensAmount: string
12+
ownerAddress: string
13+
}
14+
15+
export default function VaultDepositorsTable(props: Props) {
16+
const { vaultTokensDenom, vaultAddress, baseTokensDenom, vaultTokensAmount, ownerAddress } = props
17+
const managedVaultDepositorsData = useManagedVaultDepositors(vaultTokensDenom)
18+
19+
const columns = useVaultDepositorsColumns(
20+
vaultAddress,
21+
baseTokensDenom,
22+
vaultTokensAmount,
23+
ownerAddress,
24+
)
25+
26+
if (managedVaultDepositorsData.isLoading) {
27+
return (
28+
<div className='flex justify-center items-center h-full w-full'>
29+
<CircularProgress size={50} />
30+
</div>
31+
)
32+
}
33+
34+
if ((managedVaultDepositorsData.data || []).length === 0) {
35+
return (
36+
<div className='flex justify-center items-center h-full w-full'>
37+
<Text size='sm' className='text-white/60'>
38+
This vault has no depositors.
39+
</Text>
40+
</div>
41+
)
42+
}
43+
44+
return (
45+
<Table
46+
title='Depositors'
47+
columns={columns}
48+
data={managedVaultDepositorsData.data || []}
49+
tableBodyClassName='text-white/80'
50+
initialSorting={[{ id: 'percentage', desc: true }]}
51+
spacingClassName='p-2'
52+
hideCard
53+
/>
54+
)
55+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Image from 'next/image'
2+
import Text from 'components/common/Text'
3+
import useManagedVaultOwnerInfo from 'hooks/managedVaults/useManagedVaultOwnerInfo'
4+
import useManagedVaultOwnerPosition from 'hooks/managedVaults/useManagedVaultOwnerPosition'
5+
import { BN } from 'utils/helpers'
6+
import { truncate } from 'utils/formatters'
7+
8+
interface Props {
9+
address: string
10+
vaultAddress?: string
11+
ownerAddress?: string
12+
}
13+
14+
export default function UserAddress(props: Props) {
15+
const { address, vaultAddress, ownerAddress } = props
16+
const { vaultOwnerInfo, isLoading: isOwnerInfoLoading } = useManagedVaultOwnerInfo(address)
17+
const { data: ownerPosition } = useManagedVaultOwnerPosition(vaultAddress || '', address)
18+
19+
// Check if owner has holdings (shares > 0)
20+
const isOwner = ownerAddress && address === ownerAddress
21+
const hasHoldings = isOwner && ownerPosition?.shares && BN(ownerPosition.shares).isGreaterThan(0)
22+
23+
return (
24+
<div className='flex items-center gap-2'>
25+
<span className='h-8 w-8'>
26+
<Image
27+
src={vaultOwnerInfo.avatar.url}
28+
alt={vaultOwnerInfo.name}
29+
width={vaultOwnerInfo.avatar.width}
30+
height={vaultOwnerInfo.avatar.height}
31+
className='rounded-full w-8 h-8'
32+
/>
33+
</span>
34+
<div className='flex items-center gap-1.5'>
35+
<Text size='xs' className='inline-block text-white/60 bg-white/10 rounded px-1.5 py-0.5'>
36+
{isOwnerInfoLoading ? truncate(address, [2, 6]) : vaultOwnerInfo.name}
37+
</Text>
38+
{hasHoldings && (
39+
<Text
40+
size='xs'
41+
tag='div'
42+
className='px-1.5 py-0.5 rounded flex items-center justify-center text-success bg-success/20'
43+
>
44+
Vault Owner
45+
</Text>
46+
)}
47+
</div>
48+
</div>
49+
)
50+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import DisplayCurrency from 'components/common/DisplayCurrency'
2+
import Loading from 'components/common/Loading'
3+
import { BN } from 'utils/helpers'
4+
import { BNCoin } from 'types/classes/BNCoin'
5+
import { useManagedVaultConvertToBaseTokens } from 'hooks/managedVaults/useManagedVaultConvertToBaseTokens'
6+
7+
interface Props {
8+
value: ManagedVaultDepositor
9+
baseTokensDenom: string
10+
vaultAddress: string
11+
}
12+
13+
export default function UserValue(props: Props) {
14+
const { value, baseTokensDenom, vaultAddress } = props
15+
16+
const { data: userVaultTokensAmount, isLoading } = useManagedVaultConvertToBaseTokens(
17+
vaultAddress,
18+
value.balance.amount ?? '0',
19+
)
20+
21+
if (isLoading) {
22+
return (
23+
<div className='flex items-center justify-end'>
24+
<Loading className='w-12 h-4' />
25+
</div>
26+
)
27+
}
28+
29+
return (
30+
<DisplayCurrency
31+
coin={BNCoin.fromDenomAndBigNumber(baseTokensDenom, BN(userVaultTokensAmount ?? 0))}
32+
className='text-xs'
33+
/>
34+
)
35+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import UserAddress from 'components/managedVaults/vaultDetails/overview/DepositorTable/column/UserAddress'
2+
import UserValue from 'components/managedVaults/vaultDetails/overview/DepositorTable/column/UserValue'
3+
import { ColumnDef } from '@tanstack/react-table'
4+
import { FormattedNumber } from 'components/common/FormattedNumber'
5+
import { useMemo } from 'react'
6+
7+
const calculatePercentage = (userBalance: string, totalVaultTokens: string): number => {
8+
return totalVaultTokens && userBalance
9+
? (Number(userBalance) / Number(totalVaultTokens)) * 100
10+
: 0
11+
}
12+
13+
export default function useVaultDepositorsColumns(
14+
vaultAddress: string,
15+
baseTokensDenom: string,
16+
vaultTokensAmount: string,
17+
ownerAddress: string,
18+
) {
19+
return useMemo<ColumnDef<ManagedVaultDepositor>[]>(() => {
20+
return [
21+
{
22+
header: 'Depositor',
23+
id: 'depositor',
24+
cell: ({ row }) => (
25+
<UserAddress
26+
address={row.original.address}
27+
vaultAddress={vaultAddress}
28+
ownerAddress={ownerAddress}
29+
/>
30+
),
31+
},
32+
{
33+
header: 'Percent of Vault Shares',
34+
id: 'percentage',
35+
enableSorting: true,
36+
accessorFn: (row) => calculatePercentage(row.balance?.amount || '0', vaultTokensAmount),
37+
cell: ({ row }) => {
38+
const percentage = calculatePercentage(
39+
row.original.balance?.amount || '0',
40+
vaultTokensAmount,
41+
)
42+
return (
43+
<FormattedNumber amount={percentage} options={{ suffix: '%' }} className='text-xs' />
44+
)
45+
},
46+
},
47+
{
48+
header: 'Value',
49+
id: 'value',
50+
cell: ({ row }) => (
51+
<UserValue
52+
value={row.original}
53+
vaultAddress={vaultAddress}
54+
baseTokensDenom={baseTokensDenom}
55+
/>
56+
),
57+
},
58+
]
59+
}, [vaultAddress, baseTokensDenom, vaultTokensAmount, ownerAddress])
60+
}

src/components/managedVaults/vaultDetails/overview/VaultSummary.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import useBorrowMarketAssetsTableData from 'components/borrow/Table/useBorrowMar
99
import useLendingMarketAssetsTableData from 'components/earn/lend/Table/useLendingMarketAssetsTableData'
1010
import useAccount from 'hooks/accounts/useAccount'
1111
import useHealthComputer from 'hooks/health-computer/useHealthComputer'
12+
import VaultDepositorsTable from 'components/managedVaults/vaultDetails/overview/DepositorTable/VaultDepositorsTable'
1213
import { BN } from 'utils/helpers'
1314
import { BNCoin } from 'types/classes/BNCoin'
1415
import { CardWithTabs } from 'components/common/Card/CardWithTabs'
@@ -115,7 +116,7 @@ export default function VaultSummary(props: Props) {
115116
title: 'Balances',
116117
renderContent: () =>
117118
accountData ? (
118-
<div className='h-64 overflow-y-auto bg-white/5 scrollbar-hide'>
119+
<div className='h-74 overflow-y-auto bg-white/5 scrollbar-hide'>
119120
<AccountBalancesTable
120121
account={accountData}
121122
borrowingData={borrowAssetsData}
@@ -141,7 +142,7 @@ export default function VaultSummary(props: Props) {
141142
tabsArray.push({
142143
title: 'Strategies',
143144
renderContent: () => (
144-
<div className='h-64 overflow-y-auto bg-white/5 scrollbar-hide'>
145+
<div className='h-74 overflow-y-auto bg-white/5 scrollbar-hide'>
145146
<AccountStrategiesTable account={accountData} hideCard />
146147
</div>
147148
),
@@ -155,14 +156,31 @@ export default function VaultSummary(props: Props) {
155156
tabsArray.push({
156157
title: 'Perp Positions',
157158
renderContent: () => (
158-
<div className='h-64 overflow-y-auto bg-white/5 scrollbar-hide'>
159+
<div className='h-74 overflow-y-auto bg-white/5 scrollbar-hide'>
159160
<AccountPerpPositionTable account={accountData} hideCard />
160161
</div>
161162
),
162163
notificationCount: perpsCount,
163164
})
164165
}
165166

167+
if (accountData) {
168+
tabsArray.push({
169+
title: 'Depositors',
170+
renderContent: () => (
171+
<div className='h-74 overflow-y-auto bg-white/5 scrollbar-hide'>
172+
<VaultDepositorsTable
173+
vaultTokensDenom={details.vault_tokens_denom}
174+
vaultAddress={details.vault_address}
175+
baseTokensDenom={details.base_tokens_denom}
176+
vaultTokensAmount={details.vault_tokens_amount}
177+
ownerAddress={details.ownerAddress || ''}
178+
/>
179+
</div>
180+
),
181+
})
182+
}
183+
166184
return tabsArray
167185
}, [
168186
accountData,
@@ -175,6 +193,11 @@ export default function VaultSummary(props: Props) {
175193
leverage,
176194
netWorth.amount,
177195
positionValue.amount,
196+
details.vault_tokens_denom,
197+
details.vault_address,
198+
details.base_tokens_denom,
199+
details.vault_tokens_amount,
200+
details.ownerAddress,
178201
])
179202

180203
return <CardWithTabs tabs={tabs} textSizeClass='text-base' />

src/components/managedVaults/vaultDetails/performance/UserMetrics.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import AssetImage from 'components/common/assets/AssetImage'
33
import DisplayCurrency from 'components/common/DisplayCurrency'
44
import { FormattedNumber } from 'components/common/FormattedNumber'
55
import TitleAndSubCell from 'components/common/TitleAndSubCell'
6+
import { Tooltip } from 'components/common/Tooltip'
67
import useVaultAssets from 'hooks/assets/useVaultAssets'
78
import { useManagedVaultConvertToBaseTokens } from 'hooks/managedVaults/useManagedVaultConvertToBaseTokens'
89
import useManagedVaultUserPosition from 'hooks/managedVaults/useManagedVaultUserPosition'
@@ -42,15 +43,16 @@ export default function UserMetrics(props: Props) {
4243
},
4344
{
4445
value: userPosition?.pnl || 0,
45-
label: 'Your Total Earnings',
46+
label: 'Your Estimated PnL',
4647
isCurrency: true,
4748
formatOptions: { maxDecimals: 2, minDecimals: 2 },
4849
isProfitOrLoss: true,
4950
showSignPrefix: true,
51+
hasTooltip: true,
5052
},
5153
{
5254
value: calculateROI(userVaultTokensAmount ?? 0),
53-
label: 'Your ROI',
55+
label: 'Your Estimated ROI',
5456
formatOptions: { maxDecimals: 2, minDecimals: 2, suffix: '%' },
5557
isProfitOrLoss: true,
5658
},
@@ -88,7 +90,16 @@ export default function UserMetrics(props: Props) {
8890

8991
return (
9092
<div key={index} className='relative text-center py-4 sm:flex-1'>
91-
<TitleAndSubCell title={value} sub={metric.label} />
93+
{metric.hasTooltip ? (
94+
<Tooltip
95+
content='This is an estimate based on average vault performance, not your actual earnings.'
96+
type='info'
97+
>
98+
{<TitleAndSubCell title={value} sub={metric.label} />}
99+
</Tooltip>
100+
) : (
101+
<TitleAndSubCell title={value} sub={metric.label} />
102+
)}
92103
{index < metrics.length - 1 && (
93104
<div className='hidden sm:block absolute right-0 top-1/2 h-8 w-[1px] bg-white/10 -translate-y-1/2' />
94105
)}

0 commit comments

Comments
 (0)