diff --git a/.changeset/puny-lamps-check.md b/.changeset/puny-lamps-check.md new file mode 100644 index 0000000000..b0619c899a --- /dev/null +++ b/.changeset/puny-lamps-check.md @@ -0,0 +1,6 @@ +--- +"@venusprotocol/chains": minor +"@venusprotocol/evm": minor +--- + +add institutional vaults diff --git a/apps/evm/src/__mocks__/models/vaults.ts b/apps/evm/src/__mocks__/models/vaults.ts index 0b3593f755..5eeb82b5e0 100644 --- a/apps/evm/src/__mocks__/models/vaults.ts +++ b/apps/evm/src/__mocks__/models/vaults.ts @@ -2,15 +2,17 @@ import BigNumber from 'bignumber.js'; import type { GetFixedRatedVaultsOutput } from 'clients/api/queries/getFixedRatedVaults'; import { + type InstitutionalVault, type LockedDeposit, VaultCategory, VaultManager, VaultStatus, + VaultType, type VenusVault, } from 'types'; import type { Address } from 'viem'; -import { vai, xvs } from './tokens'; +import { usdc, vai, xvs } from './tokens'; export const vaults: VenusVault[] = [ { @@ -22,10 +24,11 @@ export const vaults: VenusVault[] = [ rewardTokenPriceCents: new BigNumber(100), dailyEmissionMantissa: new BigNumber('144000000000000000000'), dailyEmissionCents: 14400, - totalStakedMantissa: new BigNumber('415000000000000000000'), - totalStakedCents: 41500, - stakingAprPercentage: 12665.060240963856, + stakeBalanceMantissa: new BigNumber('415000000000000000000'), + stakeBalanceCents: 41500, + stakeAprPercentage: 12665.060240963856, category: VaultCategory.STABLECOINS, + vaultType: VaultType.Venus, manager: VaultManager.Venus, managerIcon: 'logoMobile', status: VaultStatus.Active, @@ -40,12 +43,13 @@ export const vaults: VenusVault[] = [ rewardTokenPriceCents: new BigNumber(100), dailyEmissionMantissa: new BigNumber('144000000000000000000'), dailyEmissionCents: 14400, - totalStakedMantissa: new BigNumber('400000000000000000000000000'), - totalStakedCents: 40000000000, - stakingAprPercentage: 12.92281835063781, - userStakedMantissa: new BigNumber('233000000000000000000'), - userStakedCents: 23300, + stakeBalanceMantissa: new BigNumber('400000000000000000000000000'), + stakeBalanceCents: 40000000000, + stakeAprPercentage: 12.92281835063781, + userStakeBalanceMantissa: new BigNumber('233000000000000000000'), + userStakeBalanceCents: 23300, category: VaultCategory.GOVERNANCE, + vaultType: VaultType.Venus, manager: VaultManager.Venus, managerIcon: 'logoMobile', status: VaultStatus.Active, @@ -98,11 +102,137 @@ export const fixedRatedVaults: GetFixedRatedVaultsOutput = [ maturityDate: '2026-06-25T00:00:00.000Z', createdAt: '2026-01-21T20:14:15.000Z', updatedAt: '2026-01-21T20:14:15.000Z', + tokenPrices: [ + { + id: 'fake-price-pendle', + tokenAddress: '0xe052823b4aefc6e230FAf46231A57d0905E30AE0', + tokenWrappedAddress: null, + chainId: '56', + priceMantissa: '682687557196753800000000000000000000000', + priceSource: 'oracle', + priceOracleAddress: '0x0000000000000000000000000000000000000001', + mainOracleAddress: '0x0000000000000000000000000000000000000001', + mainOracleName: 'ResilientOracle', + isPriceInvalid: false, + hasErrorFetchingPrice: false, + createdAt: '2026-01-21T20:14:15.000Z', + updatedAt: '2026-01-21T20:14:15.000Z', + }, + ], + }, + ], + }, + { + id: '97-institutional-0x5263D68786AaCfad74B9aa385A004c272548e8B7', + chainId: '97', + protocol: 'institutional-vault', + vaultAddress: '0x5263D68786AaCfad74B9aa385A004c272548e8B7', + underlyingAssetAddress: '0x312e39c7641cE64BEccDe53613f07952258fa810', + fixedApyDecimal: '0.08', + maturityDate: '2026-09-01T00:00:00.000Z', + protocolData: { + collateralAssetAddress: '0xCC3933141a64E26C9317b19CE4BbB4ec2c333bc6', + institutionOperatorAddress: '0x1111111111111111111111111111111111111111', + latePenaltyRateMantissa: '0', + lockDurationSeconds: 2592000, + openDurationSeconds: 604800, + settlementWindowSeconds: 259200, + }, + createdAt: '2026-04-01T00:00:00.000Z', + updatedAt: '2026-04-01T00:00:00.000Z', + loanVaultDetail: { + chainId: '97', + collateralAssetAddress: '0xCC3933141a64E26C9317b19CE4BbB4ec2c333bc6', + collateralValueCents: '0', + createdAt: '2026-04-01T00:00:00.000Z', + debtValueCents: '0', + fixedRateVaultId: '97-institutional-0x5263D68786AaCfad74B9aa385A004c272548e8B7', + id: 'loan-vault-detail-1', + institutionAddress: '0x1111111111111111111111111111111111111111', + latePenaltyRateMantissa: '0', + liquidationIncentiveMantissa: '0', + liquidationThresholdMantissa: '0', + liquidityMantissa: '500000000000', + lockEndTime: '2026-08-29T00:00:00.000Z', + maxBorrowCapMantissa: '1000000000000', + minBorrowCapMantissa: '100000000000', + minSupplierDepositMantissa: '10000000', + openEndTime: '2026-04-08T00:00:00.000Z', + outstandingDebtMantissa: '0', + reserveFactorMantissa: '0', + settlementDeadline: '2026-09-01T00:00:00.000Z', + shortfallMantissa: '0', + supplyAssetAddress: '0x312e39c7641cE64BEccDe53613f07952258fa810', + totalOwedMantissa: '0', + totalRaisedMantissa: '500000000000', + updatedAt: '2026-04-01T00:00:00.000Z', + vaultState: 2, + }, + underlyingToken: [ + { + address: '0x312e39c7641cE64BEccDe53613f07952258fa810', + chainId: '97', + name: 'Mock USDC', + symbol: 'MOCK_USDC', + decimals: 6, + maturityDate: '2026-09-01T00:00:00.000Z', + createdAt: '2026-04-01T00:00:00.000Z', + updatedAt: '2026-04-01T00:00:00.000Z', + tokenPrices: [ + { + id: 'fake-price-institutional', + tokenAddress: '0x312e39c7641cE64BEccDe53613f07952258fa810', + tokenWrappedAddress: null, + chainId: '97', + priceMantissa: '1000000000000000000000000000000', + priceSource: 'oracle', + priceOracleAddress: '0x0000000000000000000000000000000000000001', + mainOracleAddress: '0x0000000000000000000000000000000000000001', + mainOracleName: 'ResilientOracle', + isPriceInvalid: false, + hasErrorFetchingPrice: false, + createdAt: '2026-04-01T00:00:00.000Z', + updatedAt: '2026-04-01T00:00:00.000Z', + }, + ], }, ], }, ]; +export const institutionalVault: InstitutionalVault = { + vaultType: VaultType.Institutional, + category: VaultCategory.STABLECOINS, + manager: VaultManager.Ceffu, + managerIcon: 'ceefu', + managerAddress: '0x1111111111111111111111111111111111111111', + managerLink: 'https://www.ceffu.com', + status: VaultStatus.Pending, + key: '97-institutional-0x5263D68786AaCfad74B9aa385A004c272548e8B7', + stakedToken: usdc, + rewardToken: usdc, + stakedTokenPriceCents: new BigNumber(100), + rewardTokenPriceCents: new BigNumber(100), + stakeAprPercentage: 8, + stakeBalanceMantissa: new BigNumber('500000000000'), + stakeBalanceCents: 50000000, + userStakeBalanceMantissa: new BigNumber('100000000'), + userStakeBalanceCents: 10000, + vaultAddress: '0x5263D68786AaCfad74B9aa385A004c272548e8B7', + reserveFactor: 0, + vaultDeploymentDate: new Date('2026-04-01T00:00:00.000Z'), + openEndDate: new Date('2026-04-08T00:00:00.000Z'), + lockEndDate: new Date('2026-08-29T00:00:00.000Z'), + maturityDate: new Date('2026-09-01T00:00:00.000Z'), + settlementDate: new Date('2026-09-01T00:00:00.000Z'), + stakeLimitMantissa: new BigNumber('1000000000000'), + stakeMinMantissa: new BigNumber('100000000000'), + userMinIndividualStakeMantissa: new BigNumber('10000000'), + userRedeemLimitMantissa: new BigNumber(0), + userWithdrawLimitMantissa: new BigNumber(0), + lockingPeriodMs: 2592000 * 1000, +}; + export const lockedDeposits: LockedDeposit[] = [ { amountMantissa: new BigNumber('1000000000000000000'), diff --git a/apps/evm/src/clients/api/__mocks__/index.ts b/apps/evm/src/clients/api/__mocks__/index.ts index 8903936d3c..1c83f3e6dd 100644 --- a/apps/evm/src/clients/api/__mocks__/index.ts +++ b/apps/evm/src/clients/api/__mocks__/index.ts @@ -1073,3 +1073,45 @@ export const useGetFixedRatedVaults = vi.fn(() => ({ })); export const getFixedRatedVaults = vi.fn(async () => fixedRatedVaults); + +export const useGetFixedRatedVaultUserStakedTokens = vi.fn(() => ({ + data: [], + isLoading: false, +})); + +export const getFixedRatedVaultUserStakedTokens = vi.fn(async () => []); + +export const useGetInstitutionalVaultUserMetrics = vi.fn(() => ({ + data: [], + isLoading: false, +})); + +export const getInstitutionalVaultUserMetrics = vi.fn(async () => []); + +export const useGetInstitutionalVaultUserData = vi.fn(() => ({ + data: [], + isLoading: false, +})); + +export const getInstitutionalVaultUserData = vi.fn(async () => []); + +export const useStakeIntoInstitutionalVault = vi.fn((options?: MutationObserverOptions) => + useMutation({ + mutationFn: vi.fn(), + ...options, + }), +); + +export const useRedeemFromInstitutionalVault = vi.fn((options?: MutationObserverOptions) => + useMutation({ + mutationFn: vi.fn(), + ...options, + }), +); + +export const useWithdrawFromInstitutionalVault = vi.fn((options?: MutationObserverOptions) => + useMutation({ + mutationFn: vi.fn(), + ...options, + }), +); diff --git a/apps/evm/src/clients/api/index.ts b/apps/evm/src/clients/api/index.ts index 41a1dc6470..190d4b6063 100644 --- a/apps/evm/src/clients/api/index.ts +++ b/apps/evm/src/clients/api/index.ts @@ -42,6 +42,9 @@ export * from './mutations/useWithdrawTradePositionCollateral'; export * from './mutations/useRepayWithCollateral'; export * from './mutations/useStakeInPendleVault'; export * from './mutations/useWithdrawFromPendleVault'; +export * from './mutations/useStakeIntoInstitutionalVault'; +export * from './mutations/useRedeemFromInstitutionalVault'; +export * from './mutations/useWithdrawFromInstitutionalVault'; // Queries export * from './queries/getVaiTreasuryPercentage'; export * from './queries/getVaiTreasuryPercentage/useGetVaiTreasuryPercentage'; @@ -259,6 +262,15 @@ export * from './queries/getProposalCount/useGetProposalCount'; export * from './queries/getFixedRatedVaults'; export * from './queries/getFixedRatedVaults/useGetFixedRatedVaults'; +export * from './queries/getInstitutionalVaultUserData'; +export * from './queries/getInstitutionalVaultUserData/useGetInstitutionalVaultUserData'; + +export * from './queries/getFixedRatedVaultUserStakedTokens'; +export * from './queries/getFixedRatedVaultUserStakedTokens/useGetFixedRatedVaultUserStakedTokens'; + +export * from './queries/getInstitutionalVaultUserMetrics'; +export * from './queries/getInstitutionalVaultUserMetrics/useGetInstitutionalVaultUserMetrics'; + export * from './queries/getPendleSwapQuote'; export * from './queries/getPendleSwapQuote/useGetPendleSwapQuote'; diff --git a/apps/evm/src/clients/api/mutations/invalidateInstitutionalVaultQueries/__tests__/index.spec.ts b/apps/evm/src/clients/api/mutations/invalidateInstitutionalVaultQueries/__tests__/index.spec.ts new file mode 100644 index 0000000000..b7e961730f --- /dev/null +++ b/apps/evm/src/clients/api/mutations/invalidateInstitutionalVaultQueries/__tests__/index.spec.ts @@ -0,0 +1,23 @@ +import { queryClient } from 'clients/api/queryClient'; +import FunctionKey from 'constants/functionKey'; +import type { Mock } from 'vitest'; + +import { invalidateInstitutionalVaultQueries } from '..'; + +describe('clients/api/mutations/invalidateInstitutionalVaultQueries', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('invalidates all institutional vault queries including token balances', () => { + invalidateInstitutionalVaultQueries(); + + expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(4); + expect((queryClient.invalidateQueries as Mock).mock.calls).toEqual([ + [{ queryKey: [FunctionKey.GET_BALANCE_OF] }], + [{ queryKey: [FunctionKey.GET_FIXED_RATED_VAULTS] }], + [{ queryKey: [FunctionKey.GET_TOKEN_BALANCES] }], + [{ queryKey: [FunctionKey.GET_INSTITUTIONAL_VAULT_USER_DATA] }], + ]); + }); +}); diff --git a/apps/evm/src/clients/api/mutations/invalidateInstitutionalVaultQueries/index.ts b/apps/evm/src/clients/api/mutations/invalidateInstitutionalVaultQueries/index.ts new file mode 100644 index 0000000000..2ee2ea6072 --- /dev/null +++ b/apps/evm/src/clients/api/mutations/invalidateInstitutionalVaultQueries/index.ts @@ -0,0 +1,13 @@ +import { queryClient } from 'clients/api/queryClient'; +import FunctionKey from 'constants/functionKey'; + +export const invalidateInstitutionalVaultQueries = () => { + queryClient.invalidateQueries({ queryKey: [FunctionKey.GET_BALANCE_OF] }); + queryClient.invalidateQueries({ queryKey: [FunctionKey.GET_FIXED_RATED_VAULTS] }); + queryClient.invalidateQueries({ + queryKey: [FunctionKey.GET_TOKEN_BALANCES], + }); + queryClient.invalidateQueries({ + queryKey: [FunctionKey.GET_INSTITUTIONAL_VAULT_USER_DATA], + }); +}; diff --git a/apps/evm/src/clients/api/mutations/useRedeemFromInstitutionalVault/index.ts b/apps/evm/src/clients/api/mutations/useRedeemFromInstitutionalVault/index.ts new file mode 100644 index 0000000000..20d7cddb4a --- /dev/null +++ b/apps/evm/src/clients/api/mutations/useRedeemFromInstitutionalVault/index.ts @@ -0,0 +1,49 @@ +import type BigNumber from 'bignumber.js'; +import { type UseSendTransactionOptions, useSendTransaction } from 'hooks/useSendTransaction'; +import { useAnalytics } from 'libs/analytics'; +import { institutionalVaultAbi } from 'libs/contracts/abis/institutionalVaultAbi'; +import { VError } from 'libs/errors'; +import { useAccountAddress } from 'libs/wallet'; +import type { Address } from 'viem'; +import { invalidateInstitutionalVaultQueries } from '../invalidateInstitutionalVaultQueries'; + +type RedeemFromInstitutionalVaultInput = { + amountMantissa: BigNumber; +}; + +type Options = UseSendTransactionOptions; + +export const useRedeemFromInstitutionalVault = ( + { vaultAddress }: { vaultAddress: Address }, + options?: Partial, +) => { + const { accountAddress } = useAccountAddress(); + const { captureAnalyticEvent } = useAnalytics(); + + return useSendTransaction({ + fn: ({ amountMantissa }: RedeemFromInstitutionalVaultInput) => { + if (!accountAddress) { + throw new VError({ + type: 'unexpected', + code: 'somethingWentWrong', + }); + } + + return { + abi: institutionalVaultAbi, + address: vaultAddress, + functionName: 'redeem' as const, + args: [BigInt(amountMantissa.toFixed()), accountAddress, accountAddress] as const, + }; + }, + onConfirmed: () => { + captureAnalyticEvent('Institutional vault redeem', { + vaultAddress, + accountAddress, + }); + + invalidateInstitutionalVaultQueries(); + }, + options, + }); +}; diff --git a/apps/evm/src/clients/api/mutations/useStakeIntoInstitutionalVault/index.ts b/apps/evm/src/clients/api/mutations/useStakeIntoInstitutionalVault/index.ts new file mode 100644 index 0000000000..d42278f4a0 --- /dev/null +++ b/apps/evm/src/clients/api/mutations/useStakeIntoInstitutionalVault/index.ts @@ -0,0 +1,49 @@ +import type BigNumber from 'bignumber.js'; +import { type UseSendTransactionOptions, useSendTransaction } from 'hooks/useSendTransaction'; +import { useAnalytics } from 'libs/analytics'; +import { institutionalVaultAbi } from 'libs/contracts/abis/institutionalVaultAbi'; +import { VError } from 'libs/errors'; +import { useAccountAddress } from 'libs/wallet'; +import type { Address } from 'viem'; +import { invalidateInstitutionalVaultQueries } from '../invalidateInstitutionalVaultQueries'; + +type StakeIntoInstitutionalVaultInput = { + amountMantissa: BigNumber; +}; + +type Options = UseSendTransactionOptions; + +export const useStakeIntoInstitutionalVault = ( + { vaultAddress }: { vaultAddress: Address }, + options?: Partial, +) => { + const { accountAddress } = useAccountAddress(); + const { captureAnalyticEvent } = useAnalytics(); + + return useSendTransaction({ + fn: ({ amountMantissa }: StakeIntoInstitutionalVaultInput) => { + if (!accountAddress) { + throw new VError({ + type: 'unexpected', + code: 'somethingWentWrong', + }); + } + + return { + abi: institutionalVaultAbi, + address: vaultAddress, + functionName: 'deposit' as const, + args: [BigInt(amountMantissa.toFixed()), accountAddress] as const, + }; + }, + onConfirmed: () => { + captureAnalyticEvent('Institutional vault deposit', { + vaultAddress, + accountAddress, + }); + + invalidateInstitutionalVaultQueries(); + }, + options, + }); +}; diff --git a/apps/evm/src/clients/api/mutations/useWithdrawFromInstitutionalVault/index.ts b/apps/evm/src/clients/api/mutations/useWithdrawFromInstitutionalVault/index.ts new file mode 100644 index 0000000000..ba6556110e --- /dev/null +++ b/apps/evm/src/clients/api/mutations/useWithdrawFromInstitutionalVault/index.ts @@ -0,0 +1,49 @@ +import type BigNumber from 'bignumber.js'; +import { type UseSendTransactionOptions, useSendTransaction } from 'hooks/useSendTransaction'; +import { useAnalytics } from 'libs/analytics'; +import { institutionalVaultAbi } from 'libs/contracts/abis/institutionalVaultAbi'; +import { VError } from 'libs/errors'; +import { useAccountAddress } from 'libs/wallet'; +import type { Address } from 'viem'; +import { invalidateInstitutionalVaultQueries } from '../invalidateInstitutionalVaultQueries'; + +type WithdrawFromInstitutionalVaultInput = { + amountMantissa: BigNumber; +}; + +type Options = UseSendTransactionOptions; + +export const useWithdrawFromInstitutionalVault = ( + { vaultAddress }: { vaultAddress: Address }, + options?: Partial, +) => { + const { accountAddress } = useAccountAddress(); + const { captureAnalyticEvent } = useAnalytics(); + + return useSendTransaction({ + fn: ({ amountMantissa }: WithdrawFromInstitutionalVaultInput) => { + if (!accountAddress) { + throw new VError({ + type: 'unexpected', + code: 'somethingWentWrong', + }); + } + + return { + abi: institutionalVaultAbi, + address: vaultAddress, + functionName: 'withdraw' as const, + args: [BigInt(amountMantissa.toFixed()), accountAddress, accountAddress] as const, + }; + }, + onConfirmed: () => { + captureAnalyticEvent('Institutional vault withdraw', { + vaultAddress, + accountAddress, + }); + + invalidateInstitutionalVaultQueries(); + }, + options, + }); +}; diff --git a/apps/evm/src/clients/api/queries/getFixedRatedVaultUserStakedTokens/__tests__/index.spec.ts b/apps/evm/src/clients/api/queries/getFixedRatedVaultUserStakedTokens/__tests__/index.spec.ts new file mode 100644 index 0000000000..bc65731557 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getFixedRatedVaultUserStakedTokens/__tests__/index.spec.ts @@ -0,0 +1,65 @@ +import BigNumber from 'bignumber.js'; +import type { PublicClient } from 'viem'; + +import { getFixedRatedVaultUserStakedTokens } from '..'; + +const fakeAccountAddress = '0x3d759121234cd36F8124C21aFe1c6852d2bEd848' as const; +const fakeVaultAddresses = [ + '0x1111111111111111111111111111111111111111', + '0x2222222222222222222222222222222222222222', +] as const; + +describe('getFixedRatedVaultUserStakedTokens', () => { + it('throws when accountAddress is missing', async () => { + await expect( + getFixedRatedVaultUserStakedTokens({ + accountAddress: '' as `0x${string}`, + vaultAddresses: [...fakeVaultAddresses], + publicClient: {} as PublicClient, + }), + ).rejects.toThrow(); + }); + + it('returns multicall balance results', async () => { + const mockPublicClient = { + multicall: vi.fn().mockResolvedValue([ + { status: 'success', result: 500000000000000000n }, + { status: 'success', result: 1000000000000000000n }, + ]), + } as unknown as PublicClient; + + const result = await getFixedRatedVaultUserStakedTokens({ + accountAddress: fakeAccountAddress, + vaultAddresses: [...fakeVaultAddresses], + publicClient: mockPublicClient, + }); + + expect(result).toEqual([ + { + vaultAddress: fakeVaultAddresses[0], + tokensMantissa: new BigNumber('500000000000000000'), + }, + { + vaultAddress: fakeVaultAddresses[1], + tokensMantissa: new BigNumber('1000000000000000000'), + }, + ]); + }); + + it('throws when one multicall balance read fails', async () => { + const mockPublicClient = { + multicall: vi.fn().mockResolvedValue([ + { status: 'success', result: 500000000000000000n }, + { status: 'failure', error: new Error('boom') }, + ]), + } as unknown as PublicClient; + + await expect( + getFixedRatedVaultUserStakedTokens({ + accountAddress: fakeAccountAddress, + vaultAddresses: [...fakeVaultAddresses], + publicClient: mockPublicClient, + }), + ).rejects.toThrow(); + }); +}); diff --git a/apps/evm/src/clients/api/queries/getFixedRatedVaultUserStakedTokens/index.ts b/apps/evm/src/clients/api/queries/getFixedRatedVaultUserStakedTokens/index.ts new file mode 100644 index 0000000000..cc8162af9e --- /dev/null +++ b/apps/evm/src/clients/api/queries/getFixedRatedVaultUserStakedTokens/index.ts @@ -0,0 +1,34 @@ +import type { Address, PublicClient } from 'viem'; + +import type BigNumber from 'bignumber.js'; +import { getInstitutionalVaultUserData } from '../getInstitutionalVaultUserData'; + +export interface GetFixedRatedVaultUserStakedTokensInput { + accountAddress: Address; + vaultAddresses: Address[]; + publicClient: PublicClient; +} + +export type FixedRatedVaultUserStakedAmount = { + vaultAddress: Address; + tokensMantissa: BigNumber; +}; + +export type GetFixedRatedVaultUserStakedTokensOutput = FixedRatedVaultUserStakedAmount[]; + +export const getFixedRatedVaultUserStakedTokens = async ({ + publicClient, + accountAddress, + vaultAddresses, +}: GetFixedRatedVaultUserStakedTokensInput): Promise => { + const userData = await getInstitutionalVaultUserData({ + publicClient, + accountAddress, + vaultAddresses, + }); + + return userData.map(({ vaultAddress, tokensMantissa }) => ({ + vaultAddress, + tokensMantissa, + })); +}; diff --git a/apps/evm/src/clients/api/queries/getFixedRatedVaultUserStakedTokens/useGetFixedRatedVaultUserStakedTokens.ts b/apps/evm/src/clients/api/queries/getFixedRatedVaultUserStakedTokens/useGetFixedRatedVaultUserStakedTokens.ts new file mode 100644 index 0000000000..59c69f6b92 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getFixedRatedVaultUserStakedTokens/useGetFixedRatedVaultUserStakedTokens.ts @@ -0,0 +1,29 @@ +import type { Address } from 'viem'; + +import type { GetFixedRatedVaultUserStakedTokensOutput } from '.'; +import { + type UseGetInstitutionalVaultUserDataOptions, + useGetInstitutionalVaultUserData, +} from '../getInstitutionalVaultUserData/useGetInstitutionalVaultUserData'; + +export interface UseGetFixedRatedVaultUserStakedTokensInput { + vaultAddresses: Address[]; +} + +export const useGetFixedRatedVaultUserStakedTokens = ( + { vaultAddresses }: UseGetFixedRatedVaultUserStakedTokensInput, + options?: Partial< + UseGetInstitutionalVaultUserDataOptions + >, +) => + useGetInstitutionalVaultUserData( + { vaultAddresses }, + { + ...options, + select: userData => + userData.map(({ vaultAddress, tokensMantissa }) => ({ + vaultAddress, + tokensMantissa, + })), + }, + ); diff --git a/apps/evm/src/clients/api/queries/getFixedRatedVaults/__tests__/index.spec.ts b/apps/evm/src/clients/api/queries/getFixedRatedVaults/__tests__/index.spec.ts index 5133771030..7f8843d7cc 100644 --- a/apps/evm/src/clients/api/queries/getFixedRatedVaults/__tests__/index.spec.ts +++ b/apps/evm/src/clients/api/queries/getFixedRatedVaults/__tests__/index.spec.ts @@ -85,6 +85,7 @@ describe('getFixedRatedVaults', () => { method: 'GET', params: { chainId: ChainId.BSC_MAINNET, + includeExpired: true, }, }); }); diff --git a/apps/evm/src/clients/api/queries/getFixedRatedVaults/index.ts b/apps/evm/src/clients/api/queries/getFixedRatedVaults/index.ts index 4698c9ba61..166156ce23 100644 --- a/apps/evm/src/clients/api/queries/getFixedRatedVaults/index.ts +++ b/apps/evm/src/clients/api/queries/getFixedRatedVaults/index.ts @@ -10,12 +10,14 @@ export * from './types'; export const getFixedRatedVaults = async ({ chainId, + includeExpired = true, }: GetFixedRatedVaultsInput): Promise => { const response = await restService({ endpoint: '/fixed-rate-vaults', method: 'GET', params: { chainId, + includeExpired, }, }); diff --git a/apps/evm/src/clients/api/queries/getFixedRatedVaults/types.ts b/apps/evm/src/clients/api/queries/getFixedRatedVaults/types.ts index 8e6220b2d0..1ebafc93f8 100644 --- a/apps/evm/src/clients/api/queries/getFixedRatedVaults/types.ts +++ b/apps/evm/src/clients/api/queries/getFixedRatedVaults/types.ts @@ -3,6 +3,7 @@ import type { Address } from 'viem'; export type GetFixedRatedVaultsInput = { chainId: ChainId; + includeExpired?: boolean; }; type FixedRatedVaultAsset = { @@ -13,7 +14,7 @@ type FixedRatedVaultAsset = { priceUsd: number; }; -type FixedRatedVaultProtocolData = { +export type PendleVaultProtocolData = { startDate: string; ptDiscount: number; ptTokenSymbol: string; @@ -26,6 +27,17 @@ type FixedRatedVaultProtocolData = { pendleMarketAddress: Address; }; +export type InstitutionalVaultProtocolData = { + collateralAssetAddress: Address; + institutionOperatorAddress: Address; + latePenaltyRateMantissa: string; + lockDurationSeconds: number; + openDurationSeconds: number; + settlementWindowSeconds: number; +}; + +type FixedRatedVaultProtocolData = PendleVaultProtocolData | InstitutionalVaultProtocolData; + type FixedRatedVaultUnderlyingToken = { address: Address; chainId: string; @@ -35,6 +47,50 @@ type FixedRatedVaultUnderlyingToken = { maturityDate: string; createdAt: string; updatedAt: string; + tokenPrices?: { + id: string; + tokenAddress: Address; + tokenWrappedAddress: Address | null; + chainId: string; + priceMantissa: string; + priceSource: string; + priceOracleAddress: Address; + mainOracleAddress: Address; + mainOracleName: string; + isPriceInvalid: boolean; + hasErrorFetchingPrice: boolean; + createdAt: string; + updatedAt: string; + }[]; +}; + +export type LoanVaultDetail = { + id: string; + chainId: string; + collateralAssetAddress: Address; + collateralValueCents: string; + createdAt: string; + debtValueCents: string; + fixedRateVaultId: string; + institutionAddress: Address; + latePenaltyRateMantissa: string; + liquidationIncentiveMantissa: string; + liquidationThresholdMantissa: string; + liquidityMantissa: string; + lockEndTime: string; + maxBorrowCapMantissa: string; + minBorrowCapMantissa: string; + minSupplierDepositMantissa: string; + openEndTime: string; + outstandingDebtMantissa: string; + reserveFactorMantissa: string; + settlementDeadline: string; + shortfallMantissa: string; + supplyAssetAddress: Address; + totalOwedMantissa: string; + totalRaisedMantissa: string; + updatedAt: string; + vaultState: number; }; type FixedRatedVault = { @@ -48,6 +104,7 @@ type FixedRatedVault = { protocolData: FixedRatedVaultProtocolData; createdAt: string; updatedAt: string; + loanVaultDetail?: LoanVaultDetail; underlyingToken: FixedRatedVaultUnderlyingToken[]; }; diff --git a/apps/evm/src/clients/api/queries/getFixedRatedVaults/useGetFixedRatedVaults.ts b/apps/evm/src/clients/api/queries/getFixedRatedVaults/useGetFixedRatedVaults.ts index 39a00b1904..1def2f9ae0 100644 --- a/apps/evm/src/clients/api/queries/getFixedRatedVaults/useGetFixedRatedVaults.ts +++ b/apps/evm/src/clients/api/queries/getFixedRatedVaults/useGetFixedRatedVaults.ts @@ -21,7 +21,7 @@ export const useGetFixedRatedVaults = (options?: Partial) => { return useQuery({ queryKey: [FunctionKey.GET_FIXED_RATED_VAULTS, chainId], - queryFn: () => getFixedRatedVaults({ chainId }), + queryFn: () => getFixedRatedVaults({ chainId, includeExpired: true }), ...options, }); }; diff --git a/apps/evm/src/clients/api/queries/getInstitutionalVaultUserData/__tests__/index.spec.ts b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserData/__tests__/index.spec.ts new file mode 100644 index 0000000000..cbb39fd515 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserData/__tests__/index.spec.ts @@ -0,0 +1,77 @@ +import BigNumber from 'bignumber.js'; +import type { PublicClient } from 'viem'; + +import { getInstitutionalVaultUserData } from '..'; + +const fakeAccountAddress = '0x3d759121234cd36F8124C21aFe1c6852d2bEd848' as const; +const fakeVaultAddresses = [ + '0x1111111111111111111111111111111111111111', + '0x2222222222222222222222222222222222222222', +] as const; + +describe('getInstitutionalVaultUserData', () => { + it('throws when accountAddress is missing', async () => { + await expect( + getInstitutionalVaultUserData({ + accountAddress: '' as `0x${string}`, + vaultAddresses: [...fakeVaultAddresses], + publicClient: {} as PublicClient, + }), + ).rejects.toThrow(); + }); + + it('returns multicall user data', async () => { + const mockPublicClient = { + multicall: vi.fn().mockResolvedValue([ + { status: 'success', result: 500000000000000000n }, + { status: 'success', result: 250000000000000000n }, + { status: 'success', result: 300000000000000000n }, + { status: 'success', result: 1000000000000000000n }, + { status: 'success', result: 750000000000000000n }, + { status: 'success', result: 900000000000000000n }, + ]), + } as unknown as PublicClient; + + const result = await getInstitutionalVaultUserData({ + accountAddress: fakeAccountAddress, + vaultAddresses: [...fakeVaultAddresses], + publicClient: mockPublicClient, + }); + + expect(result).toEqual([ + { + vaultAddress: fakeVaultAddresses[0], + tokensMantissa: new BigNumber('500000000000000000'), + maxRedeemAmountMantissa: new BigNumber('250000000000000000'), + maxWithdrawAmountMantissa: new BigNumber('300000000000000000'), + }, + { + vaultAddress: fakeVaultAddresses[1], + tokensMantissa: new BigNumber('1000000000000000000'), + maxRedeemAmountMantissa: new BigNumber('750000000000000000'), + maxWithdrawAmountMantissa: new BigNumber('900000000000000000'), + }, + ]); + }); + + it('throws when one multicall read fails', async () => { + const mockPublicClient = { + multicall: vi.fn().mockResolvedValue([ + { status: 'success', result: 500000000000000000n }, + { status: 'success', result: 250000000000000000n }, + { status: 'success', result: 300000000000000000n }, + { status: 'success', result: 1000000000000000000n }, + { status: 'failure', error: new Error('boom') }, + { status: 'success', result: 900000000000000000n }, + ]), + } as unknown as PublicClient; + + await expect( + getInstitutionalVaultUserData({ + accountAddress: fakeAccountAddress, + vaultAddresses: [...fakeVaultAddresses], + publicClient: mockPublicClient, + }), + ).rejects.toThrow(); + }); +}); diff --git a/apps/evm/src/clients/api/queries/getInstitutionalVaultUserData/index.ts b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserData/index.ts new file mode 100644 index 0000000000..edb16f8706 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserData/index.ts @@ -0,0 +1,92 @@ +import BigNumber from 'bignumber.js'; +import type { Address, PublicClient } from 'viem'; + +import { institutionalVaultAbi } from 'libs/contracts/abis/institutionalVaultAbi'; +import { VError } from 'libs/errors'; + +const READS_PER_VAULT = 3; + +export interface GetInstitutionalVaultUserDataInput { + accountAddress: Address; + vaultAddresses: Address[]; + publicClient: PublicClient; +} + +export type InstitutionalVaultUserData = { + vaultAddress: Address; + tokensMantissa: BigNumber; + maxRedeemAmountMantissa: BigNumber; + maxWithdrawAmountMantissa: BigNumber; +}; + +export type GetInstitutionalVaultUserDataOutput = InstitutionalVaultUserData[]; + +export const getInstitutionalVaultUserData = async ({ + publicClient, + accountAddress, + vaultAddresses, +}: GetInstitutionalVaultUserDataInput): Promise => { + if (!accountAddress) { + throw new VError({ + type: 'unexpected', + code: 'somethingWentWrong', + data: { exception: 'accountAddress is required' }, + }); + } + + const results = await publicClient.multicall({ + contracts: vaultAddresses.flatMap(vaultAddress => [ + { + abi: institutionalVaultAbi, + address: vaultAddress, + functionName: 'balanceOf' as const, + args: [accountAddress], + }, + { + abi: institutionalVaultAbi, + address: vaultAddress, + functionName: 'maxRedeem' as const, + args: [accountAddress], + }, + { + abi: institutionalVaultAbi, + address: vaultAddress, + functionName: 'maxWithdraw' as const, + args: [accountAddress], + }, + ]), + }); + + const failedResult = results.find(result => result.status !== 'success'); + + if (failedResult) { + throw new VError({ + type: 'unexpected', + code: 'somethingWentWrong', + }); + } + + return vaultAddresses.map((vaultAddress, index) => { + const balanceResult = results[index * READS_PER_VAULT]; + const maxRedeemResult = results[index * READS_PER_VAULT + 1]; + const maxWithdrawResult = results[index * READS_PER_VAULT + 2]; + + if ( + balanceResult.status !== 'success' || + maxRedeemResult.status !== 'success' || + maxWithdrawResult.status !== 'success' + ) { + throw new VError({ + type: 'unexpected', + code: 'somethingWentWrong', + }); + } + + return { + vaultAddress, + tokensMantissa: new BigNumber(balanceResult.result.toString()), + maxRedeemAmountMantissa: new BigNumber(maxRedeemResult.result.toString()), + maxWithdrawAmountMantissa: new BigNumber(maxWithdrawResult.result.toString()), + }; + }); +}; diff --git a/apps/evm/src/clients/api/queries/getInstitutionalVaultUserData/useGetInstitutionalVaultUserData.ts b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserData/useGetInstitutionalVaultUserData.ts new file mode 100644 index 0000000000..a76c7ccf55 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserData/useGetInstitutionalVaultUserData.ts @@ -0,0 +1,54 @@ +import { type QueryObserverOptions, useQuery } from '@tanstack/react-query'; +import type { Address } from 'viem'; + +import { NULL_ADDRESS } from 'constants/address'; +import FunctionKey from 'constants/functionKey'; +import { useAccountAddress, useChainId, usePublicClient } from 'libs/wallet'; +import type { ChainId } from 'types'; +import { callOrThrow } from 'utilities'; +import { type GetInstitutionalVaultUserDataOutput, getInstitutionalVaultUserData } from '.'; + +export type UseGetInstitutionalVaultUserDataQueryKey = [ + FunctionKey.GET_INSTITUTIONAL_VAULT_USER_DATA, + { chainId: ChainId; accountAddress: Address; vaultAddresses: Address[] }, +]; + +export type UseGetInstitutionalVaultUserDataOptions< + TSelectData = GetInstitutionalVaultUserDataOutput, +> = QueryObserverOptions< + GetInstitutionalVaultUserDataOutput, + Error, + TSelectData, + GetInstitutionalVaultUserDataOutput, + UseGetInstitutionalVaultUserDataQueryKey +>; + +export interface UseGetInstitutionalVaultUserDataInput { + vaultAddresses: Address[]; +} + +export const useGetInstitutionalVaultUserData = ( + { vaultAddresses }: UseGetInstitutionalVaultUserDataInput, + options?: Partial>, +) => { + const { publicClient } = usePublicClient(); + const { chainId } = useChainId(); + const { accountAddress } = useAccountAddress(); + + return useQuery({ + queryKey: [ + FunctionKey.GET_INSTITUTIONAL_VAULT_USER_DATA, + { chainId, accountAddress: accountAddress ?? NULL_ADDRESS, vaultAddresses }, + ], + queryFn: () => + callOrThrow({ accountAddress }, params => + getInstitutionalVaultUserData({ + publicClient, + ...params, + vaultAddresses, + }), + ), + enabled: vaultAddresses.length > 0 && !!accountAddress, + ...options, + }); +}; diff --git a/apps/evm/src/clients/api/queries/getInstitutionalVaultUserMetrics/__tests__/index.spec.ts b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserMetrics/__tests__/index.spec.ts new file mode 100644 index 0000000000..bb1f267fe6 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserMetrics/__tests__/index.spec.ts @@ -0,0 +1,71 @@ +import BigNumber from 'bignumber.js'; +import type { PublicClient } from 'viem'; + +import { getInstitutionalVaultUserMetrics } from '..'; + +const fakeAccountAddress = '0x3d759121234cd36F8124C21aFe1c6852d2bEd848' as const; +const fakeVaultAddresses = [ + '0x1111111111111111111111111111111111111111', + '0x2222222222222222222222222222222222222222', +] as const; + +describe('getInstitutionalVaultUserMetrics', () => { + it('throws when accountAddress is missing', async () => { + await expect( + getInstitutionalVaultUserMetrics({ + accountAddress: '' as `0x${string}`, + vaultAddresses: [...fakeVaultAddresses], + publicClient: {} as PublicClient, + }), + ).rejects.toThrow(); + }); + + it('returns multicall user metrics', async () => { + const mockPublicClient = { + multicall: vi.fn().mockResolvedValue([ + { status: 'success', result: 500000000000000000n }, + { status: 'success', result: 1000000000000000000n }, + { status: 'success', result: 1500000000000000000n }, + { status: 'success', result: 2000000000000000000n }, + ]), + } as unknown as PublicClient; + + const result = await getInstitutionalVaultUserMetrics({ + accountAddress: fakeAccountAddress, + vaultAddresses: [...fakeVaultAddresses], + publicClient: mockPublicClient, + }); + + expect(result).toEqual([ + { + vaultAddress: fakeVaultAddresses[0], + maxRedeemAmountMantissa: new BigNumber('500000000000000000'), + maxWithdrawAmountMantissa: new BigNumber('1000000000000000000'), + }, + { + vaultAddress: fakeVaultAddresses[1], + maxRedeemAmountMantissa: new BigNumber('1500000000000000000'), + maxWithdrawAmountMantissa: new BigNumber('2000000000000000000'), + }, + ]); + }); + + it('throws when one multicall read fails', async () => { + const mockPublicClient = { + multicall: vi.fn().mockResolvedValue([ + { status: 'success', result: 500000000000000000n }, + { status: 'success', result: 1000000000000000000n }, + { status: 'failure', error: new Error('boom') }, + { status: 'success', result: 2000000000000000000n }, + ]), + } as unknown as PublicClient; + + await expect( + getInstitutionalVaultUserMetrics({ + accountAddress: fakeAccountAddress, + vaultAddresses: [...fakeVaultAddresses], + publicClient: mockPublicClient, + }), + ).rejects.toThrow(); + }); +}); diff --git a/apps/evm/src/clients/api/queries/getInstitutionalVaultUserMetrics/index.ts b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserMetrics/index.ts new file mode 100644 index 0000000000..020b8b420a --- /dev/null +++ b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserMetrics/index.ts @@ -0,0 +1,36 @@ +import type { Address, PublicClient } from 'viem'; + +import type BigNumber from 'bignumber.js'; +import { getInstitutionalVaultUserData } from '../getInstitutionalVaultUserData'; + +export interface GetInstitutionalVaultUserMetricsInput { + accountAddress: Address; + vaultAddresses: Address[]; + publicClient: PublicClient; +} + +export type InstitutionalVaultUserMetric = { + vaultAddress: Address; + maxRedeemAmountMantissa: BigNumber; + maxWithdrawAmountMantissa: BigNumber; +}; + +export type GetInstitutionalVaultUserMetricsOutput = InstitutionalVaultUserMetric[]; + +export const getInstitutionalVaultUserMetrics = async ({ + publicClient, + accountAddress, + vaultAddresses, +}: GetInstitutionalVaultUserMetricsInput): Promise => { + const userData = await getInstitutionalVaultUserData({ + publicClient, + accountAddress, + vaultAddresses, + }); + + return userData.map(({ vaultAddress, maxRedeemAmountMantissa, maxWithdrawAmountMantissa }) => ({ + vaultAddress, + maxRedeemAmountMantissa, + maxWithdrawAmountMantissa, + })); +}; diff --git a/apps/evm/src/clients/api/queries/getInstitutionalVaultUserMetrics/useGetInstitutionalVaultUserMetrics.ts b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserMetrics/useGetInstitutionalVaultUserMetrics.ts new file mode 100644 index 0000000000..48ef416515 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getInstitutionalVaultUserMetrics/useGetInstitutionalVaultUserMetrics.ts @@ -0,0 +1,30 @@ +import type { Address } from 'viem'; + +import type { GetInstitutionalVaultUserMetricsOutput } from '.'; +import { + type UseGetInstitutionalVaultUserDataOptions, + useGetInstitutionalVaultUserData, +} from '../getInstitutionalVaultUserData/useGetInstitutionalVaultUserData'; + +export interface UseGetInstitutionalVaultUserMetricsInput { + vaultAddresses: Address[]; +} + +export const useGetInstitutionalVaultUserMetrics = ( + { vaultAddresses }: UseGetInstitutionalVaultUserMetricsInput, + options?: Partial< + UseGetInstitutionalVaultUserDataOptions + >, +) => + useGetInstitutionalVaultUserData( + { vaultAddresses }, + { + ...options, + select: userData => + userData.map(({ vaultAddress, maxRedeemAmountMantissa, maxWithdrawAmountMantissa }) => ({ + vaultAddress, + maxRedeemAmountMantissa, + maxWithdrawAmountMantissa, + })), + }, + ); diff --git a/apps/evm/src/clients/api/queries/useGetVaults/__tests__/__snapshots__/index.spec.tsx.snap b/apps/evm/src/clients/api/queries/useGetVaults/__tests__/__snapshots__/index.spec.tsx.snap index 8d8a2ae723..14587300e1 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/__tests__/__snapshots__/index.spec.tsx.snap +++ b/apps/evm/src/clients/api/queries/useGetVaults/__tests__/__snapshots__/index.spec.tsx.snap @@ -43,12 +43,105 @@ exports[`useGetVaults > fetches and returns vaults correctly 1`] = ` "maturityDate": "2026-06-25T00:00:00.000Z", "name": null, "symbol": null, + "tokenPrices": [ + { + "chainId": "56", + "createdAt": "2026-01-21T20:14:15.000Z", + "hasErrorFetchingPrice": false, + "id": "fake-price-pendle", + "isPriceInvalid": false, + "mainOracleAddress": "0x0000000000000000000000000000000000000001", + "mainOracleName": "ResilientOracle", + "priceMantissa": "682687557196753800000000000000000000000", + "priceOracleAddress": "0x0000000000000000000000000000000000000001", + "priceSource": "oracle", + "tokenAddress": "0xe052823b4aefc6e230FAf46231A57d0905E30AE0", + "tokenWrappedAddress": null, + "updatedAt": "2026-01-21T20:14:15.000Z", + }, + ], "updatedAt": "2026-01-21T20:14:15.000Z", }, ], "updatedAt": "2026-03-15T15:38:02.000Z", "vaultAddress": "0x6d3BD68E90B42615cb5abF4B8DE92b154ADc435e", }, + { + "chainId": "97", + "createdAt": "2026-04-01T00:00:00.000Z", + "fixedApyDecimal": "0.08", + "id": "97-institutional-0x5263D68786AaCfad74B9aa385A004c272548e8B7", + "loanVaultDetail": { + "chainId": "97", + "collateralAssetAddress": "0xCC3933141a64E26C9317b19CE4BbB4ec2c333bc6", + "collateralValueCents": "0", + "createdAt": "2026-04-01T00:00:00.000Z", + "debtValueCents": "0", + "fixedRateVaultId": "97-institutional-0x5263D68786AaCfad74B9aa385A004c272548e8B7", + "id": "loan-vault-detail-1", + "institutionAddress": "0x1111111111111111111111111111111111111111", + "latePenaltyRateMantissa": "0", + "liquidationIncentiveMantissa": "0", + "liquidationThresholdMantissa": "0", + "liquidityMantissa": "500000000000", + "lockEndTime": "2026-08-29T00:00:00.000Z", + "maxBorrowCapMantissa": "1000000000000", + "minBorrowCapMantissa": "100000000000", + "minSupplierDepositMantissa": "10000000", + "openEndTime": "2026-04-08T00:00:00.000Z", + "outstandingDebtMantissa": "0", + "reserveFactorMantissa": "0", + "settlementDeadline": "2026-09-01T00:00:00.000Z", + "shortfallMantissa": "0", + "supplyAssetAddress": "0x312e39c7641cE64BEccDe53613f07952258fa810", + "totalOwedMantissa": "0", + "totalRaisedMantissa": "500000000000", + "updatedAt": "2026-04-01T00:00:00.000Z", + "vaultState": 2, + }, + "maturityDate": "2026-09-01T00:00:00.000Z", + "protocol": "institutional-vault", + "protocolData": { + "collateralAssetAddress": "0xCC3933141a64E26C9317b19CE4BbB4ec2c333bc6", + "institutionOperatorAddress": "0x1111111111111111111111111111111111111111", + "latePenaltyRateMantissa": "0", + "lockDurationSeconds": 2592000, + "openDurationSeconds": 604800, + "settlementWindowSeconds": 259200, + }, + "underlyingAssetAddress": "0x312e39c7641cE64BEccDe53613f07952258fa810", + "underlyingToken": [ + { + "address": "0x312e39c7641cE64BEccDe53613f07952258fa810", + "chainId": "97", + "createdAt": "2026-04-01T00:00:00.000Z", + "decimals": 6, + "maturityDate": "2026-09-01T00:00:00.000Z", + "name": "Mock USDC", + "symbol": "MOCK_USDC", + "tokenPrices": [ + { + "chainId": "97", + "createdAt": "2026-04-01T00:00:00.000Z", + "hasErrorFetchingPrice": false, + "id": "fake-price-institutional", + "isPriceInvalid": false, + "mainOracleAddress": "0x0000000000000000000000000000000000000001", + "mainOracleName": "ResilientOracle", + "priceMantissa": "1000000000000000000000000000000", + "priceOracleAddress": "0x0000000000000000000000000000000000000001", + "priceSource": "oracle", + "tokenAddress": "0x312e39c7641cE64BEccDe53613f07952258fa810", + "tokenWrappedAddress": null, + "updatedAt": "2026-04-01T00:00:00.000Z", + }, + ], + "updatedAt": "2026-04-01T00:00:00.000Z", + }, + ], + "updatedAt": "2026-04-01T00:00:00.000Z", + "vaultAddress": "0x5263D68786AaCfad74B9aa385A004c272548e8B7", + }, { "category": "stablecoins", "dailyEmissionCents": 14400, @@ -66,6 +159,9 @@ exports[`useGetVaults > fetches and returns vaults correctly 1`] = ` "symbol": "XVS", }, "rewardTokenPriceCents": "100", + "stakeAprPercentage": 12665.060240963856, + "stakeBalanceCents": 41500, + "stakeBalanceMantissa": "415000000000000000000", "stakedToken": { "address": "0x5fFbE5302BadED40941A403228E6AD03f93752d9", "chainId": 97, @@ -74,10 +170,8 @@ exports[`useGetVaults > fetches and returns vaults correctly 1`] = ` "symbol": "VAI", }, "stakedTokenPriceCents": "100", - "stakingAprPercentage": 12665.060240963856, "status": "active", - "totalStakedCents": 41500, - "totalStakedMantissa": "415000000000000000000", + "vaultType": "venus", }, { "category": "governance", @@ -96,6 +190,9 @@ exports[`useGetVaults > fetches and returns vaults correctly 1`] = ` "symbol": "XVS", }, "rewardTokenPriceCents": "100", + "stakeAprPercentage": 12.92281835063781, + "stakeBalanceCents": 40000000000, + "stakeBalanceMantissa": "4e+26", "stakedToken": { "address": "0xB9e0E753630434d7863528cc73CB7AC638a7c8ff", "chainId": 97, @@ -104,12 +201,10 @@ exports[`useGetVaults > fetches and returns vaults correctly 1`] = ` "symbol": "XVS", }, "stakedTokenPriceCents": "100", - "stakingAprPercentage": 12.92281835063781, "status": "active", - "totalStakedCents": 40000000000, - "totalStakedMantissa": "4e+26", - "userStakedCents": 23300, - "userStakedMantissa": "233000000000000000000", + "userStakeBalanceCents": 23300, + "userStakeBalanceMantissa": "233000000000000000000", + "vaultType": "venus", }, { "category": "governance", @@ -127,6 +222,9 @@ exports[`useGetVaults > fetches and returns vaults correctly 1`] = ` "symbol": "XVS", }, "rewardTokenPriceCents": "100", + "stakeAprPercentage": 45625, + "stakeBalanceCents": 4e-7, + "stakeBalanceMantissa": "4000000000", "stakedToken": { "address": "0xB9e0E753630434d7863528cc73CB7AC638a7c8ff", "chainId": 97, @@ -135,12 +233,10 @@ exports[`useGetVaults > fetches and returns vaults correctly 1`] = ` "symbol": "XVS", }, "stakedTokenPriceCents": "100", - "stakingAprPercentage": 45625, "status": "active", - "totalStakedCents": 4e-7, - "totalStakedMantissa": "4000000000", - "userStakedCents": 10000000, - "userStakedMantissa": "1e+23", + "userStakeBalanceCents": 10000000, + "userStakeBalanceMantissa": "1e+23", + "vaultType": "venus", }, ] `; diff --git a/apps/evm/src/clients/api/queries/useGetVaults/__tests__/index.spec.tsx b/apps/evm/src/clients/api/queries/useGetVaults/__tests__/index.spec.tsx index 5c25f6b2c0..da1a5ea30b 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/__tests__/index.spec.tsx +++ b/apps/evm/src/clients/api/queries/useGetVaults/__tests__/index.spec.tsx @@ -31,11 +31,11 @@ const { lockingPeriodMs: _lockingPeriodMs, ...vaiVault } = { key: 'venus-VAI-XVS-0', rewardTokenPriceCents: new BigNumber('100'), stakedTokenPriceCents: new BigNumber('100'), - stakingAprPercentage: 45625, - totalStakedCents: 0.0000004, - totalStakedMantissa: new BigNumber('4000000000'), - userStakedCents: 10000000, - userStakedMantissa: new BigNumber('100000000000000000000000'), + stakeAprPercentage: 45625, + stakeBalanceCents: 0.0000004, + stakeBalanceMantissa: new BigNumber('4000000000'), + userStakeBalanceCents: 10000000, + userStakeBalanceMantissa: new BigNumber('100000000000000000000000'), }; describe('useGetVaults', () => { diff --git a/apps/evm/src/clients/api/queries/useGetVaults/calculateVaultAprPercentage/__tests__/index.spec.ts b/apps/evm/src/clients/api/queries/useGetVaults/calculateVaultAprPercentage/__tests__/index.spec.ts new file mode 100644 index 0000000000..ac1de25a1a --- /dev/null +++ b/apps/evm/src/clients/api/queries/useGetVaults/calculateVaultAprPercentage/__tests__/index.spec.ts @@ -0,0 +1,46 @@ +import BigNumber from 'bignumber.js'; + +import { usdc, vai, xvs } from '__mocks__/models/tokens'; + +import { calculateVaultAprPercentage } from '..'; + +describe('calculateVaultAprPercentage', () => { + it('calculates APR from emission and stake values', () => { + const stakeAprPercentage = calculateVaultAprPercentage({ + dailyEmissionMantissa: new BigNumber('144000000000000000000'), + rewardToken: xvs, + rewardTokenPriceCents: new BigNumber(100), + stakeBalanceMantissa: new BigNumber('415000000000000000000'), + stakedToken: vai, + stakedTokenPriceCents: new BigNumber(100), + }); + + expect(stakeAprPercentage).toBe(12665.060240963856); + }); + + it('normalizes token decimals before comparing reward and stake values', () => { + const stakeAprPercentage = calculateVaultAprPercentage({ + dailyEmissionMantissa: new BigNumber('1000000000000000000'), + rewardToken: xvs, + rewardTokenPriceCents: new BigNumber(2000), + stakeBalanceMantissa: new BigNumber('1000000000'), + stakedToken: usdc, + stakedTokenPriceCents: new BigNumber(100), + }); + + expect(stakeAprPercentage).toBe(730); + }); + + it('returns 0 when the total staked balance is 0', () => { + const stakeAprPercentage = calculateVaultAprPercentage({ + dailyEmissionMantissa: new BigNumber('1000000000000000000'), + rewardToken: xvs, + rewardTokenPriceCents: new BigNumber(2000), + stakeBalanceMantissa: new BigNumber(0), + stakedToken: usdc, + stakedTokenPriceCents: new BigNumber(100), + }); + + expect(stakeAprPercentage).toBe(0); + }); +}); diff --git a/apps/evm/src/clients/api/queries/useGetVaults/calculateVaultAprPercentage/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/calculateVaultAprPercentage/index.ts new file mode 100644 index 0000000000..089ca86db9 --- /dev/null +++ b/apps/evm/src/clients/api/queries/useGetVaults/calculateVaultAprPercentage/index.ts @@ -0,0 +1,44 @@ +import type BigNumber from 'bignumber.js'; + +import { DAYS_PER_YEAR } from 'constants/time'; +import type { Token } from 'types'; +import { convertMantissaToTokens } from 'utilities'; + +export interface CalculateVaultAprPercentageInput { + dailyEmissionMantissa: BigNumber; + rewardToken: Token; + rewardTokenPriceCents: BigNumber; + stakeBalanceMantissa: BigNumber; + stakedToken: Token; + stakedTokenPriceCents: BigNumber; +} + +export const calculateVaultAprPercentage = ({ + dailyEmissionMantissa, + rewardToken, + rewardTokenPriceCents, + stakeBalanceMantissa, + stakedToken, + stakedTokenPriceCents, +}: CalculateVaultAprPercentageInput): number => { + const dailyEmissionTokens = convertMantissaToTokens({ + value: dailyEmissionMantissa, + token: rewardToken, + }); + + const stakeBalanceTokens = convertMantissaToTokens({ + value: stakeBalanceMantissa, + token: stakedToken, + }); + + if (stakeBalanceTokens.lte(0)) { + return 0; + } + + return dailyEmissionTokens + .multipliedBy(rewardTokenPriceCents) + .multipliedBy(DAYS_PER_YEAR) + .multipliedBy(100) + .dividedBy(stakeBalanceTokens.multipliedBy(stakedTokenPriceCents)) + .toNumber(); +}; diff --git a/apps/evm/src/clients/api/queries/useGetVaults/calculateVaultCentsValues/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/calculateVaultCentsValues/index.ts index 0391f71fbb..238f2bb49c 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/calculateVaultCentsValues/index.ts +++ b/apps/evm/src/clients/api/queries/useGetVaults/calculateVaultCentsValues/index.ts @@ -7,26 +7,26 @@ export const calculateVaultCentsValues = ({ rewardTokenDecimals, stakedTokenPriceCents, rewardTokenPriceCents, - totalStakedMantissa, - userStakedMantissa, + stakeBalanceMantissa, + userStakeBalanceMantissa, dailyEmissionMantissa, }: { stakedTokenDecimals: number; rewardTokenDecimals: number; stakedTokenPriceCents: BigNumber; rewardTokenPriceCents: BigNumber; - totalStakedMantissa: BigNumber; - userStakedMantissa?: BigNumber; + stakeBalanceMantissa: BigNumber; + userStakeBalanceMantissa?: BigNumber; dailyEmissionMantissa?: BigNumber; }) => ({ - totalStakedCents: convertPriceMantissaToDollars({ - priceMantissa: totalStakedMantissa.times(stakedTokenPriceCents), + stakeBalanceCents: convertPriceMantissaToDollars({ + priceMantissa: stakeBalanceMantissa.times(stakedTokenPriceCents), decimals: stakedTokenDecimals, }).toNumber(), - userStakedCents: - userStakedMantissa !== undefined + userStakeBalanceCents: + userStakeBalanceMantissa !== undefined ? convertPriceMantissaToDollars({ - priceMantissa: userStakedMantissa.times(stakedTokenPriceCents), + priceMantissa: userStakeBalanceMantissa.times(stakedTokenPriceCents), decimals: stakedTokenDecimals, }).toNumber() : undefined, diff --git a/apps/evm/src/clients/api/queries/useGetVaults/formatToVenusVault/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/formatToVenusVault/index.ts index 9e0ba692fe..8f023bcb6b 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/formatToVenusVault/index.ts +++ b/apps/evm/src/clients/api/queries/useGetVaults/formatToVenusVault/index.ts @@ -1,6 +1,9 @@ -import { VaultCategory, VaultManager, VaultStatus, type VenusVault } from 'types'; +import { VaultCategory, VaultManager, VaultStatus, VaultType, type VenusVault } from 'types'; -export type VaultData = Omit; +export type VaultData = Omit< + VenusVault, + 'key' | 'category' | 'manager' | 'managerIcon' | 'status' | 'vaultType' +>; export const formatToVenusVault = (vault: VaultData): VenusVault => { const venusVault: VenusVault = { @@ -10,6 +13,7 @@ export const formatToVenusVault = (vault: VaultData): VenusVault => { }`, category: vault.stakedToken.symbol === 'XVS' ? VaultCategory.GOVERNANCE : VaultCategory.STABLECOINS, + vaultType: VaultType.Venus, manager: VaultManager.Venus, managerIcon: 'logoMobile' as const, status: diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/__tests__/__snapshots__/index.spec.tsx.snap b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/__tests__/__snapshots__/index.spec.tsx.snap index 5c7e89832c..5af04bbc73 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/__tests__/__snapshots__/index.spec.tsx.snap +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/__tests__/__snapshots__/index.spec.tsx.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`useGetFormattedFixedRatedVaults > fetches and returns pendle vaults correctly 1`] = ` +exports[`useGetFormattedFixedRatedVaults > fetches and returns pendle and ceffu vaults correctly 1`] = ` [ { "asset": { @@ -79,6 +79,9 @@ exports[`useGetFormattedFixedRatedVaults > fetches and returns pendle vaults cor "symbol": "PT-clisBNB-25JUN2026", }, "rewardTokenPriceCents": "65348.52516038151", + "stakeAprPercentage": 3.39809766, + "stakeBalanceCents": 742673002, + "stakeBalanceMantissa": "1.13645e+22", "stakedToken": { "address": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", "chainId": 56, @@ -88,13 +91,56 @@ exports[`useGetFormattedFixedRatedVaults > fetches and returns pendle vaults cor "symbol": "BNB", }, "stakedTokenPriceCents": "66036.349666", - "stakingAprPercentage": 3.39809766, "status": "earning", - "totalStakedCents": 742673002, - "totalStakedMantissa": "1.13645e+22", - "userStakedCents": 326742, - "userStakedMantissa": "5000000000000000000", + "userStakeBalanceCents": 326742, + "userStakeBalanceMantissa": "5000000000000000000", + "vaultAddress": "0x6d3BD68E90B42615cb5abF4B8DE92b154ADc435e", "vaultDeploymentDate": 2025-10-09T09:04:39.000Z, + "vaultType": "pendle", + }, + { + "category": "stablecoins", + "key": "97-institutional-0x5263D68786AaCfad74B9aa385A004c272548e8B7", + "lockEndDate": 2026-08-29T00:00:00.000Z, + "lockingPeriodMs": 2592000000, + "manager": "ceffu", + "managerAddress": "0x1111111111111111111111111111111111111111", + "managerIcon": "ceefu", + "managerLink": "https://www.ceffu.com", + "maturityDate": 2026-09-01T00:00:00.000Z, + "openEndDate": 2026-04-08T00:00:00.000Z, + "reserveFactor": 0, + "rewardToken": { + "address": "0x312e39c7641cE64BEccDe53613f07952258fa810", + "chainId": 97, + "decimals": 6, + "iconSrc": "", + "symbol": "MOCK_USDC", + }, + "rewardTokenPriceCents": "100", + "settlementDate": 2026-09-01T00:00:00.000Z, + "stakeAprPercentage": 8, + "stakeBalanceCents": 50000000, + "stakeBalanceMantissa": "500000000000", + "stakeLimitMantissa": "1000000000000", + "stakeMinMantissa": "100000000000", + "stakedToken": { + "address": "0x312e39c7641cE64BEccDe53613f07952258fa810", + "chainId": 97, + "decimals": 6, + "iconSrc": "", + "symbol": "MOCK_USDC", + }, + "stakedTokenPriceCents": "100", + "status": "pending", + "userMinIndividualStakeMantissa": "10000000", + "userRedeemLimitMantissa": "25000000", + "userStakeBalanceCents": 10000, + "userStakeBalanceMantissa": "100000000", + "userWithdrawLimitMantissa": "50000000", + "vaultAddress": "0x5263D68786AaCfad74B9aa385A004c272548e8B7", + "vaultDeploymentDate": 2026-04-01T00:00:00.000Z, + "vaultType": "institutional", }, ] `; diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/__tests__/index.spec.tsx b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/__tests__/index.spec.tsx index 2f1fc87b5e..e207293b34 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/__tests__/index.spec.tsx +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/__tests__/index.spec.tsx @@ -9,19 +9,20 @@ import { legacyCorePool } from '__mocks__/models/pools'; import { useGetPools } from 'clients/api/queries/useGetPools'; import { useGetTokens } from 'libs/tokens'; import { renderComponent } from 'testUtils/render'; -import type { Asset, Token, VToken } from 'types'; -import { VaultStatus } from 'types'; +import type { Asset, InstitutionalVault, PendleVault, Token, VToken } from 'types'; +import { VaultStatus, VaultType } from 'types'; -import { useGetFixedRatedVaults } from 'clients/api'; +import { useGetFixedRatedVaults, useGetInstitutionalVaultUserData } from 'clients/api'; import type { GetFixedRatedVaultsOutput } from 'clients/api/queries/getFixedRatedVaults/types'; import { type UseGetPendleVaultsOutput, useGetFormattedFixedRatedVaults } from '../index'; vi.mock('clients/api/queries/getFixedRatedVaults/useGetFixedRatedVaults'); +vi.mock('clients/api/queries/getInstitutionalVaultUserData/useGetInstitutionalVaultUserData'); vi.mock('clients/api/queries/useGetPools'); vi.mock('libs/tokens'); // Real API response data from the /fixed-rate-vaults endpoint -const fakeVaultProduct: GetFixedRatedVaultsOutput[number] = { +const fakePendleVaultProduct: GetFixedRatedVaultsOutput[number] = { id: '56-pendle-0x6d3BD68E90B42615cb5abF4B8DE92b154ADc435e', chainId: '56', protocol: 'pendle', @@ -65,6 +66,84 @@ const fakeVaultProduct: GetFixedRatedVaultsOutput[number] = { maturityDate: '2026-06-25T00:00:00.000Z', createdAt: '2026-01-21T20:14:15.000Z', updatedAt: '2026-01-21T20:14:15.000Z', + tokenPrices: [], + }, + ], +}; + +const fakeCeffuVaultProduct: GetFixedRatedVaultsOutput[number] = { + id: '97-institutional-0x5263D68786AaCfad74B9aa385A004c272548e8B7', + chainId: '97', + protocol: 'institutional-vault', + vaultAddress: '0x5263D68786AaCfad74B9aa385A004c272548e8B7', + underlyingAssetAddress: '0x312e39c7641cE64BEccDe53613f07952258fa810', + fixedApyDecimal: '0.08', + maturityDate: '2026-09-01T00:00:00.000Z', + protocolData: { + collateralAssetAddress: '0xCC3933141a64E26C9317b19CE4BbB4ec2c333bc6', + institutionOperatorAddress: '0x1111111111111111111111111111111111111111', + latePenaltyRateMantissa: '0', + lockDurationSeconds: 2592000, + openDurationSeconds: 604800, + settlementWindowSeconds: 259200, + }, + createdAt: '2026-04-01T00:00:00.000Z', + updatedAt: '2026-04-01T00:00:00.000Z', + loanVaultDetail: { + chainId: '97', + collateralAssetAddress: '0xCC3933141a64E26C9317b19CE4BbB4ec2c333bc6', + collateralValueCents: '0', + createdAt: '2026-04-01T00:00:00.000Z', + debtValueCents: '0', + fixedRateVaultId: '97-institutional-0x5263D68786AaCfad74B9aa385A004c272548e8B7', + id: 'loan-vault-detail-1', + institutionAddress: '0x1111111111111111111111111111111111111111', + latePenaltyRateMantissa: '0', + liquidationIncentiveMantissa: '0', + liquidationThresholdMantissa: '0', + liquidityMantissa: '500000000000', + lockEndTime: '2026-08-29T00:00:00.000Z', + maxBorrowCapMantissa: '1000000000000', + minBorrowCapMantissa: '100000000000', + minSupplierDepositMantissa: '10000000', + openEndTime: '2026-04-08T00:00:00.000Z', + outstandingDebtMantissa: '0', + reserveFactorMantissa: '0', + settlementDeadline: '2026-09-01T00:00:00.000Z', + shortfallMantissa: '0', + supplyAssetAddress: '0x312e39c7641cE64BEccDe53613f07952258fa810', + totalOwedMantissa: '0', + totalRaisedMantissa: '500000000000', + updatedAt: '2026-04-01T00:00:00.000Z', + vaultState: 2, + }, + underlyingToken: [ + { + address: '0x312e39c7641cE64BEccDe53613f07952258fa810', + chainId: '97', + name: 'Mock USDC', + symbol: 'MOCK_USDC', + decimals: 6, + maturityDate: '2026-09-01T00:00:00.000Z', + createdAt: '2026-04-01T00:00:00.000Z', + updatedAt: '2026-04-01T00:00:00.000Z', + tokenPrices: [ + { + id: 'fake-price-institutional', + tokenAddress: '0x312e39c7641cE64BEccDe53613f07952258fa810', + tokenWrappedAddress: null, + chainId: '97', + priceMantissa: '1000000000000000000000000000000', + priceSource: 'oracle', + priceOracleAddress: '0x0000000000000000000000000000000000000001', + mainOracleAddress: '0x0000000000000000000000000000000000000001', + mainOracleName: 'ResilientOracle', + isPriceInvalid: false, + hasErrorFetchingPrice: false, + createdAt: '2026-04-01T00:00:00.000Z', + updatedAt: '2026-04-01T00:00:00.000Z', + }, + ], }, ], }; @@ -88,6 +167,14 @@ const bnbToken: Token = { iconSrc: '', }; +const mockUsdcToken: Token = { + chainId: 97, + address: '0x312e39c7641cE64BEccDe53613f07952258fa810', + decimals: 6, + symbol: 'MOCK_USDC', + iconSrc: '', +}; + // vToken matching the vault address const vPtClisbnb: VToken = { chainId: 56, @@ -154,12 +241,14 @@ const fakePoolsData = { ], }; +const fakeVaultProducts = [fakePendleVaultProduct, fakeCeffuVaultProduct]; + describe('useGetFormattedFixedRatedVaults', () => { beforeEach(() => { - (useGetTokens as Mock).mockReturnValue([ptClisbnb, bnbToken]); + (useGetTokens as Mock).mockReturnValue([ptClisbnb, bnbToken, mockUsdcToken]); (useGetFixedRatedVaults as Mock).mockReturnValue({ - data: [fakeVaultProduct], + data: fakeVaultProducts, isLoading: false, }); @@ -167,9 +256,21 @@ describe('useGetFormattedFixedRatedVaults', () => { data: fakePoolsData, isLoading: false, }); + + (useGetInstitutionalVaultUserData as Mock).mockReturnValue({ + data: [ + { + vaultAddress: fakeCeffuVaultProduct.vaultAddress, + tokensMantissa: new BigNumber('100000000'), + maxRedeemAmountMantissa: new BigNumber('25000000'), + maxWithdrawAmountMantissa: new BigNumber('50000000'), + }, + ], + isLoading: false, + }); }); - it('fetches and returns pendle vaults correctly', async () => { + it('fetches and returns pendle and ceffu vaults correctly', async () => { let data: UseGetPendleVaultsOutput['data'] | undefined; let isLoading = false; @@ -183,6 +284,7 @@ describe('useGetFormattedFixedRatedVaults', () => { }); await waitFor(() => expect(!isLoading && data !== undefined).toBe(true)); + expect(data).toHaveLength(2); expect(data).toMatchSnapshot(); }); @@ -234,7 +336,7 @@ describe('useGetFormattedFixedRatedVaults', () => { (useGetFixedRatedVaults as Mock).mockReturnValue({ data: [ { - ...fakeVaultProduct, + ...fakePendleVaultProduct, vaultAddress: '0x0000000000000000000000000000000000000000', }, ], @@ -261,7 +363,7 @@ describe('useGetFormattedFixedRatedVaults', () => { (useGetFixedRatedVaults as Mock).mockReturnValue({ data: [ { - ...fakeVaultProduct, + ...fakePendleVaultProduct, maturityDate: '2020-01-01T00:00:00.000Z', }, ], @@ -280,8 +382,8 @@ describe('useGetFormattedFixedRatedVaults', () => { }); expect(data).toBeDefined(); - expect(data!.length).toBe(1); - expect(data![0].status).toBe(VaultStatus.Claim); + expect(data?.length).toBe(1); + expect(data?.[0].status).toBe(VaultStatus.Claim); }); it('sets status to Earning when user has supply balance and before maturity', () => { @@ -296,9 +398,68 @@ describe('useGetFormattedFixedRatedVaults', () => { accountAddress: fakeAddress, }); - // pendleVaultAsset has userSupplyBalanceCents > 0 and maturity is in the future - expect(data).toBeDefined(); - expect(data!.length).toBe(1); - expect(data![0].status).toBe(VaultStatus.Earning); + const pendleVault = data?.find( + (vault): vault is PendleVault => vault.vaultType === VaultType.Pendle, + ); + + expect(pendleVault).toBeDefined(); + expect(pendleVault?.status).toBe(VaultStatus.Earning); + }); + + it('formats ceffu vault user metrics and status', () => { + let data: UseGetPendleVaultsOutput['data'] | undefined; + + const Wrapper = () => { + ({ data } = useGetFormattedFixedRatedVaults()); + return
; + }; + + renderComponent(, { + accountAddress: fakeAddress, + }); + + const ceffuVault = data?.find( + (vault): vault is InstitutionalVault => vault.vaultType === VaultType.Institutional, + ); + + expect(ceffuVault).toBeDefined(); + expect(ceffuVault?.status).toBe(VaultStatus.Pending); + expect(ceffuVault?.userStakeBalanceMantissa?.toFixed()).toBe('100000000'); + expect(ceffuVault?.userRedeemLimitMantissa.toFixed()).toBe('25000000'); + expect(ceffuVault?.userWithdrawLimitMantissa.toFixed()).toBe('50000000'); + expect(ceffuVault?.userYieldTokens).toBeUndefined(); + }); + + it('derives userYieldTokens from on-chain withdrawable assets for liquidated vaults', () => { + (useGetFixedRatedVaults as Mock).mockReturnValue({ + data: [ + { + ...fakeCeffuVaultProduct, + loanVaultDetail: { + ...fakeCeffuVaultProduct.loanVaultDetail!, + vaultState: 9, + }, + }, + ], + isLoading: false, + }); + + let data: UseGetPendleVaultsOutput['data'] | undefined; + + const Wrapper = () => { + ({ data } = useGetFormattedFixedRatedVaults()); + return
; + }; + + renderComponent(, { + accountAddress: fakeAddress, + }); + + const ceffuVault = data?.find( + (vault): vault is InstitutionalVault => vault.vaultType === VaultType.Institutional, + ); + + expect(ceffuVault?.status).toBe(VaultStatus.Liquidated); + expect(ceffuVault?.userYieldTokens?.toFixed()).toBe('-50'); }); }); diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/getUserYieldTokens/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/getUserYieldTokens/index.ts new file mode 100644 index 0000000000..f8241ab4bf --- /dev/null +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/getUserYieldTokens/index.ts @@ -0,0 +1,30 @@ +import BigNumber from 'bignumber.js'; + +import { VaultStatus } from 'types'; + +export interface GetUserYieldTokensInput { + status: VaultStatus; + userStakedTokens: BigNumber; + userWithdrawTokens: BigNumber; + userWithdrawLimitMantissa: BigNumber; +} + +export const getUserYieldTokens = ({ + status, + userStakedTokens, + userWithdrawTokens, + userWithdrawLimitMantissa, +}: GetUserYieldTokensInput): BigNumber | undefined => { + if (status === VaultStatus.Refund) { + return new BigNumber(0); + } + + if ( + status === VaultStatus.Liquidated || + (status === VaultStatus.Claim && userWithdrawLimitMantissa.gt(0)) + ) { + return userWithdrawTokens.minus(userStakedTokens); + } + + return undefined; +}; diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/getVaultStatus/__tests__/index.spec.ts b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/getVaultStatus/__tests__/index.spec.ts new file mode 100644 index 0000000000..6d7df20feb --- /dev/null +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/getVaultStatus/__tests__/index.spec.ts @@ -0,0 +1,317 @@ +import BigNumber from 'bignumber.js'; +import { VaultStatus } from 'types'; + +import type { LoanVaultDetail } from 'clients/api/queries/getFixedRatedVaults/types'; +import { getVaultStatus } from '..'; + +const baseLoanVaultDetail: LoanVaultDetail = { + chainId: '97', + collateralAssetAddress: '0x1111111111111111111111111111111111111111', + collateralValueCents: '0', + createdAt: '2026-04-01T00:00:00.000Z', + debtValueCents: '0', + fixedRateVaultId: 'vault-1', + id: 'detail-1', + institutionAddress: '0x2222222222222222222222222222222222222222', + latePenaltyRateMantissa: '0', + liquidationIncentiveMantissa: '0', + liquidationThresholdMantissa: '0', + liquidityMantissa: '0', + lockEndTime: '2026-04-10T00:00:00.000Z', + maxBorrowCapMantissa: '0', + minBorrowCapMantissa: '100', + minSupplierDepositMantissa: '10', + openEndTime: '2026-04-05T00:00:00.000Z', + outstandingDebtMantissa: '0', + reserveFactorMantissa: '0', + settlementDeadline: '2026-04-12T00:00:00.000Z', + shortfallMantissa: '0', + supplyAssetAddress: '0x3333333333333333333333333333333333333333', + totalOwedMantissa: '0', + totalRaisedMantissa: '0', + updatedAt: '2026-04-01T00:00:00.000Z', + vaultState: 2, +}; + +const getStatus = ({ + loanVaultDetail = baseLoanVaultDetail, + userRedeemLimitMantissa, + nowMs = new Date('2026-04-01T00:00:00.000Z').getTime(), +}: { + loanVaultDetail?: LoanVaultDetail; + userRedeemLimitMantissa?: BigNumber; + nowMs?: number; +}) => + getVaultStatus({ + loanVaultDetail, + userRedeemLimitMantissa, + nowMs, + }); + +describe('getVaultStatus', () => { + it('returns Pending when the vault is missing an open end time', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + openEndTime: undefined, + } as unknown as LoanVaultDetail, + }), + ).toBe(VaultStatus.Pending); + }); + + it.each([ + { + vaultState: 7, + expectedStatus: VaultStatus.Claim, + }, + { + vaultState: 8, + expectedStatus: VaultStatus.Refund, + }, + { + vaultState: 9, + expectedStatus: VaultStatus.Liquidated, + }, + { + vaultState: 10, + expectedStatus: VaultStatus.Inactive, + }, + ])( + 'returns $expectedStatus for terminal vault state $vaultState before evaluating time-based status', + ({ vaultState, expectedStatus }) => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + totalRaisedMantissa: '100', + vaultState, + }, + nowMs: new Date('2026-04-13T00:00:00.000Z').getTime(), + }), + ).toBe(expectedStatus); + }, + ); + + describe('fundraising', () => { + it('returns Deposit when the vault is still accepting deposits', () => { + expect( + getStatus({ + nowMs: new Date('2026-04-03T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Deposit); + }); + + it('returns Refund when the fundraising window has ended below the minimum raise', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + totalRaisedMantissa: '99', + }, + nowMs: new Date('2026-04-06T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Refund); + }); + + it('returns Pending when fundraising has ended successfully but the vault is still stale onchain', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + totalRaisedMantissa: '100', + }, + nowMs: new Date('2026-04-06T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Pending); + }); + + it('does not return Claim for a stale fundraising vault even after lock end and with no debt', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '0', + }, + nowMs: new Date('2026-04-11T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Pending); + }); + }); + + describe('lock', () => { + it('returns Earning when the lock period is ongoing', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 4, + totalRaisedMantissa: '100', + }, + nowMs: new Date('2026-04-06T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Earning); + }); + + it('returns Claim when the vault is locked without a lock end time and no debt remains', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 4, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '0', + lockEndTime: undefined, + } as unknown as LoanVaultDetail, + nowMs: new Date('2026-04-06T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Claim); + }); + + it('returns Claim at the lock end timestamp when there is no outstanding debt', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 4, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '0', + }, + nowMs: new Date(baseLoanVaultDetail.lockEndTime).getTime(), + }), + ).toBe(VaultStatus.Claim); + }); + + it('returns Repaying after lock end when debt remains', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 4, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '1', + }, + nowMs: new Date('2026-04-11T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Repaying); + }); + + it('returns Claim after lock end when debt remains but the connected user can redeem', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 4, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '1', + }, + userRedeemLimitMantissa: new BigNumber('1'), + nowMs: new Date('2026-04-11T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Claim); + }); + }); + + describe('pending settlement', () => { + it('returns Claim when no debt remains', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 5, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '0', + }, + nowMs: new Date('2026-04-11T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Claim); + }); + + it('returns Repaying when debt remains', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 5, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '1', + }, + nowMs: new Date('2026-04-11T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Repaying); + }); + + it('returns Claim when debt remains but the connected user can redeem', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 5, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '1', + }, + userRedeemLimitMantissa: new BigNumber('1'), + nowMs: new Date('2026-04-11T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Claim); + }); + }); + + describe('settlement deadline exceeded', () => { + it('returns Claim when no debt remains', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 6, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '0', + }, + nowMs: new Date('2026-04-13T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Claim); + }); + + it('returns Repaying when debt remains', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 6, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '1', + }, + nowMs: new Date('2026-04-13T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Repaying); + }); + + it('returns Claim when debt remains but the connected user can redeem', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 6, + totalRaisedMantissa: '100', + outstandingDebtMantissa: '1', + }, + userRedeemLimitMantissa: new BigNumber('1'), + nowMs: new Date('2026-04-13T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Claim); + }); + }); + + it('returns Pending for unmatched states and timestamps', () => { + expect( + getStatus({ + loanVaultDetail: { + ...baseLoanVaultDetail, + vaultState: 1, + }, + nowMs: new Date('2026-04-03T00:00:00.000Z').getTime(), + }), + ).toBe(VaultStatus.Pending); + }); +}); diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/getVaultStatus/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/getVaultStatus/index.ts new file mode 100644 index 0000000000..7fc37ca83f --- /dev/null +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/getVaultStatus/index.ts @@ -0,0 +1,108 @@ +import BigNumber from 'bignumber.js'; +import type { LoanVaultDetail } from 'clients/api'; +import { VaultStatus } from 'types'; + +// Vault states: +// 0 WaitingForMargin +// 1 MarginDeposited +// 2 Fundraising +// 3 InstitutionConfirmation +// 4 Lock +// 5 PendingSettlement +// 6 SettlementDeadlineExceeded +// 7 Matured +// 8 Failed +// 9 Liquidated +// 10 Closed + +export const getVaultStatus = ({ + loanVaultDetail, + userRedeemLimitMantissa, + nowMs, +}: { + loanVaultDetail: LoanVaultDetail; + userRedeemLimitMantissa?: BigNumber; + nowMs: number; +}): VaultStatus => { + const { + vaultState, + openEndTime, + lockEndTime, + totalRaisedMantissa, + minBorrowCapMantissa, + outstandingDebtMantissa, + } = loanVaultDetail; + const openEndMs = openEndTime ? new Date(openEndTime).getTime() : undefined; + const lockEndMs = lockEndTime ? new Date(lockEndTime).getTime() : undefined; + const hasReachedMinBorrowCap = new BigNumber(totalRaisedMantissa).gte(minBorrowCapMantissa); + const hasOutstandingDebt = new BigNumber(outstandingDebtMantissa).gt(0); + const userCanRedeem = !!userRedeemLimitMantissa?.gt(0); + + // Vault has not been opened for fundraising yet, so no user action is available + if (!openEndMs) { + return VaultStatus.Pending; + } + + // Vault has been liquidated, so users can only withdraw the recovered assets + if (vaultState === 9) { + return VaultStatus.Liquidated; + } + + // Vault has been closed by governance and should not accept any user action + if (vaultState === 10) { + return VaultStatus.Inactive; + } + + // Vault has already failed onchain, so users can immediately claim a refund + if (vaultState === 8) { + return VaultStatus.Refund; + } + + // Vault has fully matured onchain, so users can redeem their position immediately + if (vaultState === 7) { + return VaultStatus.Claim; + } + + // Vault missed the settlement deadline: users can claim if debt is cleared, otherwise repayment + // is still pending + if (vaultState === 6) { + return hasOutstandingDebt && !userCanRedeem ? VaultStatus.Repaying : VaultStatus.Claim; + } + + // Vault reached pending settlement: users can claim if debt is cleared, otherwise repayment is + // still pending + if (vaultState === 5) { + return hasOutstandingDebt && !userCanRedeem ? VaultStatus.Repaying : VaultStatus.Claim; + } + + // Vault is locked and still generating yield before maturity + if (vaultState === 4 && lockEndMs && nowMs < lockEndMs) { + return VaultStatus.Earning; + } + + // Vault is locked but the earning period has ended: users can claim if debt is cleared, otherwise + // repayment is pending + if (vaultState === 4) { + return hasOutstandingDebt && !userCanRedeem ? VaultStatus.Repaying : VaultStatus.Claim; + } + + // Vault is still fundraising and the deposit window is open + if (vaultState === 2 && nowMs < openEndMs) { + return VaultStatus.Deposit; + } + + // Vault is stale in fundraising after the deposit window, and the next user tx would push it into + // Failed + if (vaultState === 2 && !hasReachedMinBorrowCap) { + return VaultStatus.Refund; + } + + // Vault is stale in fundraising after a successful raise, but it still must advance onchain + // before users can claim + if (vaultState === 2) { + return VaultStatus.Pending; + } + + // Vault is in a pre-action or unmapped state, so keep the frontend non-actionable + return VaultStatus.Pending; +}; diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/index.ts new file mode 100644 index 0000000000..762ca3025a --- /dev/null +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToCeffuVault/index.ts @@ -0,0 +1,147 @@ +import BigNumber from 'bignumber.js'; +import type { GetFixedRatedVaultsOutput, GetInstitutionalVaultUserDataOutput } from 'clients/api'; +import { type InstitutionalVault, type Token, VaultCategory, VaultManager, VaultType } from 'types'; +import { + convertDollarsToCents, + convertFactorFromSmartContract, + convertMantissaToTokens, + convertPriceMantissaToDollars, + findTokenByAddress, +} from 'utilities'; + +import { getUserYieldTokens } from './getUserYieldTokens'; +import { getVaultStatus } from './getVaultStatus'; + +const CEFFU_MANAGER_LINK = 'https://www.ceffu.com'; + +export const formatToCeffuVault = ({ + vaultData, + tokens, + userData, + nowMs, +}: { + tokens: Token[]; + nowMs: number; + vaultData: GetFixedRatedVaultsOutput[number]; + userData?: GetInstitutionalVaultUserDataOutput[number]; +}): InstitutionalVault | undefined => { + const loanVaultDetail = vaultData.loanVaultDetail; + const stakedToken = findTokenByAddress({ + address: vaultData.underlyingAssetAddress, + tokens, + }); + + if (!stakedToken || !loanVaultDetail) { + return undefined; + } + + const userStakeBalanceMantissa = userData?.tokensMantissa ?? new BigNumber(0); + const stakeBalanceMantissa = new BigNumber(loanVaultDetail.totalRaisedMantissa); + const stakeLimitMantissa = new BigNumber(loanVaultDetail.maxBorrowCapMantissa); + const stakeMinMantissa = new BigNumber(loanVaultDetail.minBorrowCapMantissa); + const userMinIndividualStakeMantissa = loanVaultDetail.minSupplierDepositMantissa + ? new BigNumber(loanVaultDetail.minSupplierDepositMantissa) + : undefined; + const userRedeemLimitMantissa = userData?.maxRedeemAmountMantissa ?? new BigNumber(0); + const userWithdrawLimitMantissa = userData?.maxWithdrawAmountMantissa ?? new BigNumber(0); + let stakedTokenPriceCents = new BigNumber(100); + const priceMantissa = vaultData.underlyingToken[0]?.tokenPrices?.[0]?.priceMantissa; + + if (priceMantissa) { + stakedTokenPriceCents = convertDollarsToCents( + convertPriceMantissaToDollars({ + priceMantissa: new BigNumber(priceMantissa), + decimals: vaultData.underlyingToken[0].decimals, + }), + ); + } + + const userStakedTokens = convertMantissaToTokens({ + value: userStakeBalanceMantissa, + token: stakedToken, + }); + + const stakeBalanceTokens = convertMantissaToTokens({ + value: stakeBalanceMantissa, + token: stakedToken, + }); + + const openEndDate = loanVaultDetail.openEndTime + ? new Date(loanVaultDetail.openEndTime) + : undefined; + + const lockEndDate = loanVaultDetail.lockEndTime + ? new Date(loanVaultDetail.lockEndTime) + : undefined; + + const maturityDate = vaultData.maturityDate ? new Date(vaultData.maturityDate) : undefined; + + const settlementDate = loanVaultDetail.settlementDeadline + ? new Date(loanVaultDetail.settlementDeadline) + : undefined; + + const reserveFactor = convertFactorFromSmartContract({ + factor: new BigNumber(loanVaultDetail.reserveFactorMantissa), + }); + + // We subtract the reserve factor from the APR to show the actual yields received by users + const stakeAprPercentage = new BigNumber(vaultData.fixedApyDecimal) + .multipliedBy(100) + .multipliedBy(new BigNumber(1).minus(reserveFactor)) + .toNumber(); + + const status = getVaultStatus({ + loanVaultDetail, + userRedeemLimitMantissa, + nowMs, + }); + + const userWithdrawTokens = convertMantissaToTokens({ + value: userWithdrawLimitMantissa, + token: stakedToken, + }); + + const userYieldTokens = getUserYieldTokens({ + status, + userStakedTokens, + userWithdrawTokens, + userWithdrawLimitMantissa, + }); + + return { + key: vaultData.id, + vaultType: VaultType.Institutional, + category: VaultCategory.STABLECOINS, + manager: VaultManager.Ceffu, + managerIcon: 'ceefu', + managerAddress: loanVaultDetail.institutionAddress, + managerLink: CEFFU_MANAGER_LINK, + status, + stakedToken, + rewardToken: stakedToken, + stakedTokenPriceCents, + rewardTokenPriceCents: stakedTokenPriceCents, + stakeAprPercentage, + stakeBalanceMantissa, + stakeBalanceCents: stakeBalanceTokens.times(stakedTokenPriceCents).toNumber(), + userStakeBalanceMantissa, + userStakeBalanceCents: userStakedTokens.times(stakedTokenPriceCents).toNumber(), + userMinIndividualStakeMantissa, + vaultAddress: vaultData.vaultAddress, + vaultDeploymentDate: new Date(vaultData.createdAt), + openEndDate, + lockEndDate, + settlementDate, + maturityDate, + stakeLimitMantissa, + stakeMinMantissa, + userRedeemLimitMantissa, + ...(userYieldTokens !== undefined ? { userYieldTokens } : {}), + userWithdrawLimitMantissa, + reserveFactor, + lockingPeriodMs: + 'lockDurationSeconds' in vaultData.protocolData + ? vaultData.protocolData.lockDurationSeconds * 1000 + : undefined, + }; +}; diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToPendleVault/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToPendleVault/index.ts index f896f0af92..5daf101189 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToPendleVault/index.ts +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/formatToPendleVault/index.ts @@ -1,5 +1,5 @@ import BigNumber from 'bignumber.js'; -import type { GetFixedRatedVaultsOutput } from 'clients/api'; +import type { GetFixedRatedVaultsOutput, PendleVaultProtocolData } from 'clients/api'; import { type Asset, type PendleVault, @@ -8,22 +8,23 @@ import { VaultCategory, VaultManager, VaultStatus, + VaultType, } from 'types'; import { areAddressesEqual, convertTokensToMantissa, findTokenByAddress } from 'utilities'; import type { Address } from 'viem'; -export interface BaseInput { - pools: Pool[]; - tokens: Token[]; - nowMs: number; -} - export const formatToPendleVault = ({ vaultData, pools, tokens, nowMs, -}: BaseInput & { vaultData: GetFixedRatedVaultsOutput[number] }) => { +}: { + pools: Pool[]; + tokens: Token[]; + nowMs: number; + vaultData: GetFixedRatedVaultsOutput[number]; +}) => { + const protocolData = vaultData.protocolData as PendleVaultProtocolData; let asset: Asset | undefined; let poolComptrollerContractAddress: Address | undefined; let poolName: string | undefined; @@ -42,7 +43,7 @@ export const formatToPendleVault = ({ } const stakedToken = findTokenByAddress({ - address: vaultData.protocolData?.accountingAsset?.address ?? '', + address: protocolData.accountingAsset.address, tokens, }); @@ -67,32 +68,32 @@ export const formatToPendleVault = ({ const result: PendleVault = { key: vaultData.id, + vaultType: VaultType.Pendle, stakedToken, rewardToken, - stakingAprPercentage: new BigNumber(vaultData.fixedApyDecimal).shiftedBy(2).toNumber(), - userStakedMantissa: convertTokensToMantissa({ + stakeAprPercentage: new BigNumber(vaultData.fixedApyDecimal).shiftedBy(2).toNumber(), + userStakeBalanceMantissa: convertTokensToMantissa({ value: asset.userSupplyBalanceTokens, token: asset.vToken.underlyingToken, }), - totalStakedMantissa: convertTokensToMantissa({ + stakeBalanceMantissa: convertTokensToMantissa({ value: asset.supplyBalanceTokens, token: asset.vToken.underlyingToken, }), - totalStakedCents: asset.supplyBalanceCents.toNumber(), - userStakedCents: asset.userSupplyBalanceCents.toNumber(), - stakedTokenPriceCents: new BigNumber( - vaultData.protocolData?.accountingAsset?.priceUsd, - ).shiftedBy(2), - rewardTokenPriceCents: new BigNumber(vaultData.protocolData.ptTokenPriceUsd).shiftedBy(2), + stakeBalanceCents: asset.supplyBalanceCents.toNumber(), + userStakeBalanceCents: asset.userSupplyBalanceCents.toNumber(), + stakedTokenPriceCents: new BigNumber(protocolData.accountingAsset.priceUsd).shiftedBy(2), + rewardTokenPriceCents: new BigNumber(protocolData.ptTokenPriceUsd).shiftedBy(2), maturityDate, - vaultDeploymentDate: new Date(vaultData.protocolData?.startDate), - liquidityCents: new BigNumber(vaultData.protocolData.liquidityCents), + vaultDeploymentDate: new Date(protocolData.startDate), + liquidityCents: new BigNumber(protocolData.liquidityCents), + vaultAddress: vaultData.vaultAddress, category: VaultCategory.YIELD_TOKENS, manager: VaultManager.Pendle, managerIcon: 'pendle' as const, - managerAddress: vaultData.protocolData.pendleMarketAddress, - managerLink: vaultData.protocolData.pendleMarketAddress - ? `https://app.pendle.finance/trade/pools/${vaultData.protocolData.pendleMarketAddress}/zap/in?chain=bnbchain` + managerAddress: protocolData.pendleMarketAddress, + managerLink: protocolData.pendleMarketAddress + ? `https://app.pendle.finance/trade/pools/${protocolData.pendleMarketAddress}/zap/in?chain=bnbchain` : undefined, status, asset, diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/index.ts index 8e5cfb907e..ee8f58b510 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/index.ts +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/formatVaults/index.ts @@ -1,9 +1,14 @@ -import type { GetFixedRatedVaultsOutput } from 'clients/api'; -import type { Vault } from 'types'; -import { type BaseInput, formatToPendleVault } from './formatToPendleVault'; +import type { GetFixedRatedVaultsOutput, GetInstitutionalVaultUserDataOutput } from 'clients/api'; +import type { Pool, Token, Vault } from 'types'; +import { formatToCeffuVault } from './formatToCeffuVault'; +import { formatToPendleVault } from './formatToPendleVault'; -interface FormatToPendleVaultsInput extends BaseInput { +interface FormatToPendleVaultsInput { + pools: Pool[]; + tokens: Token[]; + nowMs: number; vaultProducts: GetFixedRatedVaultsOutput; + institutionalVaultUserData?: GetInstitutionalVaultUserDataOutput; } export const formatVaults = ({ @@ -11,14 +16,30 @@ export const formatVaults = ({ pools, tokens, nowMs, -}: FormatToPendleVaultsInput): Vault[] => - vaultProducts.reduce((acc, vaultData) => { + institutionalVaultUserData, +}: FormatToPendleVaultsInput): Vault[] => { + const institutionalVaultUserDataByVaultAddress = new Map( + institutionalVaultUserData?.map(userData => [userData.vaultAddress, userData]), + ); + + return vaultProducts.reduce((acc, vaultData) => { let vault: Vault | undefined = undefined; + if (vaultData.protocol === 'pendle') { vault = formatToPendleVault({ vaultData, pools, tokens, nowMs }); + } else if (vaultData.protocol === 'institutional-vault') { + vault = formatToCeffuVault({ + vaultData, + tokens, + nowMs, + userData: institutionalVaultUserDataByVaultAddress.get(vaultData.vaultAddress), + }); } + if (vault) { acc.push(vault); } + return acc; }, []); +}; diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/index.ts index d8bbbc618c..0fcaabbd8e 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/index.ts +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetFormattedFixedRatedVaults/index.ts @@ -3,7 +3,7 @@ import { useGetTokens } from 'libs/tokens'; import { useAccountAddress } from 'libs/wallet'; import type { Vault } from 'types'; -import { useGetFixedRatedVaults } from 'clients/api'; +import { useGetFixedRatedVaults, useGetInstitutionalVaultUserData } from 'clients/api'; import { useGetPools } from '../../useGetPools'; import { formatVaults } from './formatVaults'; @@ -15,13 +15,23 @@ export interface UseGetPendleVaultsOutput { export const useGetFormattedFixedRatedVaults = (): UseGetPendleVaultsOutput => { const { accountAddress } = useAccountAddress(); const { data: vaultProducts, isLoading: isVaultProductsLoading } = useGetFixedRatedVaults(); + const { data: poolsData, isLoading: isPoolsLoading } = useGetPools({ accountAddress }); + const institutionalVaultAddresses = (vaultProducts ?? []) + .filter(vaultProduct => vaultProduct.protocol === 'institutional-vault') + .map(vaultProduct => vaultProduct.vaultAddress); + + const { data: institutionalVaultUserData, isLoading: isInstitutionalVaultUserDataLoading } = + useGetInstitutionalVaultUserData({ + vaultAddresses: institutionalVaultAddresses, + }); + const tokens = useGetTokens(); const now = useNow(); - const isLoading = isVaultProductsLoading || isPoolsLoading; + const isLoading = isVaultProductsLoading || isPoolsLoading || isInstitutionalVaultUserDataLoading; const data = vaultProducts && poolsData?.pools @@ -30,6 +40,7 @@ export const useGetFormattedFixedRatedVaults = (): UseGetPendleVaultsOutput => { pools: poolsData.pools, tokens, nowMs: now.getTime(), + institutionalVaultUserData, }) : undefined; diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetVaiVault/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/useGetVaiVault/index.ts index a639cad942..32f924772b 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/useGetVaiVault/index.ts +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetVaiVault/index.ts @@ -9,15 +9,15 @@ import { useGetVenusVaiVaultDailyRate, } from 'clients/api'; import { NULL_ADDRESS } from 'constants/address'; -import { DAYS_PER_YEAR } from 'constants/time'; import { useGetContractAddress } from 'hooks/useGetContractAddress'; import { useGetToken } from 'libs/tokens'; import { useChainId } from 'libs/wallet'; import type { VenusVault } from 'types'; -import { convertDollarsToCents, convertMantissaToTokens } from 'utilities'; +import { convertDollarsToCents } from 'utilities'; import { checkIsXvsOnZk } from 'utilities/xvsPriceOnZk'; import { XVS_FIXED_PRICE_CENTS } from 'utilities/xvsPriceOnZk/constants'; import type { Address } from 'viem'; +import { calculateVaultAprPercentage } from '../calculateVaultAprPercentage'; import { calculateVaultCentsValues } from '../calculateVaultCentsValues'; import { formatToVenusVault } from '../formatToVenusVault'; @@ -105,31 +105,26 @@ export const useGetVaiVault = ({ const stakedTokenPriceCents = convertDollarsToCents(tokenPricesData[1].tokenPriceUsd); const rewardTokenPriceCents = convertDollarsToCents(xvsPriceDollars); - const stakingAprPercentage = convertMantissaToTokens({ - value: vaiVaultDailyRateData.dailyRateMantissa, - token: xvs, - }) - .multipliedBy(xvsPriceDollars) // We assume 1 VAI = 1 dollar - .multipliedBy(DAYS_PER_YEAR) - .dividedBy( - convertMantissaToTokens({ - value: totalVaiStakedData.balanceMantissa, - token: vai, - }), - ) - .multipliedBy(100) - .toNumber(); - - const { totalStakedCents, userStakedCents, dailyEmissionCents } = calculateVaultCentsValues({ - stakedTokenDecimals: vai.decimals, - rewardTokenDecimals: xvs.decimals, - stakedTokenPriceCents, - rewardTokenPriceCents, - totalStakedMantissa: totalVaiStakedData.balanceMantissa, - userStakedMantissa: vaiVaultUserInfo?.stakedVaiMantissa, + const stakeAprPercentage = calculateVaultAprPercentage({ dailyEmissionMantissa: vaiVaultDailyRateData.dailyRateMantissa, + rewardToken: xvs, + rewardTokenPriceCents, + stakeBalanceMantissa: totalVaiStakedData.balanceMantissa, + stakedToken: vai, + stakedTokenPriceCents, }); + const { stakeBalanceCents, userStakeBalanceCents, dailyEmissionCents } = + calculateVaultCentsValues({ + stakedTokenDecimals: vai.decimals, + rewardTokenDecimals: xvs.decimals, + stakedTokenPriceCents, + rewardTokenPriceCents, + stakeBalanceMantissa: totalVaiStakedData.balanceMantissa, + userStakeBalanceMantissa: vaiVaultUserInfo?.stakedVaiMantissa, + dailyEmissionMantissa: vaiVaultDailyRateData.dailyRateMantissa, + }); + if (dailyEmissionCents === undefined) { return undefined; } @@ -142,11 +137,11 @@ export const useGetVaiVault = ({ rewardTokenPriceCents, dailyEmissionMantissa: vaiVaultDailyRateData.dailyRateMantissa, dailyEmissionCents, - totalStakedMantissa: totalVaiStakedData.balanceMantissa, - totalStakedCents, - stakingAprPercentage, - userStakedMantissa: vaiVaultUserInfo?.stakedVaiMantissa, - userStakedCents, + stakeBalanceMantissa: totalVaiStakedData.balanceMantissa, + stakeBalanceCents, + stakeAprPercentage, + userStakeBalanceMantissa: vaiVaultUserInfo?.stakedVaiMantissa, + userStakeBalanceCents, }); }, [ tokenPricesData, diff --git a/apps/evm/src/clients/api/queries/useGetVaults/useGetVestingVaults/index.ts b/apps/evm/src/clients/api/queries/useGetVaults/useGetVestingVaults/index.ts index bce66d3196..f52231b1b8 100644 --- a/apps/evm/src/clients/api/queries/useGetVaults/useGetVestingVaults/index.ts +++ b/apps/evm/src/clients/api/queries/useGetVaults/useGetVestingVaults/index.ts @@ -12,7 +12,6 @@ import { useGetXvsVaultTotalAllocationPoints, useGetXvsVaultsTotalDailyDistributedXvs, } from 'clients/api'; -import { DAYS_PER_YEAR } from 'constants/time'; import { useGetToken, useGetTokens } from 'libs/tokens'; import type { VenusVault } from 'types'; import { convertDollarsToCents, convertTokensToMantissa, indexBy } from 'utilities'; @@ -23,6 +22,7 @@ import { useChainId } from 'libs/wallet'; import { checkIsXvsOnZk } from 'utilities/xvsPriceOnZk'; import { XVS_FIXED_PRICE_CENTS } from 'utilities/xvsPriceOnZk/constants'; import type { Address } from 'viem'; +import { calculateVaultAprPercentage } from '../calculateVaultAprPercentage'; import { calculateVaultCentsValues } from '../calculateVaultCentsValues'; import { formatToVenusVault } from '../formatToVenusVault'; import { useGetXvsVaultPoolBalances } from './useGetXvsVaultPoolBalances'; @@ -239,7 +239,9 @@ export const useGetVestingVaults = (input?: { Array.from({ length: xvsVaultPoolCountData.poolCount }).reduce( (acc, _item, poolIndex) => { const lockingPeriodMs = poolData[poolIndex]?.poolInfos.lockingPeriodMs; - const userStakedMantissa = poolData[poolIndex]?.userInfos?.stakedAmountMantissa.minus( + const userStakeBalanceMantissa = poolData[ + poolIndex + ]?.userInfos?.stakedAmountMantissa.minus( poolData[poolIndex]?.userInfos?.pendingWithdrawalsTotalAmountMantissa || 0, ); @@ -247,10 +249,10 @@ export const useGetVestingVaults = (input?: { poolData[poolIndex]?.userHasPendingWithdrawalsFromBeforeUpgrade; const pendingWithdrawalsMantissa = poolData[poolIndex]?.pendingWithdrawalsBalanceMantissa; - const totalStakedMantissaData = poolBalances[poolIndex]; + const stakeBalanceMantissaData = poolBalances[poolIndex]; - const totalStakedMantissa = totalStakedMantissaData - ? totalStakedMantissaData.balanceMantissa.minus(pendingWithdrawalsMantissa ?? 0) + const stakeBalanceMantissa = stakeBalanceMantissaData + ? stakeBalanceMantissaData.balanceMantissa.minus(pendingWithdrawalsMantissa ?? 0) : new BigNumber(0); const stakedToken = @@ -280,62 +282,62 @@ export const useGetVestingVaults = (input?: { token: xvs!, }); - const stakingAprPercentage = dailyDistributedXvsMantissa - ?.multipliedBy(DAYS_PER_YEAR) - .div( - totalStakedMantissa.isGreaterThan(0) ? totalStakedMantissa : 1, // Prevent dividing by 0 if balance is 0 - ) - .multipliedBy(100) - .toNumber(); - if ( - !!stakedToken && - lockingPeriodMs !== undefined && - dailyDistributedXvsMantissa !== undefined && - totalStakedMantissaData !== undefined && - stakedTokenPriceCents !== undefined && - xvsPriceCents !== undefined && - stakingAprPercentage !== undefined && - getXvsVaultPausedData?.isVaultPaused !== undefined && - !!xvs + !stakedToken || + lockingPeriodMs === undefined || + dailyDistributedXvsMantissa === undefined || + stakeBalanceMantissaData === undefined || + stakedTokenPriceCents === undefined || + xvsPriceCents === undefined || + getXvsVaultPausedData?.isVaultPaused === undefined || + !xvs ) { - const { totalStakedCents, userStakedCents, dailyEmissionCents } = - calculateVaultCentsValues({ - stakedTokenDecimals: stakedToken.decimals, - rewardTokenDecimals: xvs.decimals, - stakedTokenPriceCents, - rewardTokenPriceCents: xvsPriceCents, - totalStakedMantissa, - userStakedMantissa, - dailyEmissionMantissa: dailyDistributedXvsMantissa, - }); - - if (dailyEmissionCents === undefined) { - return acc; - } - - const vault = formatToVenusVault({ - isPaused: getXvsVaultPausedData.isVaultPaused, - rewardToken: xvs, - stakedToken, + return acc; + } + + const stakeAprPercentage = calculateVaultAprPercentage({ + dailyEmissionMantissa: dailyDistributedXvsMantissa, + rewardToken: xvs, + rewardTokenPriceCents: xvsPriceCents, + stakeBalanceMantissa, + stakedToken, + stakedTokenPriceCents, + }); + + const { stakeBalanceCents, userStakeBalanceCents, dailyEmissionCents } = + calculateVaultCentsValues({ + stakedTokenDecimals: stakedToken.decimals, + rewardTokenDecimals: xvs.decimals, stakedTokenPriceCents, rewardTokenPriceCents: xvsPriceCents, - lockingPeriodMs, + stakeBalanceMantissa, + userStakeBalanceMantissa, dailyEmissionMantissa: dailyDistributedXvsMantissa, - dailyEmissionCents, - totalStakedMantissa, - totalStakedCents, - stakingAprPercentage, - userStakedMantissa, - userStakedCents, - poolIndex, - userHasPendingWithdrawalsFromBeforeUpgrade, }); - return [...acc, vault]; + if (dailyEmissionCents === undefined) { + return acc; } - return acc; + const vault = formatToVenusVault({ + isPaused: getXvsVaultPausedData.isVaultPaused, + rewardToken: xvs, + stakedToken, + stakedTokenPriceCents, + rewardTokenPriceCents: xvsPriceCents, + lockingPeriodMs, + dailyEmissionMantissa: dailyDistributedXvsMantissa, + dailyEmissionCents, + stakeBalanceMantissa, + stakeBalanceCents, + stakeAprPercentage, + userStakeBalanceMantissa, + userStakeBalanceCents, + poolIndex, + userHasPendingWithdrawalsFromBeforeUpgrade, + }); + + return [...acc, vault]; }, [], ), diff --git a/apps/evm/src/components/CapProgressCircle/index.tsx b/apps/evm/src/components/CapProgressCircle/index.tsx new file mode 100644 index 0000000000..e3d0586770 --- /dev/null +++ b/apps/evm/src/components/CapProgressCircle/index.tsx @@ -0,0 +1,123 @@ +import { theme } from '@venusprotocol/ui'; +import type BigNumber from 'bignumber.js'; +import { useId, useMemo } from 'react'; + +import type { Token } from 'types'; +import { + formatCentsToReadableValue, + formatPercentageToReadableValue, + formatTokensToReadableValue, +} from 'utilities'; +import { ProgressCircle } from '../ProgressCircle'; +import { Tooltip, type TooltipProps } from '../Tooltip'; + +export interface CapProgressCircleProps { + token: Token; + title: string; + tokenPriceCents: BigNumber; + tooltip?: TooltipProps['content']; + limitTokens: BigNumber; + valueTokens: BigNumber; +} + +export const CapProgressCircle: React.FC = ({ + title, + tokenPriceCents, + tooltip, + limitTokens, + valueTokens, + token, +}) => { + const gradientId = useId().replace(/:/g, ''); + + const { + readableLimitDollars, + readableLimitTokens, + readableThresholdPercentage, + readableValueDollars, + readableValueTokens, + thresholdPercentage, + } = useMemo(() => { + const valueCents = valueTokens.multipliedBy(tokenPriceCents); + const limitCents = limitTokens.multipliedBy(tokenPriceCents); + + const tmpReadableValueDollars = formatCentsToReadableValue({ + value: valueCents, + }); + + const tmpReadableValueTokens = formatTokensToReadableValue({ + value: valueTokens, + token, + addSymbol: false, + }); + + const tmpReadableLimitTokens = formatTokensToReadableValue({ + value: limitTokens, + token, + }); + + const tmpReadableLimitDollars = formatCentsToReadableValue({ value: limitCents }); + + const thresholdPercentage = limitTokens.isEqualTo(0) + ? 100 + : valueTokens.multipliedBy(100).div(limitTokens).toNumber(); + + const tmpReadableThresholdPercentage = formatPercentageToReadableValue(thresholdPercentage); + + return { + readableLimitDollars: tmpReadableLimitDollars, + readableLimitTokens: tmpReadableLimitTokens, + readableThresholdPercentage: tmpReadableThresholdPercentage, + readableValueDollars: tmpReadableValueDollars, + readableValueTokens: tmpReadableValueTokens, + thresholdPercentage, + }; + }, [limitTokens, tokenPriceCents, token, valueTokens]); + + const progressCircle = ( +
+ + + + + } + /> + +

{readableThresholdPercentage}

+
+ ); + + const content = ( +
+ {tooltip ? {progressCircle} : progressCircle} + +
+

{title}

+ +

+ {readableValueDollars} / {readableLimitDollars} +

+ +

+ {readableValueTokens} / {readableLimitTokens} +

+
+
+ ); + + return content; +}; diff --git a/apps/evm/src/components/Checkbox/styles.ts b/apps/evm/src/components/Checkbox/styles.ts index 0b92df8079..dfabc8d6ed 100644 --- a/apps/evm/src/components/Checkbox/styles.ts +++ b/apps/evm/src/components/Checkbox/styles.ts @@ -8,8 +8,8 @@ export const useStyles = () => { padding: 0; svg { - height: ${theme.spacing(6)}; - width: ${theme.spacing(6)}; + height: ${theme.spacing(5)}; + width: ${theme.spacing(5)}; } :hover { svg { diff --git a/apps/evm/src/components/Icon/icons/checkInlineDotted.tsx b/apps/evm/src/components/Icon/icons/checkInlineDotted.tsx new file mode 100644 index 0000000000..b63b24693c --- /dev/null +++ b/apps/evm/src/components/Icon/icons/checkInlineDotted.tsx @@ -0,0 +1,17 @@ +import type { SVGProps } from 'react'; + +const SvgCheckInlineDotted = (props: SVGProps) => ( + + + + +); + +export default SvgCheckInlineDotted; diff --git a/apps/evm/src/components/Icon/icons/checkInlineEmpty.tsx b/apps/evm/src/components/Icon/icons/checkInlineEmpty.tsx new file mode 100644 index 0000000000..948ecb2a9a --- /dev/null +++ b/apps/evm/src/components/Icon/icons/checkInlineEmpty.tsx @@ -0,0 +1,16 @@ +import type { SVGProps } from 'react'; + +const SvgCheckInlineEmpty = (props: SVGProps) => ( + + + +); + +export default SvgCheckInlineEmpty; diff --git a/apps/evm/src/components/Icon/icons/index.ts b/apps/evm/src/components/Icon/icons/index.ts index f6d18426ac..20481b942e 100644 --- a/apps/evm/src/components/Icon/icons/index.ts +++ b/apps/evm/src/components/Icon/icons/index.ts @@ -34,6 +34,8 @@ export { default as predictions } from './predictions'; export { default as wallet } from './wallet'; export { default as check } from './check'; export { default as checkInline } from './checkInline'; +export { default as checkInlineEmpty } from './checkInlineEmpty'; +export { default as checkInlineDotted } from './checkInlineDotted'; export { default as mark } from './mark'; export { default as arrowShaft } from './arrowShaft'; export { default as notice } from './notice'; diff --git a/apps/evm/src/components/Modal/index.tsx b/apps/evm/src/components/Modal/index.tsx index dd7f5c4d36..3e6f81488a 100644 --- a/apps/evm/src/components/Modal/index.tsx +++ b/apps/evm/src/components/Modal/index.tsx @@ -1,7 +1,6 @@ /** @jsxImportSource @emotion/react */ import { Button, - Drawer, type DrawerProps, Modal as MUIModal, type ModalProps as MUIModalProps, @@ -13,7 +12,6 @@ import type { ReactElement } from 'react'; import config from 'config'; import { cn } from '@venusprotocol/ui'; -import { useIsSmDown } from 'hooks/responsive'; import { Icon } from '../Icon'; import { useModalStyles } from './styles'; @@ -25,7 +23,6 @@ export interface ModalProps extends Omit { handleBackAction?: () => void; title?: string | ReactElement | ReactElement[]; noHorizontalPadding?: boolean; - useDrawerInXs?: boolean; anchor?: DrawerProps['anchor']; } @@ -38,22 +35,15 @@ export const Modal: React.FC = ({ isOpen, title, noHorizontalPadding, - useDrawerInXs, anchor = 'bottom', ...otherModalProps }) => { const s = useModalStyles({ hasTitleComponent: Boolean(title), noHorizontalPadding }); - const isMobile = useIsSmDown(); - - const showDrawer = isMobile && useDrawerInXs; const dom = (
@@ -84,11 +74,7 @@ export const Modal: React.FC = ({
); - return showDrawer ? ( - - {dom} - - ) : ( + return ( e.stopPropagation()} sideOffset={sideOffset} className={cn( - 'flex z-50 overflow-hidden rounded-xl bg-lightGrey p-2 max-w-75 text-sm text-primary-foreground animate-in fade-in-0 zoom-in-95', + 'flex z-50 overflow-hidden rounded-xl bg-background-hover p-2 max-w-75 text-sm text-primary-foreground animate-in fade-in-0 zoom-in-95', 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2', 'data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', className, diff --git a/apps/evm/src/components/index.ts b/apps/evm/src/components/index.ts index ea1be0f741..ad2d84b703 100644 --- a/apps/evm/src/components/index.ts +++ b/apps/evm/src/components/index.ts @@ -4,6 +4,7 @@ export * from './Accordion'; export * from './AccordionAnimatedContent'; export * from './ActiveVotingProgress'; export * from './ButtonGroup'; +export * from './CapProgressCircle'; export * from './Checkbox'; export * from './Chip'; export * from './Countdown'; diff --git a/apps/evm/src/constants/functionKey.ts b/apps/evm/src/constants/functionKey.ts index 02db725581..3837c2442b 100644 --- a/apps/evm/src/constants/functionKey.ts +++ b/apps/evm/src/constants/functionKey.ts @@ -77,6 +77,9 @@ enum FunctionKey { GET_MARKETS_TVL = 'GET_MARKETS_TVL', GET_TOP_MARKETS = 'GET_TOP_MARKETS', GET_FIXED_RATED_VAULTS = 'GET_FIXED_RATED_VAULTS', + GET_INSTITUTIONAL_VAULT_USER_DATA = 'GET_INSTITUTIONAL_VAULT_USER_DATA', + GET_FIXED_RATED_VAULTS_USER_STAKED_TOKENS = 'GET_FIXED_RATED_VAULTS_USER_STAKED_TOKENS', + GET_INSTITUTIONAL_VAULT_USER_METRICS = 'GET_INSTITUTIONAL_VAULT_USER_METRICS', GET_RAW_TRADE_POSITIONS = 'GET_RAW_TRADE_POSITIONS', GET_TOKEN_PAIR_K_LINE_CANDLES = 'GET_TOKEN_PAIR_K_LINE_CANDLES', GET_TRADE_REDUCE_SWAP_QUOTES = 'GET_TRADE_REDUCE_SWAP_QUOTES', diff --git a/apps/evm/src/constants/time.ts b/apps/evm/src/constants/time.ts index dd21aefebd..df8387bd23 100644 --- a/apps/evm/src/constants/time.ts +++ b/apps/evm/src/constants/time.ts @@ -1,7 +1,8 @@ -export const ONE_HOUR_MS = 60 * 60 * 1000; -export const ONE_DAY_MS = ONE_HOUR_MS * 24; - export const MONTHS_PER_YEAR = 12; export const DAYS_PER_YEAR = 365; export const SECONDS_PER_DAY = 60 * 60 * 24; export const SECONDS_PER_YEAR = DAYS_PER_YEAR * SECONDS_PER_DAY; + +export const ONE_HOUR_MS = 60 * 60 * 1000; +export const ONE_DAY_MS = ONE_HOUR_MS * 24; +export const ONE_YEAR_MS = SECONDS_PER_YEAR * 1000; diff --git a/apps/evm/src/containers/PrimeStatusBanner/index.tsx b/apps/evm/src/containers/PrimeStatusBanner/index.tsx index e5e7a43f73..ec522d5f28 100644 --- a/apps/evm/src/containers/PrimeStatusBanner/index.tsx +++ b/apps/evm/src/containers/PrimeStatusBanner/index.tsx @@ -21,10 +21,10 @@ import { clampToZero, formatPercentageToReadableValue, formatTokensToReadableValue, + formatWaitingPeriod, } from 'utilities'; import NoPrimeTokensLeftWarning from './NoPrimeTokensLeftWarning'; import PrimeTokensLeft from './PrimeTokensLeft'; -import { formatWaitingPeriod } from './formatWaitingPeriod'; import TEST_IDS from './testIds'; export interface PrimeStatusBannerUiProps { diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/StrategyDiagram/FlowArrow/index.tsx b/apps/evm/src/containers/VaultCard/FlowArrow/index.tsx similarity index 100% rename from apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/StrategyDiagram/FlowArrow/index.tsx rename to apps/evm/src/containers/VaultCard/FlowArrow/index.tsx diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/StrategyDiagram/FlowNode/index.tsx b/apps/evm/src/containers/VaultCard/FlowNode/index.tsx similarity index 100% rename from apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/StrategyDiagram/FlowNode/index.tsx rename to apps/evm/src/containers/VaultCard/FlowNode/index.tsx diff --git a/apps/evm/src/containers/VaultCard/InstitutionalCheckpointInlineContent/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/InstitutionalCheckpointInlineContent/__tests__/index.spec.tsx new file mode 100644 index 0000000000..9b5c848c4b --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalCheckpointInlineContent/__tests__/index.spec.tsx @@ -0,0 +1,106 @@ +import type { Mock } from 'vitest'; + +import { institutionalVault } from '__mocks__/models/vaults'; +import { useNow } from 'hooks/useNow'; +import { en, t } from 'libs/translations'; +import { renderComponent } from 'testUtils/render'; +import type { InstitutionalVault } from 'types'; +import { VaultStatus } from 'types'; + +import { InstitutionalCheckpointInlineContent } from '..'; + +vi.mock('hooks/useNow'); + +describe('InstitutionalCheckpointInlineContent', () => { + const mockUseNow = useNow as Mock; + + beforeEach(() => { + mockUseNow.mockReturnValue(new Date('2026-04-05T00:00:00.000Z')); + }); + + it('shows the deposit period end for deposit vaults', () => { + const vault = { + ...institutionalVault, + status: VaultStatus.Deposit, + } satisfies InstitutionalVault; + + const { getByText } = renderComponent(); + + expect(getByText(en.vault.modals.depositPeriodEnds)).toBeInTheDocument(); + expect( + getByText(t('vault.timeline.textualWithTime', { date: vault.openEndDate })), + ).toBeInTheDocument(); + }); + + it('shows the deposit period end for pending vaults before the open end date', () => { + const { getByText } = renderComponent( + , + ); + + expect(getByText(en.vault.modals.depositPeriodEnds)).toBeInTheDocument(); + expect( + getByText(t('vault.timeline.textualWithTime', { date: institutionalVault.openEndDate })), + ).toBeInTheDocument(); + }); + + it('shows the deposit period end and tbd when the open end date is unavailable', () => { + const vault = { + ...institutionalVault, + openEndDate: undefined, + settlementDate: undefined, + maturityDate: undefined, + } satisfies InstitutionalVault; + + const { getByText } = renderComponent(); + + expect(getByText(en.vault.modals.depositPeriodEnds)).toBeInTheDocument(); + expect(getByText(en.vault.timeline.tbd)).toBeInTheDocument(); + }); + + it('shows the claim period start for pending vaults after the open end date', () => { + mockUseNow.mockReturnValue(new Date('2026-04-09T00:00:00.000Z')); + + const { getByText } = renderComponent( + , + ); + + expect(getByText(en.vault.modals.maturityDate)).toBeInTheDocument(); + expect( + getByText(t('vault.timeline.textualWithTime', { date: institutionalVault.settlementDate })), + ).toBeInTheDocument(); + }); + + it('shows the refund period for refund vaults', () => { + const vault = { + ...institutionalVault, + status: VaultStatus.Refund, + } satisfies InstitutionalVault; + + const { getByText, queryByText } = renderComponent( + , + ); + + expect(getByText(en.vault.modals.institutionalTimeline.refundPeriod)).toBeInTheDocument(); + expect(queryByText(en.vault.modals.depositPeriodEnds)).not.toBeInTheDocument(); + expect(queryByText(en.vault.modals.maturityDate)).not.toBeInTheDocument(); + expect( + getByText(t('vault.timeline.textualWithTime', { date: vault.openEndDate })), + ).toBeInTheDocument(); + }); + + it('falls back to the maturity date when the settlement date is unavailable', () => { + mockUseNow.mockReturnValue(new Date('2026-04-09T00:00:00.000Z')); + + const vault = { + ...institutionalVault, + settlementDate: undefined, + } satisfies InstitutionalVault; + + const { getByText } = renderComponent(); + + expect(getByText(en.vault.modals.maturityDate)).toBeInTheDocument(); + expect( + getByText(t('vault.timeline.textualWithTime', { date: vault.maturityDate })), + ).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/containers/VaultCard/InstitutionalCheckpointInlineContent/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalCheckpointInlineContent/index.tsx new file mode 100644 index 0000000000..11c7ed522d --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalCheckpointInlineContent/index.tsx @@ -0,0 +1,53 @@ +import { cn } from '@venusprotocol/ui'; +import { isBefore } from 'date-fns/isBefore'; + +import { LabeledInlineContent, type LabeledInlineContentProps } from 'components'; +import { useNow } from 'hooks/useNow'; +import { useTranslation } from 'libs/translations'; +import { type InstitutionalVault, VaultStatus } from 'types'; +import { Timeline } from '../InstitutionalVaultModal/PositionTab/Footer/Timeline'; +import { getCheckpointDescription } from '../InstitutionalVaultModal/useTimeline/getCheckPointDescription'; + +export interface InstitutionalCheckpointInlineContentProps + extends Omit { + vault: InstitutionalVault; +} + +export const InstitutionalCheckpointInlineContent: React.FC< + InstitutionalCheckpointInlineContentProps +> = ({ vault, className, ...otherProps }) => { + const { t } = useTranslation(); + const now = useNow(); + + const isPendingBeforeOpenEndDate = + vault.status === VaultStatus.Pending && !!vault.openEndDate && isBefore(now, vault.openEndDate); + + let label = t('vault.modals.maturityDate'); + let startDate = vault.settlementDate || vault.maturityDate; + + if (vault.status === VaultStatus.Refund) { + label = t('vault.modals.institutionalTimeline.refundPeriod'); + startDate = vault.openEndDate; + } else if ( + vault.status === VaultStatus.Deposit || + !vault.openEndDate || + isPendingBeforeOpenEndDate + ) { + label = t('vault.modals.depositPeriodEnds'); + startDate = vault.openEndDate; + } + + return ( + } + className={cn(className, 'flex-col gap-y-1 xs:flex-row')} + > + {getCheckpointDescription({ + t, + startDate, + })} + + ); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/CampaignTimeline/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/CampaignTimeline/index.tsx new file mode 100644 index 0000000000..76b43cf3fd --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/CampaignTimeline/index.tsx @@ -0,0 +1,25 @@ +import { useTranslation } from 'libs/translations'; +import type { InstitutionalVault } from 'types'; +import { useTimeline } from '../../useTimeline'; + +export const CampaignTimeline: React.FC<{ vault: InstitutionalVault }> = ({ vault }) => { + const { t } = useTranslation(); + + const { checkpoints } = useTimeline({ + vault, + }); + + return ( +
+

{t('vault.modals.overview.campaignTimeline')}

+ + {checkpoints.map(checkpoint => ( +
+

{checkpoint.title}

+ +

{checkpoint.description}

+
+ ))} +
+ ); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/StrategyDiagram/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/StrategyDiagram/index.tsx new file mode 100644 index 0000000000..3de4e9c67f --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/StrategyDiagram/index.tsx @@ -0,0 +1,29 @@ +import { cn } from 'components'; +import { useTranslation } from 'libs/translations'; +import type { InstitutionalVault } from 'types'; +import { FlowArrow } from '../../../FlowArrow'; +import { FlowNode } from '../../../FlowNode'; + +export const StrategyDiagram: React.FC<{ vault: InstitutionalVault }> = ({ vault }) => { + const { t } = useTranslation(); + + return ( +
+

+ {t('vault.modals.overview.strategyAllocation')} +

+ + {t('vault.modals.overview.strategy.users')} + + {t('vault.modals.overview.strategy.venusVault')} + + CEFFU +
+ ); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/TotalDeposits/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/TotalDeposits/index.tsx new file mode 100644 index 0000000000..3e1616c485 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/TotalDeposits/index.tsx @@ -0,0 +1,31 @@ +import { CapProgressCircle } from 'components'; +import { useTranslation } from 'libs/translations'; +import type { InstitutionalVault } from 'types'; +import { convertMantissaToTokens } from 'utilities'; + +export const TotalDeposits: React.FC<{ vault: InstitutionalVault }> = ({ vault }) => { + const { t } = useTranslation(); + + const stakeBalanceTokens = convertMantissaToTokens({ + value: vault.stakeBalanceMantissa, + token: vault.stakedToken, + }); + const maxDepositedTokens = convertMantissaToTokens({ + value: vault.stakeLimitMantissa, + token: vault.stakedToken, + }); + + return ( +
+

{t('vault.modals.overview.totalDeposited')}

+ + +
+ ); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/index.tsx new file mode 100644 index 0000000000..d040989229 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/OverviewTab/index.tsx @@ -0,0 +1,28 @@ +import type { InstitutionalVault } from 'types'; + +import { VaultOverviewMarketInfo } from 'containers/VaultCard/VaultOverviewMarketInfo'; +import { CampaignTimeline } from './CampaignTimeline'; +import { StrategyDiagram } from './StrategyDiagram'; +import { TotalDeposits } from './TotalDeposits'; + +export interface OverviewTabProps { + vault: InstitutionalVault; +} + +export const OverviewTab: React.FC = ({ vault }) => ( +
+ + + + + + + +
+); diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/DepositForm/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/DepositForm/__tests__/index.spec.tsx new file mode 100644 index 0000000000..4af8c013fc --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/DepositForm/__tests__/index.spec.tsx @@ -0,0 +1,170 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import BigNumber from 'bignumber.js'; +import type { Mock } from 'vitest'; + +import fakeAccountAddress from '__mocks__/models/address'; +import { institutionalVault } from '__mocks__/models/vaults'; +import { useGetBalanceOf, useStakeIntoInstitutionalVault } from 'clients/api'; +import { NULL_ADDRESS } from 'constants/address'; +import useTokenApproval from 'hooks/useTokenApproval'; +import { en } from 'libs/translations'; +import { renderComponent } from 'testUtils/render'; +import type { InstitutionalVault } from 'types'; +import { formatTokensToReadableValue } from 'utilities'; + +import { CEFFU_TCS_URL, DepositForm } from '..'; + +const fakeWalletBalanceMantissa = new BigNumber('12000000'); + +const vault = { + ...institutionalVault, + stakeLimitMantissa: new BigNumber('15000000'), + stakeBalanceMantissa: new BigNumber('2000000'), +} satisfies InstitutionalVault; + +const limitedCapacityVault = { + ...vault, + stakeLimitMantissa: new BigNumber('5000000'), + userMinIndividualStakeMantissa: new BigNumber('1000000'), +} satisfies InstitutionalVault; + +const makeUseTokenApprovalOutput = () => ({ + isTokenApproved: true, + isWalletSpendingLimitLoading: false, + isApproveTokenLoading: false, + isRevokeWalletSpendingLimitLoading: false, + walletSpendingLimitTokens: new BigNumber(100), + approveToken: vi.fn(), + revokeWalletSpendingLimit: vi.fn().mockResolvedValue(undefined), +}); + +describe('DepositForm', () => { + beforeEach(() => { + (useGetBalanceOf as Mock).mockReturnValue({ + data: { + balanceMantissa: fakeWalletBalanceMantissa, + }, + isLoading: false, + }); + + (useStakeIntoInstitutionalVault as Mock).mockReturnValue({ + mutateAsync: vi.fn().mockResolvedValue(undefined), + isPending: false, + }); + + (useTokenApproval as Mock).mockReturnValue(makeUseTokenApprovalOutput()); + }); + + it('displays the disconnected state and skips the amount field', async () => { + renderComponent(); + + await waitFor(() => + expect(screen.getByText(en.vault.modals.connectWalletMessage)).toBeInTheDocument(), + ); + expect(screen.queryByRole('spinbutton')).not.toBeInTheDocument(); + + expect(useGetBalanceOf).toHaveBeenCalledWith( + { + accountAddress: NULL_ADDRESS, + token: vault.stakedToken, + }, + { + enabled: false, + }, + ); + }); + + it('caps the available amount by the remaining capacity and renders the institutional copy', async () => { + const expectedLimitTokens = new BigNumber(3); + + renderComponent(, { + accountAddress: fakeAccountAddress, + }); + + expect( + screen.getByText( + /If the vault reaches the minimum required, deposits continue up to the max target\./, + ), + ).toBeInTheDocument(); + expect( + screen.getByText(/By proceeding to deposit, I agree to the institutional vault/i), + ).toBeInTheDocument(); + expect( + screen.getByRole('link', { + name: /terms and conditions/i, + }), + ).toHaveAttribute('href', CEFFU_TCS_URL); + + fireEvent.click( + screen.getByRole('button', { + name: formatTokensToReadableValue({ + value: expectedLimitTokens, + token: vault.stakedToken, + }), + }), + ); + + await waitFor(() => expect(screen.getByRole('spinbutton')).toHaveValue(3)); + }); + + it('submits the deposit amount and closes the modal on success', async () => { + const onClose = vi.fn(); + const deposit = vi.fn().mockResolvedValue(undefined); + const expectedAmountTokens = new BigNumber(12); + const expectedAmountMantissa = new BigNumber('12000000'); + + (useStakeIntoInstitutionalVault as Mock).mockReturnValue({ + mutateAsync: deposit, + isPending: false, + }); + + renderComponent(, { + accountAddress: fakeAccountAddress, + }); + + await waitFor(() => + expect(useStakeIntoInstitutionalVault).toHaveBeenCalledWith({ + vaultAddress: vault.vaultAddress, + }), + ); + + fireEvent.click( + screen.getByRole('button', { + name: formatTokensToReadableValue({ + value: expectedAmountTokens, + token: vault.stakedToken, + }), + }), + ); + + await waitFor(() => + expect( + screen.getByRole('button', { + name: en.vault.modals.deposit, + }), + ).toBeDisabled(), + ); + + fireEvent.click(screen.getByRole('checkbox')); + + await waitFor(() => + expect( + screen.getByRole('button', { + name: en.vault.modals.deposit, + }), + ).toBeEnabled(), + ); + + fireEvent.click( + screen.getByRole('button', { + name: en.vault.modals.deposit, + }), + ); + + await waitFor(() => expect(deposit).toHaveBeenCalledTimes(1)); + expect(deposit).toHaveBeenCalledWith({ + amountMantissa: expectedAmountMantissa, + }); + await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1)); + }); +}); diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/DepositForm/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/DepositForm/index.tsx new file mode 100644 index 0000000000..3e515bd6d0 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/DepositForm/index.tsx @@ -0,0 +1,115 @@ +import BigNumber from 'bignumber.js'; + +import { useGetBalanceOf, useStakeIntoInstitutionalVault } from 'clients/api'; +import { NoticeInfo } from 'components'; +import { NULL_ADDRESS } from 'constants/address'; +import { Link } from 'containers/Link'; +import { TransactionForm } from 'containers/VaultCard/TransactionForm'; +import { useForm } from 'containers/VaultCard/useForm'; +import { useTranslation } from 'libs/translations'; +import { useAccountAddress } from 'libs/wallet'; +import type { InstitutionalVault } from 'types'; +import { convertMantissaToTokens, convertTokensToMantissa, formatWaitingPeriod } from 'utilities'; + +import { Footer } from '../Footer'; + +export const CEFFU_TCS_URL = 'https://www.ceffu.com/legal/terms-of-service'; + +export interface DepositFormProps { + vault: InstitutionalVault; + onClose: () => void; +} + +export const DepositForm: React.FC = ({ vault, onClose }) => { + const { t, Trans, language } = useTranslation(); + const { accountAddress } = useAccountAddress(); + + const { data: walletBalanceData } = useGetBalanceOf( + { + accountAddress: accountAddress || NULL_ADDRESS, + token: vault.stakedToken, + }, + { + enabled: !!accountAddress, + }, + ); + + const walletBalanceTokens = convertMantissaToTokens({ + value: walletBalanceData?.balanceMantissa || new BigNumber(0), + token: vault.stakedToken, + }); + + const remainingCapacityTokens = convertMantissaToTokens({ + value: vault.stakeLimitMantissa.minus(vault.stakeBalanceMantissa), + token: vault.stakedToken, + }); + + const limitFromTokens = BigNumber.min(walletBalanceTokens, remainingCapacityTokens); + + const minFromTokens = + vault.userMinIndividualStakeMantissa && + convertMantissaToTokens({ + value: vault.userMinIndividualStakeMantissa, + token: vault.stakedToken, + }); + + const form = useForm({ + limitFromTokens, + minFromTokens, + }); + + const fromAmountTokens = new BigNumber(form.watch('fromAmountTokens') || 0); + + const readableLockingPeriod = formatWaitingPeriod({ + waitingPeriodSeconds: vault.lockingPeriodMs ? vault.lockingPeriodMs / 1000 : 0, + locale: language.locale, + }); + + const { mutateAsync: stake, isPending: isStakeLoading } = useStakeIntoInstitutionalVault({ + vaultAddress: vault.vaultAddress, + }); + + const handleStake = async () => { + await stake({ + amountMantissa: convertTokensToMantissa({ + value: fromAmountTokens, + token: vault.stakedToken, + }), + }); + + onClose(); + }; + + return ( +
+ } + acknowledgement={ + , + }} + /> + } + /> + + {!!accountAddress && ( + + )} +
+ ); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/Timeline/Checkpoint/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/Timeline/Checkpoint/index.tsx new file mode 100644 index 0000000000..8ecea7aef3 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/Timeline/Checkpoint/index.tsx @@ -0,0 +1,47 @@ +import { cn } from '@venusprotocol/ui'; + +import { Icon, type IconName } from 'components'; +import type { TimelineCheckpoint } from 'containers/VaultCard/InstitutionalVaultModal/types'; + +export interface CheckpointProps { + checkpoint: TimelineCheckpoint; +} + +export const Checkpoint: React.FC = ({ checkpoint }) => { + let iconName: IconName = 'checkInlineEmpty'; + let iconColorClass = cn('text-grey'); + + if (checkpoint.status === 'passed') { + iconName = 'checkInline'; + iconColorClass = cn('text-green'); + } else if (checkpoint.status === 'ongoing') { + iconName = 'checkInlineDotted'; + iconColorClass = cn('text-blue'); + } + + return ( +
+
+ + +

+ {checkpoint.title} +

+
+ +

+ {checkpoint.description} +

+
+ ); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/Timeline/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/Timeline/index.tsx new file mode 100644 index 0000000000..51db8c71a1 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/Timeline/index.tsx @@ -0,0 +1,19 @@ +import { useTimeline } from 'containers/VaultCard/InstitutionalVaultModal/useTimeline'; +import type { InstitutionalVault } from 'types'; +import { Checkpoint } from './Checkpoint'; + +export interface TimelineProps { + vault: InstitutionalVault; +} + +export const Timeline: React.FC = ({ vault }) => { + const { checkpoints } = useTimeline({ vault }); + + return ( +
+ {checkpoints.map(checkpoint => ( + + ))} +
+ ); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/__tests__/index.spec.tsx new file mode 100644 index 0000000000..fe9e6838c1 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/__tests__/index.spec.tsx @@ -0,0 +1,335 @@ +import BigNumber from 'bignumber.js'; +import type { Mock } from 'vitest'; + +import fakeAccountAddress from '__mocks__/models/address'; +import { usdc } from '__mocks__/models/tokens'; +import { useNow } from 'hooks/useNow'; +import { en, t } from 'libs/translations'; +import { renderComponent } from 'testUtils/render'; +import type { InstitutionalVault } from 'types'; +import { VaultCategory, VaultManager, VaultStatus, VaultType } from 'types'; +import { + convertMantissaToTokens, + formatPercentageToReadableValue, + formatTokensToReadableValue, +} from 'utilities'; +import { Footer, type FooterProps } from '..'; +import { getProjectedYieldTokens } from '../getProjectedYieldTokens'; + +vi.mock('hooks/useNow'); + +const lockingPeriodMs = 2592000 * 1000; + +const baseVault = { + vaultType: VaultType.Institutional, + category: VaultCategory.STABLECOINS, + manager: VaultManager.Ceffu, + managerIcon: 'ceefu', + status: VaultStatus.Deposit, + key: 'institutional-usdc', + stakedToken: usdc, + rewardToken: usdc, + stakedTokenPriceCents: new BigNumber(100), + rewardTokenPriceCents: new BigNumber(100), + stakeAprPercentage: 8, + stakeBalanceMantissa: new BigNumber('500000000'), + stakeBalanceCents: 50000, + userStakeBalanceMantissa: new BigNumber('100000000'), + userStakeBalanceCents: 10000, + vaultAddress: '0x5263D68786AaCfad74B9aa385A004c272548e8B7', + reserveFactor: 0, + vaultDeploymentDate: new Date('2026-04-01T00:00:00.000Z'), + openEndDate: new Date('2026-04-08T00:00:00.000Z'), + lockEndDate: new Date('2026-08-29T00:00:00.000Z'), + maturityDate: new Date('2026-09-01T00:00:00.000Z'), + settlementDate: new Date('2026-09-01T00:00:00.000Z'), + stakeLimitMantissa: new BigNumber('1000000000'), + stakeMinMantissa: new BigNumber('10000000'), + userMinIndividualStakeMantissa: new BigNumber('10000000'), + userRedeemLimitMantissa: new BigNumber(0), + userWithdrawLimitMantissa: new BigNumber(0), + lockingPeriodMs, +} satisfies InstitutionalVault; + +const baseProps: FooterProps = { + vault: baseVault, +}; + +const getRow = (getByText: (text: string) => HTMLElement, label: string) => + getByText(label).parentElement?.parentElement; + +describe('InstitutionalVaultModal Footer', () => { + const mockUseNow = useNow as Mock; + + beforeEach(() => { + mockUseNow.mockReturnValue(new Date('2026-04-05T00:00:00.000Z')); + }); + + it('renders only the APR and deposit period rows when the wallet is disconnected', () => { + const { getByText, queryByText } = renderComponent(
); + + expect(getByText(en.vault.modals.effectiveFixedApr)).toBeInTheDocument(); + expect( + getByText(formatPercentageToReadableValue(baseVault.stakeAprPercentage)), + ).toBeInTheDocument(); + expect(getByText(en.vault.modals.depositPeriodEnds)).toBeInTheDocument(); + expect( + getByText(t('vault.timeline.textualWithTime', { date: baseVault.openEndDate })), + ).toBeInTheDocument(); + + expect(queryByText(en.vault.modals.currentDeposited)).not.toBeInTheDocument(); + expect(queryByText(en.vault.modals.totalYields)).not.toBeInTheDocument(); + }); + + it('renders the current deposited and total yields rows for connected accounts', () => { + const { getByText } = renderComponent(
, { + accountAddress: fakeAccountAddress, + }); + + const currentDepositedRow = getRow(getByText, en.vault.modals.currentDeposited); + const totalYieldsRow = getRow(getByText, en.vault.modals.totalYields); + + expect(currentDepositedRow).toHaveTextContent( + formatTokensToReadableValue({ + value: convertMantissaToTokens({ + value: baseVault.userStakeBalanceMantissa, + token: baseVault.stakedToken, + }), + token: baseVault.stakedToken, + }), + ); + expect(totalYieldsRow).toHaveTextContent( + formatTokensToReadableValue({ + value: getProjectedYieldTokens({ + depositTokens: new BigNumber(100), + stakeAprPercentage: baseVault.stakeAprPercentage, + lockingPeriodMs, + }), + token: baseVault.stakedToken, + }), + ); + }); + + it('updates total yields based on the current and hypothetical deposited amounts', () => { + const fromAmountTokens = new BigNumber(50); + const currentDepositTokens = new BigNumber(100); + const updatedDepositTokens = currentDepositTokens.plus(fromAmountTokens); + + const { getByText } = renderComponent( +
, + { + accountAddress: fakeAccountAddress, + }, + ); + + const currentDepositedRow = getRow(getByText, en.vault.modals.currentDeposited); + const totalYieldsRow = getRow(getByText, en.vault.modals.totalYields); + + expect(currentDepositedRow).toHaveTextContent( + formatTokensToReadableValue({ + value: currentDepositTokens, + token: baseVault.stakedToken, + }), + ); + expect(currentDepositedRow).toHaveTextContent( + formatTokensToReadableValue({ + value: updatedDepositTokens, + token: baseVault.stakedToken, + }), + ); + expect(totalYieldsRow).toHaveTextContent( + formatTokensToReadableValue({ + value: getProjectedYieldTokens({ + depositTokens: currentDepositTokens, + stakeAprPercentage: baseVault.stakeAprPercentage, + lockingPeriodMs, + }), + token: baseVault.stakedToken, + }), + ); + expect(totalYieldsRow).toHaveTextContent( + formatTokensToReadableValue({ + value: getProjectedYieldTokens({ + depositTokens: updatedDepositTokens, + stakeAprPercentage: baseVault.stakeAprPercentage, + lockingPeriodMs, + }), + token: baseVault.stakedToken, + }), + ); + }); + + it('prefers userYieldTokens over the projected yield when available', () => { + const { getByText } = renderComponent( +
, + { + accountAddress: fakeAccountAddress, + }, + ); + + const totalYieldsRow = getRow(getByText, en.vault.modals.totalYields); + + expect(totalYieldsRow).toHaveTextContent( + formatTokensToReadableValue({ + value: new BigNumber(-40), + token: baseVault.stakedToken, + }), + ); + }); + + it('omits hypothetical updates when the entered amount is zero', () => { + const currentDepositTokens = new BigNumber(100); + const updatedDepositTokens = currentDepositTokens.plus(50); + + const { getByText, queryByText } = renderComponent( +
, + { + accountAddress: fakeAccountAddress, + }, + ); + + const currentDepositedRow = getRow(getByText, en.vault.modals.currentDeposited); + const totalYieldsRow = getRow(getByText, en.vault.modals.totalYields); + + expect(currentDepositedRow).toHaveTextContent( + formatTokensToReadableValue({ + value: currentDepositTokens, + token: baseVault.stakedToken, + }), + ); + expect(totalYieldsRow).toHaveTextContent( + formatTokensToReadableValue({ + value: getProjectedYieldTokens({ + depositTokens: currentDepositTokens, + stakeAprPercentage: baseVault.stakeAprPercentage, + lockingPeriodMs, + }), + token: baseVault.stakedToken, + }), + ); + expect( + queryByText( + formatTokensToReadableValue({ + value: updatedDepositTokens, + token: baseVault.stakedToken, + }), + ), + ).not.toBeInTheDocument(); + expect( + queryByText( + formatTokensToReadableValue({ + value: getProjectedYieldTokens({ + depositTokens: updatedDepositTokens, + stakeAprPercentage: baseVault.stakeAprPercentage, + lockingPeriodMs, + }), + token: baseVault.stakedToken, + }), + ), + ).not.toBeInTheDocument(); + }); + + it('does not render total yields when the locking period is unavailable', () => { + const { queryByText } = renderComponent( +
, + { + accountAddress: fakeAccountAddress, + }, + ); + + expect(queryByText(en.vault.modals.totalYields)).not.toBeInTheDocument(); + }); + + it('falls back to zero projected yield for refund vaults when userYieldTokens is unavailable', () => { + const { getByText } = renderComponent( +
, + { + accountAddress: fakeAccountAddress, + }, + ); + + const totalYieldsRow = getRow(getByText, en.vault.modals.totalYields); + + expect(totalYieldsRow).toHaveTextContent( + formatTokensToReadableValue({ + value: new BigNumber(0), + token: baseVault.stakedToken, + }), + ); + }); + + it('renders the deposit period end row for pending vaults before deposits close', () => { + const openEndDate = new Date('2026-04-10T00:00:00.000Z'); + + const { getByText, queryByText } = renderComponent( +
, + ); + + expect(getByText(en.vault.modals.depositPeriodEnds)).toBeInTheDocument(); + expect( + getByText(t('vault.timeline.textualWithTime', { date: openEndDate })), + ).toBeInTheDocument(); + expect(queryByText(en.vault.modals.maturityDate)).not.toBeInTheDocument(); + }); + + it('renders a TBD deposit period end when the open end date is unavailable', () => { + const { getByText, queryByText } = renderComponent( +
, + ); + + expect(getByText(en.vault.modals.depositPeriodEnds)).toBeInTheDocument(); + expect(getByText(t('vault.timeline.tbd'))).toBeInTheDocument(); + expect(queryByText(en.vault.modals.maturityDate)).not.toBeInTheDocument(); + }); + + it('renders the maturity date row after the deposit period ends', () => { + const { getByText, queryByText } = renderComponent( +
, + ); + + expect(getByText(en.vault.modals.maturityDate)).toBeInTheDocument(); + expect( + getByText(t('vault.timeline.textualWithTime', { date: baseVault.maturityDate })), + ).toBeInTheDocument(); + expect(queryByText(en.vault.modals.depositPeriodEnds)).not.toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/getProjectedYieldTokens/index.ts b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/getProjectedYieldTokens/index.ts new file mode 100644 index 0000000000..be2f3ed01d --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/getProjectedYieldTokens/index.ts @@ -0,0 +1,13 @@ +import type BigNumber from 'bignumber.js'; + +import { ONE_YEAR_MS } from 'constants/time'; + +export const getProjectedYieldTokens = ({ + depositTokens, + stakeAprPercentage, + lockingPeriodMs, +}: { + depositTokens: BigNumber; + stakeAprPercentage: number; + lockingPeriodMs: number; +}) => depositTokens.times(stakeAprPercentage).div(100).times(lockingPeriodMs).div(ONE_YEAR_MS); diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/index.tsx new file mode 100644 index 0000000000..32ca815e6b --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/Footer/index.tsx @@ -0,0 +1,138 @@ +import BigNumber from 'bignumber.js'; +import type { ReactNode } from 'react'; +import { Fragment } from 'react/jsx-runtime'; + +import { Delimiter, LabeledInlineContent, ValueUpdate } from 'components'; +import { InstitutionalCheckpointInlineContent } from 'containers/VaultCard/InstitutionalCheckpointInlineContent'; +import { useTranslation } from 'libs/translations'; +import { useAccountAddress } from 'libs/wallet'; +import { type InstitutionalVault, VaultStatus } from 'types'; +import { + convertMantissaToTokens, + formatPercentageToReadableValue, + formatTokensToReadableValue, + formatWaitingPeriod, +} from 'utilities'; +import { getProjectedYieldTokens } from './getProjectedYieldTokens'; + +export interface FooterProps { + vault: InstitutionalVault; + fromAmountTokens?: BigNumber; +} + +export const Footer: React.FC = ({ vault, fromAmountTokens }) => { + const { t, language } = useTranslation(); + const { accountAddress } = useAccountAddress(); + + const userStakedTokens = convertMantissaToTokens({ + value: vault.userStakeBalanceMantissa ?? new BigNumber(0), + token: vault.stakedToken, + }); + + const hypotheticalUserStakedTokens = fromAmountTokens?.gt(0) + ? userStakedTokens.plus(fromAmountTokens) + : undefined; + + const projectedUserYieldTokens = + vault.status === VaultStatus.Refund || !vault.lockingPeriodMs + ? new BigNumber(0) + : getProjectedYieldTokens({ + depositTokens: userStakedTokens, + stakeAprPercentage: vault.stakeAprPercentage, + lockingPeriodMs: vault.lockingPeriodMs, + }); + + const readableLockingPeriod = formatWaitingPeriod({ + waitingPeriodSeconds: vault.lockingPeriodMs ? vault.lockingPeriodMs / 1000 : 0, + locale: language.locale, + }); + + const items: Array<{ key: string; node: ReactNode }> = []; + + if (accountAddress) { + items.push({ + key: 'currentDeposited', + node: ( + + + + ), + }); + } + + items.push({ + key: 'effectiveFixedApr', + node: ( + + {formatPercentageToReadableValue(vault.stakeAprPercentage)} + + ), + }); + + if (accountAddress && vault.lockingPeriodMs !== undefined) { + items.push({ + key: 'totalYields', + node: ( + + + + ), + }); + } + + items.push({ + key: 'checkpoint', + node: , + }); + + return ( +
+ {items.map(({ key, node }, index) => ( + + {node} + + {index < items.length - 1 && } + + ))} +
+ ); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/StatusContent/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/StatusContent/__tests__/index.spec.tsx new file mode 100644 index 0000000000..cd25ef7166 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/StatusContent/__tests__/index.spec.tsx @@ -0,0 +1,171 @@ +import { fireEvent, waitFor } from '@testing-library/react'; +import BigNumber from 'bignumber.js'; +import type { Mock } from 'vitest'; + +import { institutionalVault } from '__mocks__/models/vaults'; +import { useRedeemFromInstitutionalVault, useWithdrawFromInstitutionalVault } from 'clients/api'; +import { en } from 'libs/translations'; +import { renderComponent } from 'testUtils/render'; +import type { InstitutionalVault } from 'types'; +import { VaultStatus } from 'types'; + +import { StatusContent } from '..'; + +describe('StatusContent', () => { + const redeem = vi.fn().mockResolvedValue(undefined); + const withdraw = vi.fn().mockResolvedValue(undefined); + + beforeEach(() => { + (useRedeemFromInstitutionalVault as Mock).mockReturnValue({ + mutateAsync: redeem, + isPending: false, + }); + + (useWithdrawFromInstitutionalVault as Mock).mockReturnValue({ + mutateAsync: withdraw, + isPending: false, + }); + }); + + it('claims the full redeemable amount and closes the modal', async () => { + const onClose = vi.fn(); + const vault = { + ...institutionalVault, + status: VaultStatus.Claim, + userRedeemLimitMantissa: new BigNumber('250000000'), + } satisfies InstitutionalVault; + + const { getByRole } = renderComponent(); + + fireEvent.click( + getByRole('button', { + name: en.vault.modals.claim, + }), + ); + + await waitFor(() => expect(redeem).toHaveBeenCalledTimes(1)); + expect(redeem).toHaveBeenCalledWith({ + amountMantissa: vault.userRedeemLimitMantissa, + }); + await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1)); + + expect(useRedeemFromInstitutionalVault).toHaveBeenCalledWith({ + vaultAddress: vault.vaultAddress, + }); + }); + + it('disables the claim button when there is no redeemable amount', () => { + const vault = { + ...institutionalVault, + status: VaultStatus.Claim, + userRedeemLimitMantissa: new BigNumber(0), + userStakeBalanceMantissa: new BigNumber(0), + } satisfies InstitutionalVault; + + const { getByRole } = renderComponent(); + + expect( + getByRole('button', { + name: en.vault.modals.claim, + }), + ).toBeDisabled(); + }); + + it('claims the full share balance when the vault is claimable before maxRedeem is updated', async () => { + const onClose = vi.fn(); + const vault = { + ...institutionalVault, + status: VaultStatus.Claim, + userRedeemLimitMantissa: new BigNumber(0), + userStakeBalanceMantissa: new BigNumber('150000000'), + } satisfies InstitutionalVault; + + const { getByRole } = renderComponent(); + + fireEvent.click( + getByRole('button', { + name: en.vault.modals.claim, + }), + ); + + await waitFor(() => expect(redeem).toHaveBeenCalledTimes(1)); + expect(redeem).toHaveBeenCalledWith({ + amountMantissa: vault.userStakeBalanceMantissa, + }); + await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1)); + }); + + it('withdraws the full refundable amount and closes the modal', async () => { + const onClose = vi.fn(); + const vault = { + ...institutionalVault, + status: VaultStatus.Refund, + userWithdrawLimitMantissa: new BigNumber('120000000'), + } satisfies InstitutionalVault; + + const { getByRole } = renderComponent(); + + fireEvent.click( + getByRole('button', { + name: en.vault.modals.withdraw, + }), + ); + + await waitFor(() => expect(withdraw).toHaveBeenCalledTimes(1)); + expect(withdraw).toHaveBeenCalledWith({ + amountMantissa: vault.userWithdrawLimitMantissa, + }); + await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1)); + + expect(useWithdrawFromInstitutionalVault).toHaveBeenCalledWith({ + vaultAddress: vault.vaultAddress, + }); + }); + + it('withdraws the full share balance when the vault is refundable before maxWithdraw is updated', async () => { + const onClose = vi.fn(); + const vault = { + ...institutionalVault, + status: VaultStatus.Refund, + userWithdrawLimitMantissa: new BigNumber(0), + userStakeBalanceMantissa: new BigNumber('150000000'), + } satisfies InstitutionalVault; + + const { getByRole } = renderComponent(); + + fireEvent.click( + getByRole('button', { + name: en.vault.modals.withdraw, + }), + ); + + await waitFor(() => expect(withdraw).toHaveBeenCalledTimes(1)); + expect(withdraw).toHaveBeenCalledWith({ + amountMantissa: vault.userStakeBalanceMantissa, + }); + await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1)); + }); + + it('renders the deposits paused notice for non-claimable statuses', () => { + const vault = { + ...institutionalVault, + status: VaultStatus.Paused, + } satisfies InstitutionalVault; + + const { getByText, queryByRole } = renderComponent( + , + ); + + expect(getByText(en.vault.modals.depositsPausedNotice)).toBeInTheDocument(); + expect( + queryByRole('button', { + name: en.vault.modals.claim, + }), + ).not.toBeInTheDocument(); + expect( + queryByRole('button', { + name: en.vault.modals.withdraw, + }), + ).not.toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/StatusContent/getExitAmountMantissa/index.ts b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/StatusContent/getExitAmountMantissa/index.ts new file mode 100644 index 0000000000..40aefcf2c7 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/StatusContent/getExitAmountMantissa/index.ts @@ -0,0 +1,26 @@ +import BigNumber from 'bignumber.js'; +import type { VaultStatus } from 'types'; + +export interface GetExitAmountMantissaInput { + fallbackAmountMantissa?: BigNumber; + primaryAmountMantissa: BigNumber; + requiredStatus: VaultStatus; + status: VaultStatus; +} + +export const getExitAmountMantissa = ({ + fallbackAmountMantissa, + primaryAmountMantissa, + requiredStatus, + status, +}: GetExitAmountMantissaInput): BigNumber => { + if (primaryAmountMantissa.gt(0)) { + return primaryAmountMantissa; + } + + if (status !== requiredStatus) { + return new BigNumber(0); + } + + return fallbackAmountMantissa ?? new BigNumber(0); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/StatusContent/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/StatusContent/index.tsx new file mode 100644 index 0000000000..32b2923d58 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/StatusContent/index.tsx @@ -0,0 +1,82 @@ +import { useRedeemFromInstitutionalVault, useWithdrawFromInstitutionalVault } from 'clients/api'; +import { NoticeInfo, PrimaryButton } from 'components'; +import { useTranslation } from 'libs/translations'; +import { type InstitutionalVault, VaultStatus } from 'types'; +import { Footer } from '../Footer'; +import { getExitAmountMantissa } from './getExitAmountMantissa'; + +export interface StatusContentProps { + vault: InstitutionalVault; + onClose: () => void; +} + +export const StatusContent: React.FC = ({ vault, onClose }) => { + const { t } = useTranslation(); + const { mutateAsync: redeem, isPending: isRedeemLoading } = useRedeemFromInstitutionalVault({ + vaultAddress: vault.vaultAddress, + }); + const { mutateAsync: withdraw, isPending: isWithdrawLoading } = useWithdrawFromInstitutionalVault( + { + vaultAddress: vault.vaultAddress, + }, + ); + const claimAmountMantissa = getExitAmountMantissa({ + primaryAmountMantissa: vault.userRedeemLimitMantissa, + fallbackAmountMantissa: vault.userStakeBalanceMantissa, + status: vault.status, + requiredStatus: VaultStatus.Claim, + }); + const refundAmountMantissa = getExitAmountMantissa({ + primaryAmountMantissa: vault.userWithdrawLimitMantissa, + fallbackAmountMantissa: vault.userStakeBalanceMantissa, + status: vault.status, + requiredStatus: VaultStatus.Refund, + }); + + const handleClaim = async () => { + if (!claimAmountMantissa.gt(0)) { + return; + } + + await redeem({ + amountMantissa: claimAmountMantissa, + }); + + onClose(); + }; + + const handleRefund = async () => { + if (!refundAmountMantissa.gt(0)) { + return; + } + + await withdraw({ + amountMantissa: refundAmountMantissa, + }); + + onClose(); + }; + + const isClaim = vault.status === VaultStatus.Claim; + const isRefund = vault.status === VaultStatus.Refund; + const isLiquidated = vault.status === VaultStatus.Liquidated; + + return ( +
+
+ + {isClaim || isRefund || isLiquidated ? ( + + {isClaim ? t('vault.modals.claim') : t('vault.modals.withdraw')} + + ) : ( + + )} +
+ ); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/index.tsx new file mode 100644 index 0000000000..52a73d9764 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/PositionTab/index.tsx @@ -0,0 +1,29 @@ +import { useAccountAddress } from 'libs/wallet'; +import { type InstitutionalVault, VaultStatus } from 'types'; +import { DepositForm } from './DepositForm'; +import { StatusContent } from './StatusContent'; + +export interface PositionTabProps { + vault: InstitutionalVault; + onClose: () => void; +} + +export const PositionTab: React.FC = ({ vault, onClose }) => { + const { accountAddress } = useAccountAddress(); + + const shouldRenderStatusContent = + accountAddress && + (vault.status === VaultStatus.Claim || + vault.status === VaultStatus.Refund || + vault.status === VaultStatus.Earning || + vault.status === VaultStatus.Pending || + vault.status === VaultStatus.Repaying || + vault.status === VaultStatus.Paused || + vault.status === VaultStatus.Inactive); + + if (shouldRenderStatusContent) { + return ; + } + + return ; +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/index.tsx new file mode 100644 index 0000000000..83c016b89b --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/index.tsx @@ -0,0 +1,40 @@ +import { Modal, Tabs } from 'components'; +import { useTranslation } from 'libs/translations'; +import type { InstitutionalVault } from 'types'; + +import { VaultName } from '../VaultName'; +import { OverviewTab } from './OverviewTab'; +import { PositionTab } from './PositionTab'; + +export interface InstitutionalVaultModalProps { + vault: InstitutionalVault; + handleClose: () => void; + isOpen: boolean; +} + +export const InstitutionalVaultModal: React.FC = ({ + vault, + handleClose, + isOpen, +}) => { + const { t } = useTranslation(); + + const tabs = [ + { + id: 'position', + title: t('vault.modals.positionTab'), + content: , + }, + { + id: 'overview', + title: t('vault.modals.overviewTab'), + content: , + }, + ]; + + return ( + }> + + + ); +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/types/index.ts b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/types/index.ts new file mode 100644 index 0000000000..67130f8f3e --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/types/index.ts @@ -0,0 +1,5 @@ +export interface TimelineCheckpoint { + title: string; + description: string; + status: 'passed' | 'ongoing' | 'upcoming'; +} diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/__tests__/__snapshots__/index.spec.tsx.snap b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..01853de15d --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,51 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`useTimeline > returns the standard four-checkpoint timeline for non-refund vaults 1`] = ` +[ + { + "description": "Apr 1, 2026 12:00 AM - Apr 8, 2026 12:00 AM", + "status": "passed", + "title": "Deposit period", + }, + { + "description": "Apr 8, 2026 12:00 AM - Aug 29, 2026 12:00 AM", + "status": "passed", + "title": "Estimated earning period", + }, + { + "description": "Aug 29, 2026 12:00 AM - Sep 1, 2026 12:00 AM", + "status": "ongoing", + "title": "Estimated repaying period", + }, + { + "description": "Sep 1, 2026 12:00 AM", + "status": "upcoming", + "title": "Claim period", + }, +] +`; + +exports[`useTimeline > uses the maturity date for repaying and tbd for claim when settlement date is unavailable 1`] = ` +[ + { + "description": "Apr 1, 2026 12:00 AM - Apr 8, 2026 12:00 AM", + "status": "upcoming", + "title": "Deposit period", + }, + { + "description": "Apr 8, 2026 12:00 AM - Aug 29, 2026 12:00 AM", + "status": "upcoming", + "title": "Estimated earning period", + }, + { + "description": "Aug 29, 2026 12:00 AM", + "status": "upcoming", + "title": "Estimated repaying period", + }, + { + "description": "TBD", + "status": "upcoming", + "title": "Claim period", + }, +] +`; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/__tests__/index.spec.tsx new file mode 100644 index 0000000000..833860148d --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/__tests__/index.spec.tsx @@ -0,0 +1,70 @@ +import type { Mock } from 'vitest'; + +import { institutionalVault } from '__mocks__/models/vaults'; +import { useNow } from 'hooks/useNow'; +import { t } from 'libs/translations'; +import { renderHook } from 'testUtils/render'; +import type { InstitutionalVault } from 'types'; +import { VaultStatus } from 'types'; + +import { useTimeline } from '..'; + +vi.mock('hooks/useNow'); + +describe('useTimeline', () => { + const mockUseNow = useNow as Mock; + + beforeEach(() => { + mockUseNow.mockReturnValue(new Date('2026-04-05T00:00:00.000Z')); + }); + + it('returns the standard four-checkpoint timeline for non-refund vaults', () => { + mockUseNow.mockReturnValue(new Date('2026-08-30T00:00:00.000Z')); + + const vault = { + ...institutionalVault, + status: VaultStatus.Repaying, + } satisfies InstitutionalVault; + + const { result } = renderHook(() => useTimeline({ vault })); + + expect(result.current.checkpoints).toMatchSnapshot(); + }); + + it('returns the refund-specific timeline when the vault is refunding', () => { + mockUseNow.mockReturnValue(new Date('2026-04-09T00:00:00.000Z')); + + const vault = { + ...institutionalVault, + status: VaultStatus.Refund, + } satisfies InstitutionalVault; + + const { result } = renderHook(() => useTimeline({ vault })); + + expect(result.current.checkpoints).toEqual([ + { + title: t('vault.modals.institutionalTimeline.depositPeriod'), + description: `${t('vault.timeline.textualWithTime', { + date: vault.vaultDeploymentDate, + })} - ${t('vault.timeline.textualWithTime', { date: vault.openEndDate })}`, + status: 'passed', + }, + { + title: t('vault.modals.institutionalTimeline.refundPeriod'), + description: t('vault.timeline.textualWithTime', { date: vault.openEndDate }), + status: 'ongoing', + }, + ]); + }); + + it('uses the maturity date for repaying and tbd for claim when settlement date is unavailable', () => { + const vault = { + ...institutionalVault, + settlementDate: undefined, + } satisfies InstitutionalVault; + + const { result } = renderHook(() => useTimeline({ vault })); + + expect(result.current.checkpoints).toMatchSnapshot(); + }); +}); diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/getCheckPointDescription/index.ts b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/getCheckPointDescription/index.ts new file mode 100644 index 0000000000..c4779406c4 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/getCheckPointDescription/index.ts @@ -0,0 +1,27 @@ +import type { useTranslation } from 'libs/translations'; + +export const getCheckpointDescription = ({ + startDate, + endDate, + t, +}: { + t: ReturnType['t']; + startDate?: Date; + endDate?: Date; +}) => { + if (!startDate && !endDate) { + return t('vault.timeline.tbd'); + } + + let description = ''; + + if (startDate) { + description += t('vault.timeline.textualWithTime', { date: startDate }); + } + + if (endDate) { + description += ` - ${t('vault.timeline.textualWithTime', { date: endDate })}`; + } + + return description; +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/getCheckpointStatus/index.ts b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/getCheckpointStatus/index.ts new file mode 100644 index 0000000000..1f6891b16f --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/getCheckpointStatus/index.ts @@ -0,0 +1,27 @@ +import { isBefore } from 'date-fns/isBefore'; + +import type { InstitutionalVault, VaultStatus } from 'types'; + +export type CheckpointStatus = 'passed' | 'ongoing' | 'upcoming'; + +export const getCheckpointStatus = ({ + vault, + status, + now, + endDate, +}: { + vault: InstitutionalVault; + status: VaultStatus; + now: Date; + endDate?: Date; +}): CheckpointStatus => { + if (endDate && isBefore(endDate, now)) { + return 'passed'; + } + + if (vault.status === status) { + return 'ongoing'; + } + + return 'upcoming'; +}; diff --git a/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/index.tsx b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/index.tsx new file mode 100644 index 0000000000..fc39cf2092 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/InstitutionalVaultModal/useTimeline/index.tsx @@ -0,0 +1,89 @@ +import { useNow } from 'hooks/useNow'; +import { useTranslation } from 'libs/translations'; +import { type InstitutionalVault, VaultStatus } from 'types'; +import type { TimelineCheckpoint } from '../types'; +import { getCheckpointDescription } from './getCheckPointDescription'; +import { getCheckpointStatus } from './getCheckpointStatus'; + +export const useTimeline = ({ vault }: { vault: InstitutionalVault }) => { + const { t } = useTranslation(); + + const now = useNow(); + + const checkpoints: TimelineCheckpoint[] = [ + { + title: t('vault.modals.institutionalTimeline.depositPeriod'), + description: getCheckpointDescription({ + startDate: vault.vaultDeploymentDate, + endDate: vault.openEndDate, + t, + }), + status: getCheckpointStatus({ + vault, + now, + endDate: vault.openEndDate, + status: VaultStatus.Deposit, + }), + }, + ]; + + if (vault.status === VaultStatus.Refund) { + checkpoints.push({ + title: t('vault.modals.institutionalTimeline.refundPeriod'), + description: getCheckpointDescription({ + startDate: vault.openEndDate, + t, + }), + status: getCheckpointStatus({ + vault, + now, + status: VaultStatus.Refund, + }), + }); + } else { + checkpoints.push( + { + title: t('vault.modals.institutionalTimeline.estimatedEarningPeriod'), + description: getCheckpointDescription({ + startDate: vault.openEndDate, + endDate: vault.lockEndDate, + t, + }), + status: getCheckpointStatus({ + vault, + now, + endDate: vault.lockEndDate, + status: VaultStatus.Earning, + }), + }, + { + title: t('vault.modals.institutionalTimeline.estimatedRepayingPeriod'), + description: getCheckpointDescription({ + startDate: vault.lockEndDate, + endDate: vault.settlementDate, + t, + }), + status: getCheckpointStatus({ + vault, + now, + endDate: vault.settlementDate, + status: VaultStatus.Repaying, + }), + }, + { + title: t('vault.modals.institutionalTimeline.claimPeriod'), + description: getCheckpointDescription({ + startDate: vault.settlementDate, + t, + }), + status: getCheckpointStatus({ + vault, + now, + status: VaultStatus.Claim, + }), + }, + ); + } + + return { checkpoints }; +}; diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/MarketInfo/index.tsx b/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/MarketInfo/index.tsx deleted file mode 100644 index 29058152cc..0000000000 --- a/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/MarketInfo/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Icon, LabeledInlineContent } from 'components'; -import { PLACEHOLDER_KEY } from 'constants/placeholders'; -import { CopyAddressButton } from 'containers/CopyAddressButton'; -import { Link } from 'containers/Link'; -import { useTranslation } from 'libs/translations'; -import type { PendleVault } from 'types'; - -interface MarketInfoProps { - vault: PendleVault; -} - -export const MarketInfo: React.FC = ({ vault }) => { - const { t, Trans } = useTranslation(); - - const formattedDeploymentDate = vault.vaultDeploymentDate - ? t('vault.modals.textualDate', { date: vault.vaultDeploymentDate }) - : PLACEHOLDER_KEY; - - return ( -
-

{t('vault.modals.overview.marketInfo')}

- - - {formattedDeploymentDate} - - - -
- - PENDLE - {vault.managerLink && ( - - - - )} - {vault.asset?.vToken?.underlyingToken?.address && ( - - )} -
-
- -
-

- {t('vault.modals.overview.marketRiskDisclosures')} -

-

- , - }} - /> -

-
-
- ); -}; diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/StrategyDiagram/index.tsx b/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/StrategyDiagram/index.tsx index f9a675d049..ffdb8f3b03 100644 --- a/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/StrategyDiagram/index.tsx +++ b/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/StrategyDiagram/index.tsx @@ -1,8 +1,8 @@ import { cn } from 'components'; import { useTranslation } from 'libs/translations'; import type { PendleVault } from 'types'; -import { FlowArrow } from './FlowArrow'; -import { FlowNode } from './FlowNode'; +import { FlowArrow } from '../../../FlowArrow'; +import { FlowNode } from '../../../FlowNode'; interface StrategyDiagramProps { vault: PendleVault; diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/index.tsx b/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/index.tsx index 15efe2bf36..3526e9b610 100644 --- a/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/index.tsx +++ b/apps/evm/src/containers/VaultCard/PendleVaultModal/OverviewTab/index.tsx @@ -1,6 +1,6 @@ import type { PendleVault } from 'types'; -import { MarketInfo } from './MarketInfo'; +import { VaultOverviewMarketInfo } from 'containers/VaultCard/VaultOverviewMarketInfo'; import { StrategyDiagram } from './StrategyDiagram'; import { TotalDeposits } from './TotalDeposits'; @@ -15,7 +15,13 @@ export const OverviewTab: React.FC = ({ vault }) => { - +
); }; diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/DepositForm/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/DepositForm/__tests__/index.spec.tsx index 3b873740d9..46301f153c 100644 --- a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/DepositForm/__tests__/index.spec.tsx +++ b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/DepositForm/__tests__/index.spec.tsx @@ -9,6 +9,7 @@ import { bnb } from '__mocks__/models/tokens'; import { fixedRatedVaults, vaults } from '__mocks__/models/vaults'; import { useGetBalanceOf, useGetPendleSwapQuote, useStakeInPendleVault } from 'clients/api'; import type { GetPendleSwapQuoteOutput } from 'clients/api'; +import type { PendleVaultProtocolData } from 'clients/api/queries/getFixedRatedVaults/types'; import { NULL_ADDRESS } from 'constants/address'; import { useGetContractAddress } from 'hooks/useGetContractAddress'; import { useGetUserSlippageTolerance } from 'hooks/useGetUserSlippageTolerance'; @@ -16,18 +17,27 @@ import useTokenApproval from 'hooks/useTokenApproval'; import { en } from 'libs/translations'; import { renderComponent } from 'testUtils/render'; import type { PendleVault, Token, VToken } from 'types'; -import { ChainId, VaultCategory, VaultManager, VaultStatus } from 'types'; +import { ChainId, VaultCategory, VaultManager, VaultStatus, VaultType } from 'types'; import { convertTokensToMantissa, formatTokensToReadableValue } from 'utilities'; import type { Address } from 'viem'; import { DepositForm } from '..'; vi.mock('hooks/useGetUserSlippageTolerance'); +vi.mock('hooks/useDebounceValue', () => ({ + default: (value: unknown) => value, +})); const fakePendlePtVaultAddress = '0xfakePendlePtVaultContractAddress' as Address; const fakePendleMarketAddress = '0xfakePendleMarketAddress' as Address; const fakeWalletBalanceMantissa = new BigNumber('12000000000000000000'); const fixedRatedVault = fixedRatedVaults[0]; +const protocolData = fixedRatedVault.protocolData as PendleVaultProtocolData; + +type GetPendleSwapQuoteCall = [ + Parameters[0], + Parameters[1], +]; const ptClisBnbToken: Token = { chainId: ChainId.BSC_TESTNET, @@ -56,16 +66,18 @@ const vault: PendleVault = { ...vaults[1], key: `${ChainId.BSC_TESTNET}-pendle-${fixedRatedVault.vaultAddress}`, category: VaultCategory.YIELD_TOKENS, + vaultType: VaultType.Pendle, manager: VaultManager.Pendle, managerIcon: 'pendle', - managerAddress: fixedRatedVault.protocolData.pendleMarketAddress as Address, - managerLink: `https://app.pendle.finance/trade/pools/${fixedRatedVault.protocolData.pendleMarketAddress}/zap/in?chain=bnbchain`, + managerAddress: protocolData.pendleMarketAddress as Address, + managerLink: `https://app.pendle.finance/trade/pools/${protocolData.pendleMarketAddress}/zap/in?chain=bnbchain`, status: VaultStatus.Deposit, + vaultAddress: fixedRatedVault.vaultAddress, stakedToken: bnb, rewardToken: ptClisBnbToken, maturityDate: new Date(fixedRatedVault.maturityDate), - vaultDeploymentDate: new Date(fixedRatedVault.protocolData.startDate), - liquidityCents: new BigNumber(fixedRatedVault.protocolData.liquidityCents), + vaultDeploymentDate: new Date(protocolData.startDate), + liquidityCents: new BigNumber(protocolData.liquidityCents), asset, poolComptrollerContractAddress: poolData[0].comptrollerAddress as Address, poolName: poolData[0].name, @@ -127,7 +139,7 @@ describe('DepositForm', () => { await waitFor(() => expect(screen.getByText(en.vault.modals.connectWalletMessage)).toBeInTheDocument(), ); - expect(screen.queryByRole('spinbutton')).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText('0.00')).not.toBeInTheDocument(); expect(useGetBalanceOf).toHaveBeenCalledWith( { @@ -146,6 +158,7 @@ describe('DepositForm', () => { renderComponent(, { accountAddress: fakeAccountAddress, }); + const input = screen.getByPlaceholderText('0.00') as HTMLInputElement; const readableLimit = formatTokensToReadableValue({ value: expectedLimit, @@ -158,7 +171,7 @@ describe('DepositForm', () => { }), ); - await waitFor(() => expect(screen.getByRole('spinbutton')).toHaveValue(3)); + await waitFor(() => expect(input.value).toBe('3')); await waitFor(() => expect(useTokenApproval).toHaveBeenCalledWith( expect.objectContaining({ @@ -169,27 +182,19 @@ describe('DepositForm', () => { ), ); - await waitFor(() => { - const lastCall = (useGetPendleSwapQuote as Mock).mock.lastCall; - - expect(lastCall).toBeDefined(); - - const [quoteInput, quoteOptions] = lastCall as [ - { - fromToken: Token; - toToken: Token; - amountTokens: BigNumber; - slippagePercentage: number; - }, - { enabled?: boolean }, - ]; - - expect(quoteInput.fromToken).toEqual(vault.stakedToken); - expect(quoteInput.toToken).toEqual(vault.rewardToken); - expect(quoteInput.amountTokens.toFixed()).toBe('3'); - expect(quoteInput.slippagePercentage).toBe(0.5); - expect(quoteOptions).toEqual({ enabled: true }); - }); + expect( + (useGetPendleSwapQuote as Mock).mock.calls.some(call => { + const [quoteInput, quoteOptions] = call as GetPendleSwapQuoteCall; + + return ( + quoteInput.fromToken === vault.stakedToken && + quoteInput.toToken === vault.rewardToken && + quoteInput.amountTokens.toFixed() === '3' && + quoteInput.slippagePercentage === 0.5 && + quoteOptions?.enabled === true + ); + }), + ).toBe(true); }); it('submits the deposit with the swap quote and closes the modal', async () => { diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/DepositForm/index.tsx b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/DepositForm/index.tsx index 401cdf0213..04ebd5bf84 100644 --- a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/DepositForm/index.tsx +++ b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/DepositForm/index.tsx @@ -46,7 +46,7 @@ export const DepositForm: React.FC = ({ vault, onClose }) => { const userWalletBalanceMantissa = getUserWalletBalanceData?.balanceMantissa; const userStakedTokens = convertMantissaToTokens({ - value: vault.userStakedMantissa ?? new BigNumber(0), + value: vault.userStakeBalanceMantissa ?? new BigNumber(0), token: vault.asset.vToken.underlyingToken, }); diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/Footer/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/Footer/__tests__/index.spec.tsx index 6b63222887..a5c04c10ee 100644 --- a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/Footer/__tests__/index.spec.tsx +++ b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/Footer/__tests__/index.spec.tsx @@ -7,7 +7,7 @@ import type { GetPendleSwapQuoteOutput } from 'clients/api'; import { en, t } from 'libs/translations'; import { renderComponent } from 'testUtils/render'; import type { PendleVault } from 'types'; -import { VaultCategory, VaultManager, VaultStatus } from 'types'; +import { VaultCategory, VaultManager, VaultStatus, VaultType } from 'types'; import { convertMantissaToTokens, formatCentsToReadableValue, @@ -19,11 +19,13 @@ import { Footer, type FooterProps } from '..'; const baseVault: PendleVault = { ...vaults[0], category: VaultCategory.YIELD_TOKENS, + vaultType: VaultType.Pendle, manager: VaultManager.Pendle, managerIcon: 'logoMobile', status: VaultStatus.Active, key: 'pendle-VAI-XVS-2026-06-25', - stakingAprPercentage: 3.39809766, + stakeAprPercentage: 3.39809766, + vaultAddress: '0x2222222222222222222222222222222222222222', maturityDate: new Date('2026-06-25T00:00:00.000Z'), liquidityCents: new BigNumber('742673002'), asset: assetData[0], @@ -59,7 +61,7 @@ describe('Footer', () => { expect(getByText(en.vault.modals.effectiveFixedApr)).toBeInTheDocument(); expect( - getByText(formatPercentageToReadableValue(baseVault.stakingAprPercentage)), + getByText(formatPercentageToReadableValue(baseVault.stakeAprPercentage)), ).toBeInTheDocument(); expect(getByText(en.vault.modals.maturityDate)).toBeInTheDocument(); expect( diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/Footer/index.tsx b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/Footer/index.tsx index c0efb022d4..1377b55d69 100644 --- a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/Footer/index.tsx +++ b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/Footer/index.tsx @@ -85,7 +85,7 @@ export const Footer: React.FC = ({ label={t('vault.modals.effectiveFixedApr')} tooltip={t('vault.modals.effectiveFixedAprPendleTooltip')} > - {formatPercentageToReadableValue(vault.stakingAprPercentage)} + {formatPercentageToReadableValue(vault.stakeAprPercentage)} diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/WithdrawForm/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/WithdrawForm/__tests__/index.spec.tsx index b591c3fe02..78faa63045 100644 --- a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/WithdrawForm/__tests__/index.spec.tsx +++ b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/WithdrawForm/__tests__/index.spec.tsx @@ -20,6 +20,7 @@ import { useNow } from 'hooks/useNow'; import { en } from 'libs/translations'; import { renderComponent } from 'testUtils/render'; import type { PendleVault } from 'types'; +import { VaultType } from 'types'; import type { Address } from 'viem'; import { WithdrawForm } from '..'; @@ -45,11 +46,13 @@ type GetPendleSwapQuoteCall = [ const vault: PendleVault = { ...vaults[1], key: 'pendle-test-vault', + vaultType: VaultType.Pendle, rewardToken: xvs, stakedToken: busd, rewardTokenPriceCents: new BigNumber(123), stakedTokenPriceCents: new BigNumber(456), - userStakedMantissa: new BigNumber('12000000000000000000'), + userStakeBalanceMantissa: new BigNumber('12000000000000000000'), + vaultAddress: '0x3333333333333333333333333333333333333333', maturityDate: new Date('2026-06-25T00:00:00.000Z'), liquidityCents: new BigNumber(1000000), asset: { diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/WithdrawForm/index.tsx b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/WithdrawForm/index.tsx index 49514748a4..929ef04b90 100644 --- a/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/WithdrawForm/index.tsx +++ b/apps/evm/src/containers/VaultCard/PendleVaultModal/PositionTab/WithdrawForm/index.tsx @@ -45,7 +45,7 @@ export const WithdrawForm: React.FC = ({ vault, onClose }) => const fromTokenPriceCents = vault.rewardTokenPriceCents; const userStakedTokens = convertMantissaToTokens({ - value: vault.userStakedMantissa ?? new BigNumber(0), + value: vault.userStakeBalanceMantissa ?? new BigNumber(0), token: vault.asset.vToken.underlyingToken, }); diff --git a/apps/evm/src/containers/VaultCard/PendleVaultModal/index.tsx b/apps/evm/src/containers/VaultCard/PendleVaultModal/index.tsx index 94b5bb7f52..9c195ade13 100644 --- a/apps/evm/src/containers/VaultCard/PendleVaultModal/index.tsx +++ b/apps/evm/src/containers/VaultCard/PendleVaultModal/index.tsx @@ -33,12 +33,7 @@ export const PendleVaultModal: React.FC = ({ ]; return ( - } - useDrawerInXs - > + }> ); diff --git a/apps/evm/src/containers/VaultCard/PrimeEligibilityInlineContent/index.tsx b/apps/evm/src/containers/VaultCard/PrimeEligibilityInlineContent/index.tsx index 4f740ee595..6fcfe9a4f4 100644 --- a/apps/evm/src/containers/VaultCard/PrimeEligibilityInlineContent/index.tsx +++ b/apps/evm/src/containers/VaultCard/PrimeEligibilityInlineContent/index.tsx @@ -1,13 +1,12 @@ import { LabeledInlineContent } from 'components'; import { routes } from 'constants/routing'; import { Link } from 'containers/Link'; -import { formatWaitingPeriod } from 'containers/PrimeStatusBanner/formatWaitingPeriod'; import { useGetUserPrimeInfo } from 'hooks/useGetUserPrimeInfo'; import { useGetToken } from 'libs/tokens'; import { useTranslation } from 'libs/translations'; import { useAccountAddress } from 'libs/wallet'; import { useMemo } from 'react'; -import { clampToZero, formatTokensToReadableValue } from 'utilities'; +import { clampToZero, formatTokensToReadableValue, formatWaitingPeriod } from 'utilities'; import { Progress } from '../Progress'; export interface PrimeEligibilityInlineContentProps { diff --git a/apps/evm/src/containers/VaultCard/Progress/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/Progress/__tests__/index.spec.tsx new file mode 100644 index 0000000000..923564b3ef --- /dev/null +++ b/apps/evm/src/containers/VaultCard/Progress/__tests__/index.spec.tsx @@ -0,0 +1,77 @@ +import BigNumber from 'bignumber.js'; + +import { usdc } from '__mocks__/models/tokens'; +import { renderComponent } from 'testUtils/render'; + +import { Progress } from '..'; + +const getProgressFill = (container: HTMLElement) => { + const progressFill = container.querySelector('.w-25 > div'); + + expect(progressFill).toBeInTheDocument(); + + return progressFill as HTMLDivElement; +}; + +describe('Progress', () => { + it('renders the amount, max value, and percentage', () => { + const { getByText } = renderComponent( + , + ); + + expect(getByText('50 USDC / 100 USDC')).toBeInTheDocument(); + expect(getByText('50%')).toBeInTheDocument(); + }); + + it('uses a blue progress bar below 80 percent', () => { + const { container } = renderComponent( + , + ); + + const progressFill = getProgressFill(container); + + expect(progressFill).toHaveClass('bg-blue'); + expect(progressFill).toHaveStyle({ width: '50%' }); + }); + + it('uses a green progress bar at or above 80 percent', () => { + const { container, getByText } = renderComponent( + , + ); + + const progressFill = getProgressFill(container); + + expect(getByText('80%')).toBeInTheDocument(); + expect(progressFill).toHaveClass('bg-green'); + expect(progressFill).toHaveStyle({ width: '80%' }); + }); + + it('caps the displayed percentage and width at 100 percent', () => { + const { container, getByText } = renderComponent( + , + ); + + const progressFill = getProgressFill(container); + + expect(getByText('150 USDC / 100 USDC')).toBeInTheDocument(); + expect(getByText('100%')).toBeInTheDocument(); + expect(progressFill).toHaveStyle({ width: '100%' }); + }); + + it('applies the custom progress bar override class', () => { + const { container } = renderComponent( + , + ); + + expect(getProgressFill(container)).toHaveClass('bg-yellow'); + }); +}); diff --git a/apps/evm/src/containers/VaultCard/Progress/index.tsx b/apps/evm/src/containers/VaultCard/Progress/index.tsx index b7271d8a1f..5b29093019 100644 --- a/apps/evm/src/containers/VaultCard/Progress/index.tsx +++ b/apps/evm/src/containers/VaultCard/Progress/index.tsx @@ -12,6 +12,7 @@ export interface ProgressProps { amountTokens: BigNumber; maxTokens: BigNumber; className?: string; + progressBarClassName?: string; } export const Progress: React.FC = ({ @@ -19,6 +20,7 @@ export const Progress: React.FC = ({ amountTokens, maxTokens, className, + progressBarClassName, }) => { let percentage = calculatePercentage({ numerator: amountTokens.toNumber(), @@ -41,6 +43,8 @@ export const Progress: React.FC = ({ token, }); + const progressBarColorClassName = percentage >= 80 ? 'bg-green' : 'bg-blue'; + return (
@@ -50,7 +54,7 @@ export const Progress: React.FC = ({
diff --git a/apps/evm/src/containers/VaultCard/Simplified/index.tsx b/apps/evm/src/containers/VaultCard/Simplified/index.tsx index fb89cfc333..795b222e39 100644 --- a/apps/evm/src/containers/VaultCard/Simplified/index.tsx +++ b/apps/evm/src/containers/VaultCard/Simplified/index.tsx @@ -7,14 +7,17 @@ import useConvertMantissaToReadableTokenString from 'hooks/useConvertMantissaToR import { useTranslation } from 'libs/translations'; import { useAccountAddress } from 'libs/wallet'; import type { Vault } from 'types'; +import { VaultStatus } from 'types'; import { convertMantissaToTokens, formatPercentageToReadableValue, formatTokensToReadableValue, + isInstitutionalVault, isLegacyVenusVault, isPendleVault, } from 'utilities'; +import { InstitutionalVaultModal } from 'containers/VaultCard/InstitutionalVaultModal'; import { PendleVaultModal } from 'containers/VaultCard/PendleVaultModal'; import { useState } from 'react'; import { VenusVaultModal } from '../VenusVaultModal'; @@ -39,13 +42,13 @@ export const VaultCardSimplified: React.FC = ({ vault, const readableUserStakedTokens = useConvertMantissaToReadableTokenString({ token: displayToken, - value: vault.userStakedMantissa || new BigNumber(0), + value: vault.userStakeBalanceMantissa || new BigNumber(0), addSymbol: false, }); - const isPaused = 'isPaused' in vault && vault.isPaused; + const isPaused = ('isPaused' in vault && vault.isPaused) || vault.status === VaultStatus.Inactive; - const canWithdraw = vault.userStakedMantissa?.gt(0); + const canWithdraw = vault.userStakeBalanceMantissa?.gt(0); const showHoldingsCard = accountAddress && canWithdraw; const dailyEmissionReadableValue = @@ -59,12 +62,12 @@ export const VaultCardSimplified: React.FC = ({ vault, }) : undefined; - const totalDepositedReadableValue = vault.totalStakedMantissa ? ( + const totalDepositedReadableValue = vault.stakeBalanceMantissa ? (
{formatTokensToReadableValue({ value: convertMantissaToTokens({ - value: vault.totalStakedMantissa, + value: vault.stakeBalanceMantissa, token: displayToken, }), token: displayToken, @@ -101,7 +104,7 @@ export const VaultCardSimplified: React.FC = ({ vault,
{showHoldingsCard && dailyEmissionReadableValue && ( @@ -119,6 +122,10 @@ export const VaultCardSimplified: React.FC = ({ vault, {isLegacyVenusVault(vault) && ( )} + + {isInstitutionalVault(vault) && ( + + )} ); }; diff --git a/apps/evm/src/containers/VaultCard/StatusLabel/index.spec.tsx b/apps/evm/src/containers/VaultCard/StatusLabel/index.spec.tsx new file mode 100644 index 0000000000..5d22954b35 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/StatusLabel/index.spec.tsx @@ -0,0 +1,98 @@ +import { screen } from '@testing-library/react'; + +import { en } from 'libs/translations'; +import { renderComponent } from 'testUtils/render'; +import { VaultStatus } from 'types'; + +import { StatusLabel } from '.'; + +interface StatusScenario { + status: VaultStatus; + expectedClassNames: string[]; + expectedLabel: string; +} + +const statusScenarios: StatusScenario[] = [ + { + status: VaultStatus.Claim, + expectedClassNames: ['border-green', 'bg-green/10'], + expectedLabel: en.vault.filter.claim, + }, + { + status: VaultStatus.Earning, + expectedClassNames: ['border-green', 'bg-green/10'], + expectedLabel: en.vault.filter.earning, + }, + { + status: VaultStatus.Refund, + expectedClassNames: ['border-yellow', 'bg-yellow/10'], + expectedLabel: en.vault.filter.refund, + }, + { + status: VaultStatus.Deposit, + expectedClassNames: ['border-blue', 'bg-blue/10'], + expectedLabel: en.vault.filter.deposit, + }, + { + status: VaultStatus.Active, + expectedClassNames: ['border-blue', 'bg-blue/10'], + expectedLabel: en.vault.filter.active, + }, + { + status: VaultStatus.Paused, + expectedClassNames: ['border-dark-grey-hover', 'bg-dark-grey'], + expectedLabel: en.vault.filter.paused, + }, + { + status: VaultStatus.Pending, + expectedClassNames: ['border-dark-grey-hover', 'bg-dark-grey'], + expectedLabel: en.vault.filter.pending, + }, + { + status: VaultStatus.Repaying, + expectedClassNames: ['border-dark-grey-hover', 'bg-dark-grey'], + expectedLabel: en.vault.filter.repaying, + }, + { + status: VaultStatus.Inactive, + expectedClassNames: ['border-dark-grey-hover', 'bg-dark-grey'], + expectedLabel: en.vault.filter.inactive, + }, + { + status: VaultStatus.Liquidated, + expectedClassNames: ['border-dark-grey-hover', 'bg-dark-grey'], + expectedLabel: en.vault.filter.liquidated, + }, +]; + +describe('StatusLabel', () => { + it.each(statusScenarios)( + 'renders the correct label and variant for $status vaults', + ({ status, expectedClassNames, expectedLabel }) => { + const { container } = renderComponent(); + const rootElement = container.firstElementChild as HTMLDivElement; + + expect(rootElement).toBeInTheDocument(); + expect(rootElement).toHaveClass(...expectedClassNames); + + expect(screen.getByText(expectedLabel)).toBe(rootElement); + }, + ); + + it('merges custom class names and forwards html attributes', () => { + const { getByTestId } = renderComponent( + , + ); + + const label = getByTestId('status-label'); + + expect(label).toHaveClass('custom-class'); + expect(label).toHaveAttribute('id', 'vault-status-label'); + expect(label).toHaveTextContent(en.vault.filter.active); + }); +}); diff --git a/apps/evm/src/containers/VaultCard/StatusLabel/index.tsx b/apps/evm/src/containers/VaultCard/StatusLabel/index.tsx index cc85815787..3ecee4dd8d 100644 --- a/apps/evm/src/containers/VaultCard/StatusLabel/index.tsx +++ b/apps/evm/src/containers/VaultCard/StatusLabel/index.tsx @@ -8,14 +8,11 @@ export interface StatusLabelProps extends HTMLAttributes { size?: 'md'; } -const commonClassName = cn( - 'flex justify-center items-center border border-solid rounded-full py-1 px-3 text-light-grey-active text-b1r', -); - export const StatusLabel: FC = ({ status, className, children, ...props }) => { const { t } = useTranslation(); let variantClassName = ''; + switch (status) { case VaultStatus.Claim: case VaultStatus.Earning: @@ -33,6 +30,7 @@ export const StatusLabel: FC = ({ status, className, children, } let label = ''; + switch (status) { case VaultStatus.Claim: label = t('vault.filter.claim'); @@ -52,13 +50,29 @@ export const StatusLabel: FC = ({ status, className, children, case VaultStatus.Paused: label = t('vault.filter.paused'); break; + case VaultStatus.Pending: + label = t('vault.filter.pending'); + break; case VaultStatus.Repaying: label = t('vault.filter.repaying'); break; + case VaultStatus.Inactive: + label = t('vault.filter.inactive'); + break; + case VaultStatus.Liquidated: + label = t('vault.filter.liquidated'); + break; } return ( -
+
{label}
); diff --git a/apps/evm/src/containers/VaultCard/TransactionForm/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/TransactionForm/__tests__/index.spec.tsx index c6b72adab0..3eeadd1232 100644 --- a/apps/evm/src/containers/VaultCard/TransactionForm/__tests__/index.spec.tsx +++ b/apps/evm/src/containers/VaultCard/TransactionForm/__tests__/index.spec.tsx @@ -245,6 +245,40 @@ describe('TransactionForm', () => { ).toBeDisabled(); }); + it('gates submission behind the optional acknowledgement', async () => { + const limitFromTokens = new BigNumber(12); + + renderTransactionForm({ + props: { + limitFromTokens, + acknowledgement: 'I agree', + }, + walletSpendingLimitTokens: limitFromTokens, + }); + + fireEvent.click(screen.getByRole('button', { name: '12 XVS' })); + + await waitFor(() => + expect( + screen.getByRole('button', { + name: baseProps.submitButtonLabel, + }), + ).toBeDisabled(), + ); + + expect(screen.getByText('I agree')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('checkbox')); + + await waitFor(() => + expect( + screen.getByRole('button', { + name: baseProps.submitButtonLabel, + }), + ).toBeEnabled(), + ); + }); + it('submits the form and resets the amount field on success', async () => { const onSubmit = vi.fn().mockResolvedValue(undefined); const limitFromTokens = new BigNumber(12); diff --git a/apps/evm/src/containers/VaultCard/TransactionForm/index.tsx b/apps/evm/src/containers/VaultCard/TransactionForm/index.tsx index ba0a3013af..4be4f92cc7 100644 --- a/apps/evm/src/containers/VaultCard/TransactionForm/index.tsx +++ b/apps/evm/src/containers/VaultCard/TransactionForm/index.tsx @@ -7,7 +7,7 @@ import { type PendleSwapQuoteError, useGetBalanceOf, } from 'clients/api'; -import { AvailableBalance, LabeledSlider, NoticeInfo, SpendingLimit } from 'components'; +import { AvailableBalance, Checkbox, LabeledSlider, NoticeInfo, SpendingLimit } from 'components'; import { NULL_ADDRESS } from 'constants/address'; import { HIGH_PRICE_IMPACT_THRESHOLD_PERCENTAGE, @@ -19,7 +19,7 @@ import useTokenApproval from 'hooks/useTokenApproval'; import { handleError } from 'libs/errors'; import { useTranslation } from 'libs/translations'; import { useAccountAddress } from 'libs/wallet'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import type { Token } from 'types'; import { convertMantissaToTokens } from 'utilities'; import { formatTokensToReadableValue } from 'utilities/formatTokensToReadableValue'; @@ -41,6 +41,7 @@ export interface TransactionFormProps { swapToToken?: Token; swapQuote?: GetPendleSwapQuoteOutput; swapQuoteError?: PendleSwapQuoteError; + acknowledgement?: React.ReactNode; } export const TransactionForm: React.FC = ({ @@ -58,9 +59,16 @@ export const TransactionForm: React.FC = ({ swapToToken, swapQuote, swapQuoteError, + acknowledgement, }) => { const { t } = useTranslation(); const { accountAddress } = useAccountAddress(); + + const [isAcknowledgementChecked, setIsAcknowledgementChecked] = useState(false); + + const [isUserAcknowledgingHighPriceImpact, setIsUserAcknowledgingHighPriceImpact] = + useState(false); + const fromAmountTokensFieldValue = form.watch('fromAmountTokens'); const fromAmountTokens = new BigNumber(fromAmountTokensFieldValue || 0); @@ -70,8 +78,6 @@ export const TransactionForm: React.FC = ({ swapQuote.priceImpactPercentage >= HIGH_PRICE_IMPACT_THRESHOLD_PERCENTAGE && swapQuote.priceImpactPercentage < MAXIMUM_PRICE_IMPACT_THRESHOLD_PERCENTAGE; - const isUserAcknowledgingHighPriceImpact = form.watch('acknowledgeHighPriceImpact'); - const approval: Approval | undefined = spenderAddress && fromAmountTokens.isGreaterThan(0) ? { @@ -187,6 +193,8 @@ export const TransactionForm: React.FC = ({ await onSubmit(); form.reset(); + setIsAcknowledgementChecked(false); + setIsUserAcknowledgingHighPriceImpact(false); } catch (error) { handleError({ error }); } @@ -201,6 +209,10 @@ export const TransactionForm: React.FC = ({ isFormValid = isFormValid && !!swapQuote && riskAcknowledged; } + if (acknowledgement) { + isFormValid = isFormValid && isAcknowledgementChecked; + } + return (
{!!accountAddress && ( @@ -231,15 +243,24 @@ export const TransactionForm: React.FC = ({ {footer &&
{footer}
} + {!!accountAddress && acknowledgement && ( +
+ setIsAcknowledgementChecked(event.target.checked)} + /> + + {acknowledgement} +
+ )} + - form.setValue('acknowledgeHighPriceImpact', acknowledgeHighPriceImpact) - } + setAcknowledgeHighPriceImpact={setIsUserAcknowledgingHighPriceImpact} swapFromToken={swapFromToken} swapToToken={swapToToken} swapPriceImpactPercentage={swapQuote?.priceImpactPercentage} diff --git a/apps/evm/src/containers/VaultCard/VaultName/index.tsx b/apps/evm/src/containers/VaultCard/VaultName/index.tsx index 9fd887be20..664df7fec3 100644 --- a/apps/evm/src/containers/VaultCard/VaultName/index.tsx +++ b/apps/evm/src/containers/VaultCard/VaultName/index.tsx @@ -3,7 +3,7 @@ import { cn } from '@venusprotocol/ui'; import { TokenIcon } from 'components/TokenIcon'; import { useNow } from 'hooks/useNow'; import { useTranslation } from 'libs/translations'; -import type { Vault } from 'types'; +import { type Vault, VaultType } from 'types'; import { getVaultCategoryName } from 'utilities/getVaultCategoryName'; export interface VaultNameProps { @@ -17,14 +17,14 @@ export const VaultName: React.FC = ({ vault, className }) => { let description: undefined | string; - if ('maturityDate' in vault) { + if ('maturityDate' in vault && vault.maturityDate) { const remainingDays = Math.ceil( (vault.maturityDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), ); description = t('vault.card.header.maturityDate', { date: vault.maturityDate, - count: remainingDays, + count: remainingDays > 0 ? remainingDays : 0, }); } else { description = getVaultCategoryName({ @@ -33,12 +33,20 @@ export const VaultName: React.FC = ({ vault, className }) => { }); } + let title = vault.stakedToken.symbol; + + if (vault.vaultType === VaultType.Institutional) { + title += ` - ${vault.manager.toUpperCase()}`; + } + return (
-

{vault.stakedToken.symbol}

+
+

{title}

+
{!!description &&

{description}

}
diff --git a/apps/evm/src/containers/VaultCard/VaultOverviewMarketInfo/index.tsx b/apps/evm/src/containers/VaultCard/VaultOverviewMarketInfo/index.tsx new file mode 100644 index 0000000000..c2082d76c1 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/VaultOverviewMarketInfo/index.tsx @@ -0,0 +1,85 @@ +import { Icon, type IconName, LabeledInlineContent } from 'components'; +import { PLACEHOLDER_KEY } from 'constants/placeholders'; +import { CopyAddressButton } from 'containers/CopyAddressButton'; +import { Link } from 'containers/Link'; +import { useTranslation } from 'libs/translations'; +import { VaultManager } from 'types'; + +export interface VaultOverviewMarketInfoProps { + vaultDeploymentDate?: Date; + manager: string; + managerIcon: IconName; + managerLink?: string; + copyAddress?: string; +} + +export const VaultOverviewMarketInfo: React.FC = ({ + vaultDeploymentDate, + manager, + managerIcon, + managerLink, + copyAddress, +}) => { + const { t, Trans } = useTranslation(); + + const formattedDeploymentDate = vaultDeploymentDate + ? t('vault.modals.textualDate', { date: vaultDeploymentDate }) + : PLACEHOLDER_KEY; + + const riskDisclosure = + manager === VaultManager.Pendle ? ( + , + }} + /> + ) : ( + , + }} + /> + ); + + return ( +
+

{t('vault.modals.overview.marketInfo')}

+ + + {formattedDeploymentDate} + + + +
+ + + {manager} + + {managerLink && ( + + + + )} + + {copyAddress && } +
+
+ +
+

+ {t('vault.modals.overview.marketRiskDisclosures')} +

+ +

{riskDisclosure}

+
+
+ ); +}; diff --git a/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/calculateDailyVaultEarnings/index.ts b/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/calculateDailyVaultEarnings/index.ts index 41ee7b9093..7f1a239fb5 100644 --- a/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/calculateDailyVaultEarnings/index.ts +++ b/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/calculateDailyVaultEarnings/index.ts @@ -12,7 +12,7 @@ export const calculateDailyVaultEarnings = ({ }) => { const yearlyEarnings = calculateYearlyInterests({ balance, - interestPercentage: new BigNumber(vault.stakingAprPercentage), + interestPercentage: new BigNumber(vault.stakeAprPercentage), }) // Convert to reward tokens .multipliedBy(vault.stakedTokenPriceCents) diff --git a/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/index.tsx b/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/index.tsx index c40b678566..db94361882 100644 --- a/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/index.tsx +++ b/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/index.tsx @@ -53,7 +53,7 @@ export const Footer: React.FC = ({ action, vault, fromAmountTokens if (accountAddress && vault.manager === VaultManager.Venus) { const userStakedTokens = convertMantissaToTokens({ - value: vault.userStakedMantissa || new BigNumber(0), + value: vault.userStakeBalanceMantissa || new BigNumber(0), token: vault.stakedToken, }); @@ -128,7 +128,7 @@ export const Footer: React.FC = ({ action, vault, fromAmountTokens items.push({ label: t('vaultCard.vaultModal.stakeForm.footer.apr'), - children: formatPercentageToReadableValue(vault.stakingAprPercentage), + children: formatPercentageToReadableValue(vault.stakeAprPercentage), }); return ( diff --git a/apps/evm/src/containers/VaultCard/VenusVaultModal/index.tsx b/apps/evm/src/containers/VaultCard/VenusVaultModal/index.tsx index 1651a51b68..a76cfa27c8 100644 --- a/apps/evm/src/containers/VaultCard/VenusVaultModal/index.tsx +++ b/apps/evm/src/containers/VaultCard/VenusVaultModal/index.tsx @@ -29,12 +29,7 @@ export const VenusVaultModal: React.FC = ({ vault, handleC ]; return ( - } - useDrawerInXs - > + }> ); diff --git a/apps/evm/src/containers/VaultCard/__tests__/index.spec.tsx b/apps/evm/src/containers/VaultCard/__tests__/index.spec.tsx new file mode 100644 index 0000000000..4286dd19e9 --- /dev/null +++ b/apps/evm/src/containers/VaultCard/__tests__/index.spec.tsx @@ -0,0 +1,197 @@ +import BigNumber from 'bignumber.js'; +import type { Mock } from 'vitest'; + +import { institutionalVault, vaults as venusVaults } from '__mocks__/models/vaults'; +import { useNow } from 'hooks/useNow'; +import { en, t } from 'libs/translations'; +import { renderComponent } from 'testUtils/render'; +import type { InstitutionalVault, VenusVault } from 'types'; +import { VaultStatus } from 'types'; +import { convertMantissaToTokens, formatTokensToReadableValue } from 'utilities'; +import { VaultCard } from '..'; + +vi.mock('hooks/useNow'); + +const baseVault = institutionalVault; + +const getProgressFill = (container: HTMLElement) => { + const progressFill = container.querySelector('.w-25 > div'); + + expect(progressFill).toBeInTheDocument(); + + return progressFill as HTMLDivElement; +}; + +describe('VaultCard', () => { + const mockUseNow = useNow as Mock; + + beforeEach(() => { + mockUseNow.mockReturnValue(new Date('2026-04-05T00:00:00.000Z')); + }); + + it('renders the institutional checkpoint row for deposit periods', () => { + const { getByText, queryByText } = renderComponent(); + + expect(getByText(en.vault.modals.depositPeriodEnds)).toBeInTheDocument(); + expect( + getByText(t('vault.timeline.textualWithTime', { date: baseVault.openEndDate })), + ).toBeInTheDocument(); + expect(queryByText(en.vault.modals.maturityDate)).not.toBeInTheDocument(); + }); + + it('renders the institutional checkpoint row for pending vaults after the deposit period ends', () => { + mockUseNow.mockReturnValue(new Date('2026-04-09T00:00:00.000Z')); + + const { getByText, queryByText } = renderComponent(); + const maturityDateRow = getByText(en.vault.modals.maturityDate).closest( + '.flex.w-full.justify-between', + ); + + expect(getByText(en.vault.modals.maturityDate)).toBeInTheDocument(); + expect(maturityDateRow).toHaveTextContent( + t('vault.timeline.textualWithTime', { date: baseVault.settlementDate }), + ); + expect(queryByText(en.vault.modals.depositPeriodEnds)).not.toBeInTheDocument(); + }); + + it('falls back to the maturity date in the institutional checkpoint row when no settlement date is available', () => { + mockUseNow.mockReturnValue(new Date('2026-04-09T00:00:00.000Z')); + + const vault = { + ...baseVault, + settlementDate: undefined, + } satisfies InstitutionalVault; + + const { getByText } = renderComponent(); + const maturityDateRow = getByText(en.vault.modals.maturityDate).closest( + '.flex.w-full.justify-between', + ); + + expect(getByText(en.vault.modals.maturityDate)).toBeInTheDocument(); + expect(maturityDateRow).toHaveTextContent( + t('vault.timeline.textualWithTime', { date: vault.maturityDate }), + ); + }); + + it('renders a tbd institutional checkpoint when the deposit period end date is unavailable', () => { + const vault = { + ...baseVault, + openEndDate: undefined, + settlementDate: undefined, + maturityDate: undefined, + } satisfies InstitutionalVault; + + const { getByText, queryByText } = renderComponent(); + + expect(getByText(en.vault.modals.depositPeriodEnds)).toBeInTheDocument(); + expect(getByText(en.vault.timeline.tbd)).toBeInTheDocument(); + expect(queryByText(en.vault.card.maturityDate)).not.toBeInTheDocument(); + }); + + it('renders institutional total deposited as progress against the stake limit', () => { + const { getByText, container } = renderComponent(); + + const readableStakeBalance = formatTokensToReadableValue({ + value: convertMantissaToTokens({ + value: baseVault.stakeBalanceMantissa, + token: baseVault.stakedToken, + }), + token: baseVault.stakedToken, + }); + const readableStakeLimit = formatTokensToReadableValue({ + value: convertMantissaToTokens({ + value: baseVault.stakeLimitMantissa, + token: baseVault.stakedToken, + }), + token: baseVault.stakedToken, + }); + + expect(getByText(`${readableStakeBalance} / ${readableStakeLimit}`)).toBeInTheDocument(); + expect(getByText('50%')).toBeInTheDocument(); + expect(getProgressFill(container)).toHaveClass('bg-blue'); + }); + + it('renders the minimum requested row when the institutional vault has not reached it yet', () => { + const vault = { + ...baseVault, + stakeBalanceMantissa: new BigNumber('5000000'), + } satisfies InstitutionalVault; + + const { getByText } = renderComponent(); + + const readableStakeMinimum = formatTokensToReadableValue({ + value: convertMantissaToTokens({ + value: vault.stakeMinMantissa, + token: vault.stakedToken, + }), + token: vault.stakedToken, + }); + + expect(getByText(en.vault.card.minRequested)).toBeInTheDocument(); + expect(getByText(readableStakeMinimum)).toBeInTheDocument(); + }); + + it('does not render the minimum requested row once the institutional minimum is met', () => { + const { queryByText } = renderComponent(); + + expect(queryByText(en.vault.card.minRequested)).not.toBeInTheDocument(); + }); + + it('renders a green progress bar once the institutional vault reaches 80 percent of the stake limit', () => { + const vault = { + ...baseVault, + stakeBalanceMantissa: new BigNumber('850000000000'), + } satisfies InstitutionalVault; + + const { container, getByText } = renderComponent(); + + expect(getByText('85%')).toBeInTheDocument(); + expect(getProgressFill(container)).toHaveClass('bg-green'); + }); + + it('renders a yellow progress bar override for refunding institutional vaults', () => { + const vault = { + ...baseVault, + status: VaultStatus.Refund, + } satisfies InstitutionalVault; + + const { container } = renderComponent(); + + expect(getProgressFill(container)).toHaveClass('bg-yellow'); + }); + + it('renders the paused warning for institutional paused vaults', () => { + const vault = { + ...baseVault, + status: VaultStatus.Paused, + } satisfies InstitutionalVault; + + const { getByText } = renderComponent(); + + expect(getByText(en.vault.card.pausedWarning)).toBeInTheDocument(); + }); + + it('renders the paused warning for paused Venus vaults', () => { + const vault = { + ...venusVaults[0], + status: VaultStatus.Paused, + isPaused: true, + } satisfies VenusVault; + + const { getByText } = renderComponent(); + + expect(getByText(en.vault.card.pausedWarning)).toBeInTheDocument(); + }); + + it('renders the pending withdrawals warning for paused Venus vaults that are not globally paused', () => { + const vault = { + ...venusVaults[0], + status: VaultStatus.Paused, + isPaused: false, + } satisfies VenusVault; + + const { getByText } = renderComponent(); + + expect(getByText(en.vault.card.blockingPendingWithdrawalsWarning)).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/containers/VaultCard/index.tsx b/apps/evm/src/containers/VaultCard/index.tsx index ef58860e92..1976a3fa13 100644 --- a/apps/evm/src/containers/VaultCard/index.tsx +++ b/apps/evm/src/containers/VaultCard/index.tsx @@ -4,7 +4,6 @@ import { useState } from 'react'; import { Card, Icon, LabeledInlineContent, LayeredValues, NoticeWarning } from 'components'; import { CopyAddressButton } from 'containers/CopyAddressButton'; import useConvertMantissaToReadableTokenString from 'hooks/useConvertMantissaToReadableTokenString'; -import { useNow } from 'hooks/useNow'; import { useTranslation } from 'libs/translations'; import { useAccountAddress } from 'libs/wallet'; import { type Vault, VaultCategory, VaultManager, VaultStatus } from 'types'; @@ -13,11 +12,16 @@ import { formatCentsToReadableValue, formatPercentageToReadableValue, formatTokensToReadableValue, + formatWaitingPeriod, + isInstitutionalVault, isLegacyVenusVault, isPendleVault, } from 'utilities'; +import { InstitutionalCheckpointInlineContent } from './InstitutionalCheckpointInlineContent'; +import { InstitutionalVaultModal } from './InstitutionalVaultModal'; import { PendleVaultModal } from './PendleVaultModal'; import { PrimeEligibilityInlineContent } from './PrimeEligibilityInlineContent'; +import { Progress } from './Progress'; import { StatusLabel } from './StatusLabel'; import { VaultName } from './VaultName'; import { VenusVaultModal } from './VenusVaultModal'; @@ -28,8 +32,7 @@ export interface VaultProps { } export const VaultCard: React.FC = ({ vault, className }) => { - const { t } = useTranslation(); - const now = useNow(); + const { t, language } = useTranslation(); const [modalVisible, setModalVisible] = useState(false); @@ -37,7 +40,7 @@ export const VaultCard: React.FC = ({ vault, className }) => { const readableUserStakedTokens = useConvertMantissaToReadableTokenString({ token: isPendleVault(vault) ? vault.rewardToken : vault.stakedToken, - value: vault.userStakedMantissa, + value: vault.userStakeBalanceMantissa, }); const dailyEmissionMantissa = @@ -52,11 +55,8 @@ export const VaultCard: React.FC = ({ vault, className }) => { ) : undefined; - const hasMatured = - 'maturityDate' in vault && vault.maturityDate && now.getTime() > vault.maturityDate.getTime(); - const formattedMaturityDate = - 'maturityDate' in vault + 'maturityDate' in vault && isPendleVault(vault) && vault.maturityDate ? t('vault.card.textualWithTime', { date: vault.maturityDate, }) @@ -64,20 +64,35 @@ export const VaultCard: React.FC = ({ vault, className }) => { const isInteractive = vault.status === VaultStatus.Active || + vault.status === VaultStatus.Deposit || + vault.status === VaultStatus.Pending || vault.status === VaultStatus.Earning || + vault.status === VaultStatus.Repaying || vault.status === VaultStatus.Claim || - (vault.manager === VaultManager.Pendle && vault.status === VaultStatus.Deposit); + vault.status === VaultStatus.Refund || + (isInstitutionalVault(vault) && vault.status === VaultStatus.Paused); const openModal = () => { setModalVisible(true); }; - let footerLabel: string | undefined = t('vault.card.youDeposited'); - if (hasMatured) { - footerLabel = t('vault.card.claimReward'); - } else if (isLegacyVenusVault(vault)) { - footerLabel = t('vault.card.youDeposited'); - } + const readableLockingPeriod = formatWaitingPeriod({ + waitingPeriodSeconds: vault.lockingPeriodMs ? vault.lockingPeriodMs / 1000 : 0, + locale: language.locale, + }); + + const shouldDisplayInstitutionalMinRequested = + isInstitutionalVault(vault) && vault.stakeBalanceMantissa.lt(vault.stakeMinMantissa); + + const readableInstitutionalMinRequested = shouldDisplayInstitutionalMinRequested + ? formatTokensToReadableValue({ + value: convertMantissaToTokens({ + value: vault.stakeMinMantissa, + token: vault.stakedToken, + }), + token: vault.stakedToken, + }) + : undefined; return ( <> @@ -89,9 +104,7 @@ export const VaultCard: React.FC = ({ vault, className }) => { )} onClick={isInteractive ? openModal : undefined} > - {/* Card body */}
- {/* Header */}
@@ -107,11 +120,10 @@ export const VaultCard: React.FC = ({ vault, className }) => {
- {/* Stats */}
= ({ vault, className }) => { > @@ -164,23 +176,60 @@ export const VaultCard: React.FC = ({ vault, className }) => { )} - - + + {isInstitutionalVault(vault) ? ( + + ) : ( + + )} + {shouldDisplayInstitutionalMinRequested && readableInstitutionalMinRequested && ( + + + + )} + {formattedMaturityDate && ( = ({ vault, className }) => { )} + {isInstitutionalVault(vault) && ( + + )} + {vault.category === VaultCategory.GOVERNANCE && } @@ -207,22 +260,22 @@ export const VaultCard: React.FC = ({ vault, className }) => {
- {/* Warnings */} {vault.status === VaultStatus.Paused && ( )}
- {/* Footer */} {!!accountAddress && readableUserStakedTokens && (
- {footerLabel} + {t('vault.card.youDeposited')}
{readableUserStakedTokens} @@ -246,6 +299,14 @@ export const VaultCard: React.FC = ({ vault, className }) => { handleClose={() => setModalVisible(false)} /> )} + + {isInstitutionalVault(vault) && ( + setModalVisible(false)} + /> + )} ); }; diff --git a/apps/evm/src/containers/VaultCard/useForm/index.tsx b/apps/evm/src/containers/VaultCard/useForm/index.tsx index 4618082b02..a225567845 100644 --- a/apps/evm/src/containers/VaultCard/useForm/index.tsx +++ b/apps/evm/src/containers/VaultCard/useForm/index.tsx @@ -8,18 +8,18 @@ import { useTranslation } from 'libs/translations'; export interface FormValues { fromAmountTokens: string; - acknowledgeHighPriceImpact: boolean; } export const initialFormValues: FormValues = { fromAmountTokens: '', - acknowledgeHighPriceImpact: false, }; export const useForm = ({ + minFromTokens, limitFromTokens, walletSpendingLimitTokens, }: { + minFromTokens?: BigNumber; limitFromTokens?: BigNumber; walletSpendingLimitTokens?: BigNumber; }) => { @@ -37,6 +37,16 @@ export const useForm = ({ return; } + if (minFromTokens && value.isLessThan(minFromTokens)) { + ctx.addIssue({ + code: 'custom', + message: t('operationForm.error.smallerThanMinimumAmount', { + minAmount: minFromTokens, + }), + }); + return; + } + if (limitFromTokens && value.isGreaterThan(limitFromTokens)) { ctx.addIssue({ code: 'custom', diff --git a/apps/evm/src/libs/analytics/useAnalytics/types.ts b/apps/evm/src/libs/analytics/useAnalytics/types.ts index 675f57ccb8..c68124c5f7 100644 --- a/apps/evm/src/libs/analytics/useAnalytics/types.ts +++ b/apps/evm/src/libs/analytics/useAnalytics/types.ts @@ -154,6 +154,11 @@ type PendleVault = AnalyticEvent & { slippageTolerancePercentage: number; }; +type InstitutionalVaultTx = AnalyticEvent & { + vaultAddress: Address; + accountAddress?: Address; +}; + type EventMap = { connect_wallet_initiated: AnalyticEvent; wallet_connected: AnalyticEvent; @@ -235,6 +240,9 @@ type EventMap = { 'Pendle vault deposit': PendleVault; 'Pendle vault withdraw': PendleVault; 'Pendle vault redeemAtMaturity': PendleVault; + 'Institutional vault deposit': InstitutionalVaultTx; + 'Institutional vault redeem': InstitutionalVaultTx; + 'Institutional vault withdraw': InstitutionalVaultTx; }; export type AnalyticEventName = keyof EventMap; diff --git a/apps/evm/src/libs/contracts/abis/institutionalVaultAbi/index.ts b/apps/evm/src/libs/contracts/abis/institutionalVaultAbi/index.ts new file mode 100644 index 0000000000..20211f6d0e --- /dev/null +++ b/apps/evm/src/libs/contracts/abis/institutionalVaultAbi/index.ts @@ -0,0 +1,55 @@ +export const institutionalVaultAbi = [ + { + type: 'function', + stateMutability: 'view', + name: 'balanceOf', + inputs: [{ name: 'account', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + }, + { + type: 'function', + stateMutability: 'nonpayable', + name: 'deposit', + inputs: [ + { name: 'assets', type: 'uint256', internalType: 'uint256' }, + { name: 'receiver', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: 'shares', type: 'uint256', internalType: 'uint256' }], + }, + { + type: 'function', + stateMutability: 'view', + name: 'maxRedeem', + inputs: [{ name: 'owner', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + }, + { + type: 'function', + stateMutability: 'view', + name: 'maxWithdraw', + inputs: [{ name: 'owner', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + }, + { + type: 'function', + stateMutability: 'nonpayable', + name: 'redeem', + inputs: [ + { name: 'shares', type: 'uint256', internalType: 'uint256' }, + { name: 'receiver', type: 'address', internalType: 'address' }, + { name: 'owner', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: 'assets', type: 'uint256', internalType: 'uint256' }], + }, + { + type: 'function', + stateMutability: 'nonpayable', + name: 'withdraw', + inputs: [ + { name: 'assets', type: 'uint256', internalType: 'uint256' }, + { name: 'receiver', type: 'address', internalType: 'address' }, + { name: 'owner', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: 'shares', type: 'uint256', internalType: 'uint256' }], + }, +] as const; diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index e4f8b525e4..27547f4ef8 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1074,6 +1074,7 @@ "noCollateral": "You need to supply tokens and enable them as collateral before you can borrow {{tokenSymbol}} from this pool", "noSwapQuoteFound": "No swap found.", "priceImpactTooHigh": "Price impact too high", + "smallerThanMinimumAmount": "You cannot supply less than {{minAmount}} at once", "supplyCapReached": "The supply cap of {{assetSupplyCap}} has been reached for this pool. You can not supply to this market anymore until withdraws are made or its supply cap is increased.", "tooRisky": "This transaction would get your account liquidated" }, @@ -1986,11 +1987,11 @@ "card": { "apr": "APR", "blockingPendingWithdrawalsWarning": "You must complete pending withdrawals to interact with this vault.", - "claimReward": "Claim reward", "currentDeposited": "Currently deposited", "dailyEmission": "Daily emission", "effectiveFixedApr": "Effective fixed APR", "header": { + "maturityDate_zero": "{{ date, textual }}", "maturityDate_one": "{{ date, textual }} ({{ count }} day)", "maturityDate_other": "{{ date, textual }} ({{ count }} days)" }, @@ -1999,6 +2000,7 @@ "manager": "Manager", "maturityDate": "Maturity date", "maturityDatePendleTooltip": "The end date of this fixed-yield position (from Pendle). After this date, you get back the full original asset (principal). The locked fixed return stops here, good to hold until then for the planned yield.", + "minRequested": "Min. requested", "pausedWarning": "This vault is currently paused. Interest rates are still accruing but claiming, depositing, and withdrawing are disabled until the vault is unpaused", "primeEligibility": "Prime eligibility", "textualWithTime": "{{date, textualWithTime}}", @@ -2019,22 +2021,29 @@ "claim": "Claim", "deposit": "Deposit", "earning": "Earning", + "inactive": "Inactive", "inputPlaceholder": "Search asset", + "liquidated": "Liquidated", "paused": "Paused", + "pending": "Pending", "refund": "Refund", "repaying": "Repaying" }, - "header": "Vault", + "header": "Vaults", "modals": { "afterMaturityPendleDisclaimer": "Please withdraw your PT tokens to your wallet first via the Withdraw button, then visit the Pendle official website to redeem your underlying tokens.", + "claim": "Claim", "connectWalletMessage": "Please connect your wallet to interact with this vault", "convert": "Convert", "currentDeposited": "Currently deposited", "currentDepositedTooltip": "The amount of your assets currently locked and earning in this vault.", "deposit": "Deposit", + "depositPeriodEnds": "Deposit period ends", + "depositsPausedNotice": "Deposits and withdrawals are paused while this vault is progressing through its fixed-rate lifecycle.", "depositTab": "Deposit", "effectiveFixedApr": "Effective fixed APR", "effectiveFixedAprPendleTooltip": "The annualized fixed yield offered by this vault strategy, expressed as an effective Annual Percentage Rate with compounding included.", + "effectiveFixedAprTooltip": "The annualized fixed yield offered by this vault strategy, expressed as an effective Annual Percentage Rate with compounding included.", "error": { "amountTooLow": "Input amount is too low.", "invalidAmountFromQuote": "Amount exceeds available liquidity in the Pendle market.", @@ -2045,20 +2054,35 @@ "estPenalty": "Est. penalty", "estReceived": "Est. received", "estYield": "Est. yield", + "institutionalDisclaimer": "If the vault reaches the minimum required, deposits continue up to the max target. Deposit for {{lockingPeriod}} to claim rewards. Otherwise, funds can be refunded.", + "institutionalTcsAgreement": "By proceeding to deposit, I agree to the institutional vault terms and conditions.", + "institutionalTimeline": { + "claimPeriod": "Claim period", + "depositPeriod": "Deposit period", + "estimatedEarningPeriod": "Estimated earning period", + "estimatedRepayingPeriod": "Estimated repaying period", + "refundPeriod": "Refund period" + }, "maturityDate": "Maturity date", "maturityDatePendleTooltip": "The end date of this fixed-yield position (from Pendle). After this date, you get back the full original asset (principal). The locked fixed return stops here, good to hold until then for the planned yield.", "maturityPendleDisclaimer": "Before maturity, the amount received upon withdrawal is subject to Pendle market prices. After maturity, withdrawals are redeemed 1:1 based on your PT token holdings.", "minReceived": "Min. received", "overview": { + "campaignTimeline": "Campaign timeline", + "loan": "Loan", "manager": "Manager", "managerTooltip": "The address or team that manages this underlying asset of this vault. Deposited assets are allocated to markets they oversee, and they may update redemption or settlement timelines based on market conditions.", "marketInfo": "Market info", "marketRiskDisclosures": "Market risk disclosures", - "riskDisclosureText": "Pendle is a decentralised yield-trading protocol that enables fixed-rate yield through Principal Tokens (PTs). The Venus Fixed Rate Vault routes deposits through Pendle's infrastructure to acquire PTs, which are supplied to Venus Core as collateral.
While Pendle cannot access or move your deposited funds, using this vault exposes you to risks from both protocols. These include smart contract vulnerabilities, oracle failures, and the possibility of either protocol being exploited or paused. Early withdrawals are executed at market price and may return less than the amount deposited. Fixed APY is locked at deposit and does not protect against protocol-level failures.", + "riskDisclosureText": { + "ceffu": "Venus Protocol, accessible at venus.io, is a decentralized lending and borrowing platform built on the BNB Chain that focuses on crypto money markets. Launched in 2020, it combines elements of Maker’s overcollateralized stablecoin design with Compound-style algorithmic money markets, allowing users to supply crypto assets, earn yield, and borrow against their collateral in a single application. The protocol is governed by its community and aims to provide a non-custodial, transparent alternative to centralized lenders.", + "pendle": "Pendle is a decentralised yield-trading protocol that enables fixed-rate yield through Principal Tokens (PTs). The Venus Fixed Rate Vault routes deposits through Pendle's infrastructure to acquire PTs, which are supplied to Venus Core as collateral.
While Pendle cannot access or move your deposited funds, using this vault exposes you to risks from both protocols. These include smart contract vulnerabilities, oracle failures, and the possibility of either protocol being exploited or paused. Early withdrawals are executed at market price and may return less than the amount deposited. Fixed APY is locked at deposit and does not protect against protocol-level failures." + }, "strategy": { "pendleRouter": "Pendle router", "users": "Users", - "venusCore": "Venus Core" + "venusCore": "Venus Core", + "venusVault": "Venus vault" }, "strategyAllocation": "Strategy allocation", "supply": "Supply", @@ -2071,11 +2095,16 @@ "stakeTab": "Deposit", "textualDate": "{{date, textual}}", "textualWithTime": "{{date, textualWithTime}}", + "totalYields": "Total yields", "withdraw": "Withdraw", "withdrawTab": "Withdraw" }, "overview": { "bannerVaultIllustration": "Vault illustration" + }, + "timeline": { + "tbd": "TBD", + "textualWithTime": "{{date, textualWithTime}}" } }, "vaultCard": { diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index a4e4e5c7a7..a1870fd9cc 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1074,6 +1074,7 @@ "noCollateral": "このプールから{{tokenSymbol}}を借りる前に、トークンを供給して担保として有効化する必要があります", "noSwapQuoteFound": "スワップが見つかりません。", "priceImpactTooHigh": "価格影響が高すぎます", + "smallerThanMinimumAmount": "一度に {{minAmount}} 未満を供給することはできません", "supplyCapReached": "このプールの供給上限{{assetSupplyCap}}に達しました。引き出しが行われるか上限が引き上げられるまで、このマーケットに供給できません。", "tooRisky": "この取引はアカウントが清算されます" }, @@ -1986,7 +1987,6 @@ "card": { "apr": "APR", "blockingPendingWithdrawalsWarning": "このボールトを操作するには、保留中の引き出しを完了する必要があります。", - "claimReward": "報酬を請求", "currentDeposited": "現在の預入量", "dailyEmission": "日次報酬配布", "effectiveFixedApr": "実質固定APR", @@ -1998,6 +1998,7 @@ "manager": "運用機関", "maturityDate": "満期日", "maturityDatePendleTooltip": "Pendle によるこの固定利回りポジションの終了日です。この日付以降、元の資産(元本)を全額受け取ることができます。ロックされた固定リターンはここで終了するため、計画した利回りを得るためにはそれまで保有することをお勧めします。", + "minRequested": "Min. requested", "pausedWarning": "このボールトは現在一時停止中です。利率は引き続き発生していますが、ボールトが再開されるまで、請求、預入、引き出しは無効になっています。", "primeEligibility": "Prime資格", "textualWithTime": "{{date, textualWithTime}}", @@ -2018,22 +2019,29 @@ "claim": "請求", "deposit": "預入", "earning": "収益中", + "inactive": "非アクティブ", "inputPlaceholder": "資産を検索", + "liquidated": "清算済み", "paused": "停止中", + "pending": "保留", "refund": "返金", "repaying": "返済中" }, "header": "ボールト", "modals": { "afterMaturityPendleDisclaimer": "まず「引き出し」ボタンからPTトークンをウォレットに引き出してから、Pendle公式ウェブサイトで原資産トークンを償還してください。", + "claim": "請求", "connectWalletMessage": "{{tokenSymbol}}を預け入れ / 引き出しするにはウォレットを接続してください", "convert": "変換", "currentDeposited": "現在の預入額", "currentDepositedTooltip": "このボールトに現在ロックされ、収益を得ている資産の量です。", "deposit": "預入", + "depositPeriodEnds": "預入期間終了", + "depositsPausedNotice": "このボールトが固定利回りのライフサイクルを進行している間は、預入と引き出しが一時停止されます。", "depositTab": "預入", "effectiveFixedApr": "実質固定APR", "effectiveFixedAprPendleTooltip": "このボールトストラテジーが提供する年率換算の固定利回りで、複利を含む実質年利率(APR)として表されます。", + "effectiveFixedAprTooltip": "このボールトストラテジーが提供する年率換算の固定利回りで、複利を含む実質年利率(APR)として表されます。", "error": { "amountTooLow": "入力金額が少なすぎます。", "invalidAmountFromQuote": "入力金額がPendleマーケットで利用可能な流動性を超えています。金額を減らして再試行してください。", @@ -2044,20 +2052,35 @@ "estPenalty": "推定ペナルティ", "estReceived": "推定受取額", "estYield": "推定利回り", + "institutionalDisclaimer": "ボールトが必要最低額に達した場合、預入は最大目標額に達するまで継続されます。{{lockingPeriod}} 預け入れると報酬を請求できます。そうでない場合、資金は返金されます。", + "institutionalTcsAgreement": "預け入れを続行することで、機関投資家向けボールトの利用規約に同意したものとみなされます。", + "institutionalTimeline": { + "claimPeriod": "請求期間", + "depositPeriod": "預入期間", + "estimatedEarningPeriod": "予想収益期間", + "estimatedRepayingPeriod": "予想返済期間", + "refundPeriod": "返金期間" + }, "maturityDate": "満期日", "maturityDatePendleTooltip": "Pendle によるこの固定利回りポジションの終了日です。この日付以降、元の資産(元本)を全額受け取ることができます。ロックされた固定リターンはここで終了するため、計画した利回りを得るためにはそれまで保有することをお勧めします。", "maturityPendleDisclaimer": "満期前は、引き出し時に受け取る金額は Pendle の市場価格に依存します。満期後は、保有する PT トークンに基づいて 1:1 で引き出しが換金されます。", "minReceived": "最小受取額", "overview": { + "campaignTimeline": "キャンペーンタイムライン", + "loan": "ローン", "manager": "運用機関", "managerTooltip": "このボールトの基礎資産を管理するアドレスまたはチームです。預けた資産は彼らが管理する市場に配分され、市場状況に応じて償還や決済のスケジュールが更新される場合があります。", "marketInfo": "マーケット情報", "marketRiskDisclosures": "マーケットリスク開示", - "riskDisclosureText": "Pendle は、プリンシパルトークン(PT)を通じて固定利回りを実現する分散型イールドトレーディングプロトコルです。Venus 固定金利ボールトは、Pendle のインフラストラクチャを通じて PT を取得し、Venus Core に担保として供給します。
Pendle があなたの預金にアクセスしたり移動させたりすることはできませんが、このボールトを利用することで両方のプロトコルからのリスクにさらされます。これにはスマートコントラクトの脆弱性、オラクルの障害、いずれかのプロトコルが悪用または停止される可能性が含まれます。満期前の引き出しは市場価格で実行され、預入額を下回る場合があります。固定 APY は預入時にロックされ、プロトコルレベルの障害に対する保護にはなりません。", + "riskDisclosureText": { + "ceffu": "TRANSLATION NEEDED", + "pendle": "TRANSLATION NEEDED" + }, "strategy": { "pendleRouter": "Pendle Router", "users": "ユーザー", - "venusCore": "Venus Core" + "venusCore": "Venus Core", + "venusVault": "Venusボールト" }, "strategyAllocation": "戦略配分", "supply": "供給", @@ -2070,11 +2093,16 @@ "stakeTab": "預入", "textualDate": "{{date, textual}}", "textualWithTime": "{{date, textualWithTime}}", + "totalYields": "総利回り", "withdraw": "引き出し", "withdrawTab": "引き出し" }, "overview": { "bannerVaultIllustration": "ボールトのイラスト" + }, + "timeline": { + "tbd": "未定", + "textualWithTime": "{{date, textualWithTime}}" } }, "vaultCard": { diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index 6a25ba8d51..b4aa741512 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1074,6 +1074,7 @@ "noCollateral": "คุณต้องฝากโทเคนและเปิดใช้เป็นหลักประกันก่อนจึงจะยืม {{tokenSymbol}} จากพูลนี้ได้", "noSwapQuoteFound": "ไม่พบการสวอป", "priceImpactTooHigh": "ผลกระทบต่อราคาสูงเกินไป", + "smallerThanMinimumAmount": "คุณไม่สามารถฝากน้อยกว่า {{minAmount}} ต่อครั้งได้", "supplyCapReached": "ขีดจำกัดการฝากของ {{assetSupplyCap}} สำหรับพูลนี้ถึงแล้ว คุณไม่สามารถฝากเข้าสู่ตลาดนี้ได้จนกว่าจะมีการถอนหรือเพิ่มขีดจำกัดการฝาก", "tooRisky": "ธุรกรรมนี้จะทำให้บัญชีของคุณถูกชำระบัญชี" }, @@ -1986,7 +1987,6 @@ "card": { "apr": "APR", "blockingPendingWithdrawalsWarning": "คุณต้องดำเนินการถอนเงินที่รอดำเนินการให้เสร็จสิ้นก่อนจึงจะสามารถโต้ตอบกับ vault นี้ได้", - "claimReward": "รับรางวัล", "currentDeposited": "ฝากอยู่ในปัจจุบัน", "dailyEmission": "รางวัลรายวัน", "effectiveFixedApr": "APR คงที่ที่มีผล", @@ -1998,6 +1998,7 @@ "manager": "ผู้ดูแล", "maturityDate": "วันครบกำหนด", "maturityDatePendleTooltip": "วันสิ้นสุดของตำแหน่งผลตอบแทนคงที่นี้ (จาก Pendle) หลังจากวันนี้ คุณจะได้รับสินทรัพย์ต้นฉบับครบถ้วน (เงินต้น) ผลตอบแทนคงที่ที่ล็อคไว้หยุดที่นี่ ควรถือครองจนถึงวันนั้นเพื่อรับผลตอบแทนตามแผน", + "minRequested": "Min. requested", "pausedWarning": "vault นี้ถูกหยุดชั่วคราว อัตราดอกเบี้ยยังคงสะสมอยู่ แต่การอ้างสิทธิ์ การฝาก และการถอนเงินถูกปิดใช้งานจนกว่า vault จะกลับมาทำงานอีกครั้ง", "primeEligibility": "สิทธิ์รับ Prime", "textualWithTime": "{{date, textualWithTime}}", @@ -2018,22 +2019,29 @@ "claim": "อ้างสิทธิ์", "deposit": "ฝาก", "earning": "กำลังรับผลตอบแทน", + "inactive": "ไม่ใช้งาน", "inputPlaceholder": "ค้นหาสินทรัพย์", + "liquidated": "ถูกชำระบัญชี", "paused": "หยุดชั่วคราว", + "pending": "รอดำเนินการ", "refund": "คืนเงิน", "repaying": "กำลังชำระคืน" }, "header": "วอลต์", "modals": { "afterMaturityPendleDisclaimer": "กรุณาถอน PT token ไปยังวอลเล็ตของคุณก่อนผ่านปุ่มถอน จากนั้นไปที่เว็บไซต์Pendle อย่างเป็นทางการเพื่อแลกรับ underlying token ของคุณ", + "claim": "อ้างสิทธิ์", "connectWalletMessage": "กรุณาเชื่อมต่อกระเป๋าเงินของคุณเพื่อฝาก / ถอน {{tokenSymbol}}", "convert": "แปลง", "currentDeposited": "ยอดฝากปัจจุบัน", "currentDepositedTooltip": "จำนวนสินทรัพย์ของคุณที่ล็อคและสร้างรายได้อยู่ในห้องนิรภัยนี้", "deposit": "ฝาก", + "depositPeriodEnds": "ระยะเวลาฝากสิ้นสุด", + "depositsPausedNotice": "การฝากและถอนจะถูกหยุดชั่วคราวในขณะที่ vault นี้กำลังดำเนินไปตามวงจรอัตราคงที่", "depositTab": "ฝาก", "effectiveFixedApr": "APR คงที่ที่มีผล", "effectiveFixedAprPendleTooltip": "ผลตอบแทนคงที่รายปีที่เสนอโดยกลยุทธ์ vault นี้ แสดงเป็นอัตราร้อยละต่อปีที่มีผลซึ่งรวมดอกเบี้ยทบต้นแล้ว", + "effectiveFixedAprTooltip": "ผลตอบแทนคงที่รายปีที่เสนอโดยกลยุทธ์ vault นี้ แสดงเป็นอัตราร้อยละต่อปีที่มีผลซึ่งรวมดอกเบี้ยทบต้นแล้ว", "error": { "amountTooLow": "จำนวนที่ป้อนต่ำเกินไป", "invalidAmountFromQuote": "จำนวนเกินกว่าสภาพคล่องที่ใช้ได้ในตลาด Pendle ลดจำนวนแล้วลองอีกครั้ง", @@ -2044,20 +2052,35 @@ "estPenalty": "ค่าปรับโดยประมาณ", "estReceived": "ที่จะได้รับโดยประมาณ", "estYield": "ผลตอบแทนโดยประมาณ", + "institutionalDisclaimer": "หาก vault ถึงจำนวนขั้นต่ำที่กำหนด การฝากจะดำเนินต่อไปจนถึงเป้าหมายสูงสุด ฝากเป็นเวลา {{lockingPeriod}} เพื่ออ้างสิทธิ์รับรางวัล มิฉะนั้น เงินทุนสามารถขอคืนได้", + "institutionalTcsAgreement": "เมื่อดำเนินการฝากต่อ แสดงว่าฉันยอมรับข้อกำหนดและเงื่อนไขของ institutional vault", + "institutionalTimeline": { + "claimPeriod": "ระยะเวลาอ้างสิทธิ์", + "depositPeriod": "ระยะเวลาฝาก", + "estimatedEarningPeriod": "ระยะเวลารับผลตอบแทนโดยประมาณ", + "estimatedRepayingPeriod": "ระยะเวลาชำระคืนโดยประมาณ", + "refundPeriod": "ระยะเวลาคืนเงิน" + }, "maturityDate": "วันครบกำหนด", "maturityDatePendleTooltip": "วันสิ้นสุดของตำแหน่งผลตอบแทนคงที่นี้ (จาก Pendle) หลังจากวันนี้ คุณจะได้รับสินทรัพย์ต้นฉบับครบถ้วน (เงินต้น) ผลตอบแทนคงที่ที่ล็อคไว้หยุดที่นี่ ควรถือครองจนถึงวันนั้นเพื่อรับผลตอบแทนตามแผน", "maturityPendleDisclaimer": "ก่อนครบกำหนด จำนวนเงินที่ได้รับเมื่อถอนขึ้นอยู่กับราคาตลาด Pendle หลังจากครบกำหนด การถอนจะถูกแลกตามจำนวน PT token ที่คุณถือครองในอัตรา 1:1", "minReceived": "ขั้นต่ำที่จะได้รับ", "overview": { + "campaignTimeline": "ไทม์ไลน์แคมเปญ", + "loan": "เงินกู้", "manager": "ผู้ดูแล", "managerTooltip": "ที่อยู่หรือทีมที่จัดการสินทรัพย์อ้างอิงของ vault นี้ สินทรัพย์ที่ฝากจะถูกจัดสรรไปยังตลาดที่พวกเขาดูแล และพวกเขาอาจอัปเดตระยะเวลาการไถ่ถอนหรือการชำระบัญชีตามสภาวะตลาด", "marketInfo": "ข้อมูลตลาด", "marketRiskDisclosures": "การเปิดเผยความเสี่ยงของตลาด", - "riskDisclosureText": "Pendle เป็นโปรโตคอลการซื้อขายผลตอบแทนแบบกระจายศูนย์ที่ช่วยให้ได้รับผลตอบแทนอัตราคงที่ผ่าน Principal Tokens (PTs) Venus Fixed Rate Vault จะส่งเงินฝากผ่านโครงสร้างพื้นฐานของ Pendle เพื่อซื้อ PT ซึ่งจะถูกนำไปเป็นหลักประกันใน Venus Core
แม้ว่า Pendle จะไม่สามารถเข้าถึงหรือย้ายเงินฝากของคุณได้ แต่การใช้ vault นี้ทำให้คุณเผชิญกับความเสี่ยงจากทั้งสองโปรโตคอล ซึ่งรวมถึงช่องโหว่ของสัญญาอัจฉริยะ ความล้มเหลวของ oracle และความเป็นไปได้ที่โปรโตคอลใดโปรโตคอลหนึ่งจะถูกโจมตีหรือหยุดชั่วคราว การถอนก่อนกำหนดจะดำเนินการตามราคาตลาดและอาจได้รับคืนน้อยกว่าจำนวนที่ฝาก APY คงที่จะถูกล็อคไว้เมื่อฝากและไม่สามารถป้องกันความล้มเหลวในระดับโปรโตคอล", + "riskDisclosureText": { + "ceffu": "TRANSLATION NEEDED", + "pendle": "TRANSLATION NEEDED" + }, "strategy": { "pendleRouter": "Pendle Router", "users": "ผู้ใช้งาน", - "venusCore": "Venus Core" + "venusCore": "Venus Core", + "venusVault": "วอลต์ Venus" }, "strategyAllocation": "การจัดสรรกลยุทธ์", "supply": "ฝาก", @@ -2070,11 +2093,16 @@ "stakeTab": "ฝาก", "textualDate": "{{date, textual}}", "textualWithTime": "{{date, textualWithTime}}", + "totalYields": "ผลตอบแทนรวม", "withdraw": "ถอน", "withdrawTab": "ถอน" }, "overview": { "bannerVaultIllustration": "ภาพประกอบคลัง" + }, + "timeline": { + "tbd": "TBD", + "textualWithTime": "{{date, textualWithTime}}" } }, "vaultCard": { diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index a84025ba57..5dcd426c2f 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1074,6 +1074,7 @@ "noCollateral": "Bu havuzdan {{tokenSymbol}} borç alabilmek için önce tokenları sağlayıp teminat olarak etkinleştirmelisiniz", "noSwapQuoteFound": "Swap bulunamadı.", "priceImpactTooHigh": "Fiyat etkisi çok yüksek", + "smallerThanMinimumAmount": "Bir seferde {{minAmount}} değerinden daha az sağlayamazsınız", "supplyCapReached": "Bu havuz için {{assetSupplyCap}} tedarik limiti aşıldı. Çekimler yapılana veya limit artırılana kadar bu piyasaya sağlayamazsınız.", "tooRisky": "Bu işlem hesabınızın likide olmasına neden olur" }, @@ -1986,7 +1987,6 @@ "card": { "apr": "APR", "blockingPendingWithdrawalsWarning": "Bu kasayla etkileşime geçmek için bekleyen çekimleri tamamlamanız gerekmektedir.", - "claimReward": "Ödülü talep et", "currentDeposited": "Mevcut yatırım", "dailyEmission": "Günlük ödül dağıtımı", "effectiveFixedApr": "Efektif Sabit APR", @@ -1999,6 +1999,7 @@ "manager": "Yönetici kurum", "maturityDate": "Vade Tarihi", "maturityDatePendleTooltip": "Bu sabit getirili pozisyonun (Pendle'dan) bitiş tarihidir. Bu tarihten sonra orijinal varlığın tamamını (anapara) geri alırsınız. Kilitli sabit getiri burada durur; planlanan getiriyi elde etmek için o tarihe kadar tutmanız önerilir.", + "minRequested": "Min. requested", "pausedWarning": "Bu kasa şu anda duraklatılmış durumda. Faiz oranları tahakkuk etmeye devam ediyor ancak kasa yeniden aktif olana kadar talep, yatırım ve çekim işlemleri devre dışı bırakılmıştır.", "primeEligibility": "Prime uygunluğu", "textualWithTime": "{{date, textualWithTime}}", @@ -2019,22 +2020,29 @@ "claim": "Talep et", "deposit": "Yatır", "earning": "Kazanç", + "inactive": "Aktif değil", "inputPlaceholder": "Varlık ara", + "liquidated": "Tasfiye edildi", "paused": "Duraklatıldı", + "pending": "Beklemede", "refund": "İade", "repaying": "Geri ödeniyor" }, "header": "Kasa", "modals": { "afterMaturityPendleDisclaimer": "Lütfen önce Çek butonu ile PT tokenlarınızı cüzdanınıza çekin, ardından temel tokenlarınızı geri almak için Pendle resmi web sitesini ziyaret edin.", + "claim": "Talep et", "connectWalletMessage": "{{tokenSymbol}} yatırmak / çekmek için cüzdanınızı bağlayın", "convert": "Dönüştür", "currentDeposited": "Mevcut yatırılan", "currentDepositedTooltip": "Bu kasada şu anda kilitli olan ve kazanç sağlayan varlıklarınızın miktarı.", "deposit": "Yatır", + "depositPeriodEnds": "Yatırma dönemi sona eriyor", + "depositsPausedNotice": "Bu kasa sabit getirili yaşam döngüsünde ilerlerken yatırma ve çekme işlemleri duraklatılır.", "depositTab": "Yatır", "effectiveFixedApr": "Efektif Sabit APR", "effectiveFixedAprPendleTooltip": "Bu kasa stratejisinin sunduğu yıllık sabit getiri; bileşik faiz dahil efektif Yıllık Yüzde Oranı (APR) olarak ifade edilmektedir.", + "effectiveFixedAprTooltip": "Bu kasa stratejisinin sunduğu yıllık sabit getiri; bileşik faiz dahil efektif Yıllık Yüzde Oranı (APR) olarak ifade edilmektedir.", "error": { "amountTooLow": "Girilen miktar çok düşük.", "invalidAmountFromQuote": "Girilen miktar, Pendle piyasasındaki kullanılabilir likidityi aşıyor. Miktarı azaltın ve tekrar deneyin.", @@ -2045,20 +2053,35 @@ "estPenalty": "Tahmini Ceza", "estReceived": "Tahmini Alınan", "estYield": "Tahmini Getiri", + "institutionalDisclaimer": "Kasa gerekli minimuma ulaşırsa, yatırımlar maksimum hedefe kadar devam eder. Ödülleri talep etmek için {{lockingPeriod}} boyunca yatırım yapın. Aksi halde fonlar iade edilebilir.", + "institutionalTcsAgreement": "Yatırmaya devam ederek kurumsal kasa hüküm ve koşullarını kabul ediyorum.", + "institutionalTimeline": { + "claimPeriod": "Talep dönemi", + "depositPeriod": "Yatırma dönemi", + "estimatedEarningPeriod": "Tahmini kazanç dönemi", + "estimatedRepayingPeriod": "Tahmini geri ödeme dönemi", + "refundPeriod": "İade dönemi" + }, "maturityDate": "Vade Tarihi", "maturityDatePendleTooltip": "Bu sabit getirili pozisyonun (Pendle'dan) bitiş tarihidir. Bu tarihten sonra orijinal varlığın tamamını (anapara) geri alırsınız. Kilitli sabit getiri burada durur; planlanan getiriyi elde etmek için o tarihe kadar tutmanız önerilir.", "maturityPendleDisclaimer": "Vade öncesinde, çekim sırasında alınan tutar Pendle piyasa fiyatlarına tabidir. Vade sonrasında, çekimler sahip olduğunuz PT token miktarına göre 1:1 oranında itfa edilir.", "minReceived": "Min. Alınan", "overview": { + "campaignTimeline": "Kampanya zaman çizelgesi", + "loan": "Kredi", "manager": "Yönetici kurum", "managerTooltip": "Bu kasanın temel varlığını yöneten adres veya ekip. Yatırılan varlıklar, denetledikleri piyasalara tahsis edilir ve piyasa koşullarına göre itfa veya uzlaşma sürelerini güncelleyebilirler.", "marketInfo": "Piyasa bilgisi", "marketRiskDisclosures": "Piyasa Risk Açıklamaları", - "riskDisclosureText": "Pendle, Temel Tokenlar (PT'ler) aracılığıyla sabit oranlı getiri sağlayan merkeziyetsiz bir getiri ticaret protokolüdür. Venus Sabit Faiz Kasası, PT'leri edinmek için mevduatları Pendle'ın altyapısı üzerinden yönlendirir ve bunlar Venus Core'a teminat olarak sağlanır.
Pendle yatırdığınız fonlara erişemez veya taşıyamaz olsa da bu kasayı kullanmak sizi her iki protokolden kaynaklanan risklere maruz bırakır. Bunlar arasında akıllı sözleşme güvenlik açıkları, oracle arızaları ve her iki protokolün de istismar edilmesi veya duraklatılması olasılığı yer alır. Erken para çekme işlemleri piyasa fiyatından gerçekleştirilir ve yatırılan tutardan daha az iade edilebilir. Sabit APY yatırım sırasında kilitlenir ve protokol düzeyindeki arızalara karşı koruma sağlamaz.", + "riskDisclosureText": { + "ceffu": "TRANSLATION NEEDED", + "pendle": "TRANSLATION NEEDED" + }, "strategy": { "pendleRouter": "Pendle Router", "users": "Kullanıcılar", - "venusCore": "Venus Core" + "venusCore": "Venus Core", + "venusVault": "Venus kasası" }, "strategyAllocation": "Strateji Dağılımı", "supply": "Sağla", @@ -2071,11 +2094,16 @@ "stakeTab": "Yatır", "textualDate": "{{date, textual}}", "textualWithTime": "{{date, textualWithTime}}", + "totalYields": "Toplam Getiri", "withdraw": "Çek", "withdrawTab": "Çek" }, "overview": { "bannerVaultIllustration": "Kasa görseli" + }, + "timeline": { + "tbd": "TBD", + "textualWithTime": "{{date, textualWithTime}}" } }, "vaultCard": { diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index 6dfbca4ece..892c5eac7f 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1074,6 +1074,7 @@ "noCollateral": "Bạn cần cung cấp token và bật làm tài sản thế chấp trước khi có thể vay {{tokenSymbol}} từ pool này", "noSwapQuoteFound": "Không tìm thấy giao dịch hoán đổi.", "priceImpactTooHigh": "Tác động giá quá cao", + "smallerThanMinimumAmount": "Bạn không thể cung cấp ít hơn {{minAmount}} trong một lần", "supplyCapReached": "Hạn mức cung cấp {{assetSupplyCap}} của pool này đã đạt. Bạn không thể cung cấp vào thị trường này cho đến khi có rút hoặc hạn mức cung cấp được tăng.", "tooRisky": "Giao dịch này sẽ khiến tài khoản của bạn bị thanh lý" }, @@ -1986,7 +1987,6 @@ "card": { "apr": "APR", "blockingPendingWithdrawalsWarning": "Bạn phải hoàn thành các lệnh rút tiền đang chờ xử lý để tương tác với kho này.", - "claimReward": "Nhận thưởng", "currentDeposited": "Đang gửi", "dailyEmission": "Phần thưởng hàng ngày", "effectiveFixedApr": "APR Cố Định Hiệu Quả", @@ -1998,6 +1998,7 @@ "manager": "Tổ chức quản lý", "maturityDate": "Ngày đáo hạn", "maturityDatePendleTooltip": "Ngày kết thúc của vị thế lãi suất cố định này (từ Pendle). Sau ngày này, bạn sẽ nhận lại toàn bộ tài sản gốc (vốn). Lợi nhuận cố định đã khóa dừng tại đây - tốt nhất nên giữ đến lúc đó để đạt được lợi nhuận đã lên kế hoạch.", + "minRequested": "Min. requested", "pausedWarning": "Kho này hiện đang tạm dừng. Lãi suất vẫn đang tích lũy nhưng việc nhận thưởng, gửi tiền và rút tiền bị vô hiệu hóa cho đến khi kho được mở lại.", "primeEligibility": "Điều kiện nhận Prime", "textualWithTime": "{{date, textualWithTime}}", @@ -2018,22 +2019,29 @@ "claim": "Nhận thưởng", "deposit": "Gửi", "earning": "Đang kiếm lợi nhuận", + "inactive": "Không hoạt động", "inputPlaceholder": "Tìm kiếm tài sản", + "liquidated": "Đã thanh lý", "paused": "Tạm dừng", + "pending": "Đang chờ", "refund": "Hoàn tiền", "repaying": "Đang trả nợ" }, "header": "Kho", "modals": { "afterMaturityPendleDisclaimer": "Vui lòng rút PT token về ví của bạn trước thông qua nút Rút, sau đó truy cập trang web Pendle chính thức để đổi lấy token cơ sở của bạn.", + "claim": "Nhận thưởng", "connectWalletMessage": "Vui lòng kết nối ví của bạn để gửi / rút {{tokenSymbol}}", "convert": "Chuyển đổi", "currentDeposited": "Đang gửi hiện tại", "currentDepositedTooltip": "Số lượng tài sản của bạn hiện đang được khóa và sinh lời trong kho tiền này.", "deposit": "Gửi", + "depositPeriodEnds": "Giai đoạn gửi tiền kết thúc", + "depositsPausedNotice": "Việc gửi và rút tiền sẽ bị tạm dừng trong khi kho này đang chuyển qua vòng đời lãi suất cố định của nó.", "depositTab": "Gửi", "effectiveFixedApr": "APR Cố Định Hiệu Quả", "effectiveFixedAprPendleTooltip": "Lợi nhuận cố định hàng năm được cung cấp bởi chiến lược vault này, được thể hiện dưới dạng Tỷ lệ phần trăm hàng năm hiệu quả bao gồm cả lãi kép.", + "effectiveFixedAprTooltip": "Lợi nhuận cố định hàng năm được cung cấp bởi chiến lược vault này, được thể hiện dưới dạng Tỷ lệ phần trăm hàng năm hiệu quả bao gồm cả lãi kép.", "error": { "amountTooLow": "Số tiền nhập vào quá thấp.", "invalidAmountFromQuote": "Số tiền vượt quá thanh khoản có sẵn trên thị trường Pendle. Giảm số tiền và thử lại.", @@ -2044,20 +2052,35 @@ "estPenalty": "Phạt ước tính", "estReceived": "Ước tính nhận được", "estYield": "Lợi nhuận ước tính", + "institutionalDisclaimer": "Nếu kho đạt mức tối thiểu yêu cầu, tiền gửi sẽ tiếp tục cho đến mục tiêu tối đa. Gửi trong {{lockingPeriod}} để nhận thưởng. Nếu không, tiền có thể được hoàn lại.", + "institutionalTcsAgreement": "Bằng việc tiếp tục gửi tiền, tôi đồng ý với điều khoản và điều kiện của kho tổ chức.", + "institutionalTimeline": { + "claimPeriod": "Giai đoạn nhận thưởng", + "depositPeriod": "Giai đoạn gửi tiền", + "estimatedEarningPeriod": "Giai đoạn sinh lời ước tính", + "estimatedRepayingPeriod": "Giai đoạn hoàn trả ước tính", + "refundPeriod": "Giai đoạn hoàn tiền" + }, "maturityDate": "Ngày đáo hạn", "maturityDatePendleTooltip": "Ngày kết thúc của vị thế lãi suất cố định này (từ Pendle). Sau ngày này, bạn sẽ nhận lại toàn bộ tài sản gốc (vốn). Lợi nhuận cố định đã khóa dừng tại đây - tốt nhất nên giữ đến lúc đó để đạt được lợi nhuận đã lên kế hoạch.", "maturityPendleDisclaimer": "Trước khi đáo hạn, số tiền nhận được khi rút phụ thuộc vào giá thị trường Pendle. Sau khi đáo hạn, các khoản rút được quy đổi theo tỷ lệ 1:1 dựa trên số PT token bạn nắm giữ.", "minReceived": "Tối thiểu nhận được", "overview": { + "campaignTimeline": "Dòng thời gian chiến dịch", + "loan": "Khoản vay", "manager": "Quản lý", "managerTooltip": "Địa chỉ hoặc nhóm quản lý tài sản cơ sở của kho này. Tài sản được gửi sẽ được phân bổ vào các thị trường mà họ giám sát, và họ có thể cập nhật thời hạn mua lại hoặc thanh toán dựa trên điều kiện thị trường.", "marketInfo": "Thông tin thị trường", "marketRiskDisclosures": "Công bố rủi ro thị trường", - "riskDisclosureText": "Pendle là một giao thức giao dịch lợi suất phi tập trung cho phép lợi suất cố định thông qua Token Gốc (PT). Venus Fixed Rate Vault chuyển tiền gửi qua cơ sở hạ tầng của Pendle để mua PT, sau đó được cung cấp cho Venus Core làm tài sản thế chấp.
Mặc dù Pendle không thể truy cập hoặc di chuyển tiền gửi của bạn, việc sử dụng kho này khiến bạn phải chịu rủi ro từ cả hai giao thức. Những rủi ro này bao gồm lỗ hổng hợp đồng thông minh, lỗi oracle và khả năng một trong hai giao thức bị khai thác hoặc tạm dừng. Rút tiền sớm được thực hiện theo giá thị trường và có thể nhận lại ít hơn số tiền đã gửi. APY cố định được khóa tại thời điểm gửi và không bảo vệ khỏi các lỗi ở cấp giao thức.", + "riskDisclosureText": { + "ceffu": "TRANSLATION NEEDED", + "pendle": "TRANSLATION NEEDED" + }, "strategy": { "pendleRouter": "Pendle Router", "users": "Người dùng", - "venusCore": "Venus Core" + "venusCore": "Venus Core", + "venusVault": "Kho Venus" }, "strategyAllocation": "Phân bổ chiến lược", "supply": "Cung cấp", @@ -2070,11 +2093,16 @@ "stakeTab": "Gửi", "textualDate": "{{date, textual}}", "textualWithTime": "{{date, textualWithTime}}", + "totalYields": "Tổng lợi nhuận", "withdraw": "Rút", "withdrawTab": "Rút" }, "overview": { "bannerVaultIllustration": "Hình minh họa kho" + }, + "timeline": { + "tbd": "TBD", + "textualWithTime": "{{date, textualWithTime}}" } }, "vaultCard": { diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index 508c06a11d..b69e3cd2d1 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1074,6 +1074,7 @@ "noCollateral": "在从该池借入 {{tokenSymbol}} 之前,你需要先存款代币并将其设为可抵押", "noSwapQuoteFound": "未找到兑换。", "priceImpactTooHigh": "价格影响过高", + "smallerThanMinimumAmount": "你不能一次存入少于 {{minAmount}}", "supplyCapReached": "该池存款上限 {{assetSupplyCap}} 已达。除非有人取款或提高存款上限,否则无法再向该市场存款。", "tooRisky": "该交易会导致账户被清算" }, @@ -1986,7 +1987,6 @@ "card": { "apr": "APR", "blockingPendingWithdrawalsWarning": "您必须先完成待处理的提款才能与此金库互动。", - "claimReward": "领取奖励", "currentDeposited": "当前存款", "dailyEmission": "每日奖励释放", "effectiveFixedApr": "实际固定年化利率", @@ -1998,6 +1998,7 @@ "manager": "管理机构", "maturityDate": "到期日", "maturityDatePendleTooltip": "此固定收益头寸(来自 Pendle)的到期日。在此日期之后,您将收回全部原始资产(本金)。锁定的固定收益在此停止,建议持有至此以获得预期收益。", + "minRequested": "Min. requested", "pausedWarning": "此金库目前已暂停。利率仍在累积,但领取、存入和提款功能已禁用,直至金库恢复运作。", "primeEligibility": "Prime 资格", "textualWithTime": "{{date, textualWithTime}}", @@ -2018,22 +2019,29 @@ "claim": "领取", "deposit": "存款", "earning": "收益中", + "inactive": "非活跃", "inputPlaceholder": "搜索资产", + "liquidated": "已清算", "paused": "已暂停", + "pending": "待定", "refund": "退款", "repaying": "还款中" }, "header": "金库", "modals": { "afterMaturityPendleDisclaimer": "请先通过取款按钮将您的 PT 代币提取到钱包,然后访问 Pendle 官方网站赎回您的底层代币。", + "claim": "领取", "connectWalletMessage": "请连接您的钱包以存入 / 提取 {{tokenSymbol}}", "convert": "转换", "currentDeposited": "当前存入", "currentDepositedTooltip": "您当前在此金库中锁定并产生收益的资产数量。", "deposit": "存款", + "depositPeriodEnds": "存款期结束", + "depositsPausedNotice": "当该金库处于固定利率生命周期推进过程中时,存款和提款将暂停。", "depositTab": "存款", "effectiveFixedApr": "实际固定年化利率", "effectiveFixedAprPendleTooltip": "此金库策略提供的年化固定收益,以包含复利的有效年化利率(APR)表示。", + "effectiveFixedAprTooltip": "此金库策略提供的年化固定收益,以包含复利的有效年化利率(APR)表示。", "error": { "amountTooLow": "输入金额过低。", "invalidAmountFromQuote": "输入金额超过Pendle市场的可用流动性。请减少金额后重试。", @@ -2044,20 +2052,35 @@ "estPenalty": "预计罚款", "estReceived": "预计收到", "estYield": "预计收益", + "institutionalDisclaimer": "如果金库达到所需最低额度,存款将继续进行直至达到最高目标。存入 {{lockingPeriod}} 后即可领取奖励。否则,资金可退款。", + "institutionalTcsAgreement": "继续存入即表示我同意机构金库条款与条件。", + "institutionalTimeline": { + "claimPeriod": "领取期", + "depositPeriod": "存款期", + "estimatedEarningPeriod": "预计收益期", + "estimatedRepayingPeriod": "预计还款期", + "refundPeriod": "退款期" + }, "maturityDate": "到期日", "maturityDatePendleTooltip": "此固定收益头寸(来自 Pendle)的到期日。在此日期之后,您将收回全部原始资产(本金)。锁定的固定收益在此停止,建议持有至此以获得预期收益。", "maturityPendleDisclaimer": "到期前,提款时收到的金额取决于 Pendle 市场价格。到期后,提款将根据您持有的 PT 代币 1:1 赎回。", "minReceived": "最低收到", "overview": { + "campaignTimeline": "活动时间线", + "loan": "贷款", "manager": "管理机构", "managerTooltip": "管理此金库基础资产的地址或团队。存入的资产会被分配到他们管理的市场中,他们可能会根据市场状况更新赎回或结算时间。", "marketInfo": "市场信息", "marketRiskDisclosures": "市场风险披露", - "riskDisclosureText": "Pendle 是一个去中心化的收益交易协议,通过本金代币(PT)实现固定收益。Venus 固定利率金库通过 Pendle 的基础设施获取 PT,并将其作为抵押品供应给 Venus Core。
虽然 Pendle 无法访问或转移您的存款,但使用此金库会使您面临来自两个协议的风险。这些风险包括智能合约漏洞、预言机故障,以及任一协议被攻击或暂停的可能性。提前提款按市场价格执行,可能低于存入金额。固定 APY 在存入时锁定,无法防范协议层面的故障。", + "riskDisclosureText": { + "ceffu": "TRANSLATION NEEDED", + "pendle": "TRANSLATION NEEDED" + }, "strategy": { "pendleRouter": "Pendle Router", "users": "用户", - "venusCore": "Venus Core" + "venusCore": "Venus Core", + "venusVault": "Venus 金库" }, "strategyAllocation": "策略分配", "supply": "质押", @@ -2070,11 +2093,16 @@ "stakeTab": "存款", "textualDate": "{{date, textual}}", "textualWithTime": "{{date, textualWithTime}}", + "totalYields": "总收益", "withdraw": "取款", "withdrawTab": "取款" }, "overview": { "bannerVaultIllustration": "金库插图" + }, + "timeline": { + "tbd": "待定", + "textualWithTime": "{{date, textualWithTime}}" } }, "vaultCard": { diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index 2ea4bfdea5..78c10971ef 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1074,6 +1074,7 @@ "noCollateral": "在從該池借入 {{tokenSymbol}} 之前,你需要先供應代幣並將其設為抵押", "noSwapQuoteFound": "找不到兌換。", "priceImpactTooHigh": "價格影響過高", + "smallerThanMinimumAmount": "你不能一次供應少於 {{minAmount}}", "supplyCapReached": "該池供應上限 {{assetSupplyCap}} 已達。除非有人提取或提高供應上限,否則無法再向該市場供應。", "tooRisky": "該交易會導致賬戶被清算" }, @@ -1986,7 +1987,6 @@ "card": { "apr": "APR", "blockingPendingWithdrawalsWarning": "您必須先完成待處理的提款才能與此金庫互動。", - "claimReward": "領取獎勵", "currentDeposited": "當前存款", "dailyEmission": "每日獎勵釋放", "effectiveFixedApr": "實際固定年化利率", @@ -1998,6 +1998,7 @@ "manager": "管理機構", "maturityDate": "到期日", "maturityDatePendleTooltip": "此固定收益部位(來自 Pendle)的到期日。在此日期之後,您將收回全部原始資產(本金)。鎖定的固定收益在此停止,建議持有至此以獲得預期收益。", + "minRequested": "Min. requested", "pausedWarning": "此金庫目前已暫停。利率仍在累積,但領取、存入和提款功能已禁用,直至金庫恢復運作。", "primeEligibility": "Prime 資格", "textualWithTime": "{{date, textualWithTime}}", @@ -2018,22 +2019,29 @@ "claim": "領取", "deposit": "存款", "earning": "收益中", + "inactive": "非活躍", "inputPlaceholder": "搜尋資產", + "liquidated": "已清算", "paused": "已暫停", + "pending": "待定", "refund": "退款", "repaying": "還款中" }, "header": "金庫", "modals": { "afterMaturityPendleDisclaimer": "請先透過提取按鈕將您的 PT 代幣提取到錢包,然後訪問 Pendle 官方網站贖回您的底層代幣。", + "claim": "領取", "connectWalletMessage": "請連接您的錢包以存入 / 提取 {{tokenSymbol}}", "convert": "轉換", "currentDeposited": "當前存入", "currentDepositedTooltip": "您當前在此金庫中鎖定並產生收益的資產數量。", "deposit": "存款", + "depositPeriodEnds": "存款期結束", + "depositsPausedNotice": "當該金庫處於固定利率生命週期推進過程中時,存款和提款將暫停。", "depositTab": "存款", "effectiveFixedApr": "實際固定年化利率", "effectiveFixedAprPendleTooltip": "此金庫策略提供的年化固定收益,以包含複利的有效年化利率(APR)表示。", + "effectiveFixedAprTooltip": "此金庫策略提供的年化固定收益,以包含複利的有效年化利率(APR)表示。", "error": { "amountTooLow": "輸入金額過低。", "invalidAmountFromQuote": "輸入金額超過Pendle市場的可用流動性。請減少金額後重試。", @@ -2044,20 +2052,35 @@ "estPenalty": "預計罰款", "estReceived": "預計收到", "estYield": "預計收益", + "institutionalDisclaimer": "如果金庫達到所需最低額度,存款將繼續進行直至達到最高目標。存入 {{lockingPeriod}} 後即可領取獎勵。否則,資金可退款。", + "institutionalTcsAgreement": "繼續存入即表示我同意機構金庫條款與條件。", + "institutionalTimeline": { + "claimPeriod": "領取期", + "depositPeriod": "存款期", + "estimatedEarningPeriod": "預計收益期", + "estimatedRepayingPeriod": "預計還款期", + "refundPeriod": "退款期" + }, "maturityDate": "到期日", "maturityDatePendleTooltip": "此固定收益部位(來自 Pendle)的到期日。在此日期之後,您將收回全部原始資產(本金)。鎖定的固定收益在此停止,建議持有至此以獲得預期收益。", "maturityPendleDisclaimer": "到期前,提款時收到的金額取決於 Pendle 市場價格。到期後,提款將根據您持有的 PT 代幣 1:1 贖回。", "minReceived": "最低收到", "overview": { + "campaignTimeline": "活動時間線", + "loan": "貸款", "manager": "管理機構", "managerTooltip": "管理此金庫基礎資產的地址或團隊。存入的資產會被分配到他們管理的市場中,他們可能會根據市場狀況更新贖回或結算時間。", "marketInfo": "市場資訊", "marketRiskDisclosures": "市場風險披露", - "riskDisclosureText": "Pendle 是一個去中心化的收益交易協議,通過本金代幣(PT)實現固定收益。Venus 固定利率金庫通過 Pendle 的基礎設施獲取 PT,並將其作為抵押品供應給 Venus Core。
雖然 Pendle 無法存取或轉移您的存款,但使用此金庫會使您面臨來自兩個協議的風險。這些風險包括智能合約漏洞、預言機故障,以及任一協議被攻擊或暫停的可能性。提前提款按市場價格執行,可能低於存入金額。固定 APY 在存入時鎖定,無法防範協議層面的故障。", + "riskDisclosureText": { + "ceffu": "TRANSLATION NEEDED", + "pendle": "TRANSLATION NEEDED" + }, "strategy": { "pendleRouter": "Pendle Router", "users": "用戶", - "venusCore": "Venus Core" + "venusCore": "Venus Core", + "venusVault": "Venus 金庫" }, "strategyAllocation": "策略分配", "supply": "供應", @@ -2070,11 +2093,16 @@ "stakeTab": "存款", "textualDate": "{{date, textual}}", "textualWithTime": "{{date, textualWithTime}}", + "totalYields": "總收益", "withdraw": "提取", "withdrawTab": "提取" }, "overview": { "bannerVaultIllustration": "金庫插圖" + }, + "timeline": { + "tbd": "待定", + "textualWithTime": "{{date, textualWithTime}}" } }, "vaultCard": { diff --git a/apps/evm/src/pages/Dashboard/Guide/__tests__/index.spec.tsx b/apps/evm/src/pages/Dashboard/Guide/__tests__/index.spec.tsx index d49569dac6..acca2b1bdc 100644 --- a/apps/evm/src/pages/Dashboard/Guide/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Dashboard/Guide/__tests__/index.spec.tsx @@ -28,7 +28,7 @@ describe('Guide', () => { const customFakeVault: Vault = { ...vaults[0], stakedToken: xvs, - userStakedMantissa: new BigNumber('1000000000000000000'), + userStakeBalanceMantissa: new BigNumber('1000000000000000000'), }; (useGetVaults as Mock).mockImplementation(() => ({ @@ -66,7 +66,7 @@ describe('Guide', () => { const customFakeVault: Vault = { ...vaults[0], stakedToken: xvs, - userStakedMantissa: new BigNumber(0), + userStakeBalanceMantissa: new BigNumber(0), }; (useGetVaults as Mock).mockImplementation(() => ({ diff --git a/apps/evm/src/pages/Dashboard/Guide/index.tsx b/apps/evm/src/pages/Dashboard/Guide/index.tsx index fa41eecd55..8766d07a57 100644 --- a/apps/evm/src/pages/Dashboard/Guide/index.tsx +++ b/apps/evm/src/pages/Dashboard/Guide/index.tsx @@ -31,7 +31,7 @@ export const Guide: React.FC = () => { const vaults = getVaultsData || []; const isUserStakingInXvsVault = vaults.some( - vault => vault.stakedToken.symbol === 'XVS' && vault.userStakedMantissa?.isGreaterThan(0), + vault => vault.stakedToken.symbol === 'XVS' && vault.userStakeBalanceMantissa?.isGreaterThan(0), ); const isUserSupplying = pool?.userSupplyBalanceCents?.isGreaterThan(0) || false; const isUserBorrowing = pool?.userBorrowBalanceCents?.isGreaterThan(0) || false; diff --git a/apps/evm/src/pages/Dashboard/Overview/useExtractData/index.tsx b/apps/evm/src/pages/Dashboard/Overview/useExtractData/index.tsx index a7595cb9b8..7d08a3c39c 100644 --- a/apps/evm/src/pages/Dashboard/Overview/useExtractData/index.tsx +++ b/apps/evm/src/pages/Dashboard/Overview/useExtractData/index.tsx @@ -27,7 +27,7 @@ export const useExtractData = ({ if (vaults) { vaults.forEach(vault => { const vaultStakeCents = convertMantissaToTokens({ - value: new BigNumber(vault.userStakedMantissa || 0), + value: new BigNumber(vault.userStakeBalanceMantissa || 0), token: vault.stakedToken, }).multipliedBy(vault.stakedToken.symbol === 'XVS' ? xvsPriceCents : vaiPriceCents); @@ -43,7 +43,7 @@ export const useExtractData = ({ yearlyVaultEarningsCents = yearlyVaultEarningsCents.plus( calculateYearlyInterests({ balance: vaultStakeCents, - interestPercentage: new BigNumber(vault.stakingAprPercentage), + interestPercentage: new BigNumber(vault.stakeAprPercentage), }), ); }); diff --git a/apps/evm/src/pages/Dashboard/Vaults/index.tsx b/apps/evm/src/pages/Dashboard/Vaults/index.tsx index 4f9210aa4b..3c90d75c4c 100644 --- a/apps/evm/src/pages/Dashboard/Vaults/index.tsx +++ b/apps/evm/src/pages/Dashboard/Vaults/index.tsx @@ -17,7 +17,7 @@ export const Vaults: React.FC = ({ vaults }) => { const { t } = useTranslation(); // Filter out vaults user has not staked in - const filteredVaults = vaults.filter(vault => vault.userStakedMantissa?.isGreaterThan(0)); + const filteredVaults = vaults.filter(vault => vault.userStakeBalanceMantissa?.isGreaterThan(0)); const filteredVaultsLength = filteredVaults.length; @@ -45,19 +45,23 @@ export const Vaults: React.FC = ({ vaults }) => { ); } - const { totalStakedCents, dailyEarningsCents } = filteredVaults.reduce( + const { stakeBalanceCents, dailyEarningsCents } = filteredVaults.reduce( (acc, curr) => { const userDailyEarningsCents = - curr.userStakedMantissa && curr.totalStakedMantissa.gt(0) && 'dailyEmissionCents' in curr - ? curr.userStakedMantissa.div(curr.totalStakedMantissa).times(curr.dailyEmissionCents) + curr.userStakeBalanceMantissa && + curr.stakeBalanceMantissa.gt(0) && + 'dailyEmissionCents' in curr + ? curr.userStakeBalanceMantissa + .div(curr.stakeBalanceMantissa) + .times(curr.dailyEmissionCents) : new BigNumber(0); return { - totalStakedCents: acc.totalStakedCents.plus(curr.userStakedCents ?? 0), + stakeBalanceCents: acc.stakeBalanceCents.plus(curr.userStakeBalanceCents ?? 0), dailyEarningsCents: acc.dailyEarningsCents.plus(userDailyEarningsCents), }; }, - { totalStakedCents: new BigNumber(0), dailyEarningsCents: new BigNumber(0) }, + { stakeBalanceCents: new BigNumber(0), dailyEarningsCents: new BigNumber(0) }, ); const overviewCells: CellProps[] = [ @@ -65,7 +69,7 @@ export const Vaults: React.FC = ({ vaults }) => { label: t('dashboard.vaults.totalStakedValue'), value: ( - {formatCentsToReadableValue({ value: totalStakedCents })} + {formatCentsToReadableValue({ value: stakeBalanceCents })} ), }, diff --git a/apps/evm/src/pages/Dashboard/useExtractData/index.tsx b/apps/evm/src/pages/Dashboard/useExtractData/index.tsx index 98be644db7..04a3c2f9a9 100644 --- a/apps/evm/src/pages/Dashboard/useExtractData/index.tsx +++ b/apps/evm/src/pages/Dashboard/useExtractData/index.tsx @@ -54,7 +54,7 @@ export const useExtractData = ({ if (vaults) { vaults.forEach(vault => { const vaultStakeCents = convertMantissaToTokens({ - value: new BigNumber(vault.userStakedMantissa || 0), + value: new BigNumber(vault.userStakeBalanceMantissa || 0), token: vault.stakedToken, }).multipliedBy(vault.stakedToken.symbol === 'XVS' ? xvsPriceCents : vaiPriceCents); @@ -70,7 +70,7 @@ export const useExtractData = ({ yearlyVaultEarningsCents = yearlyVaultEarningsCents.plus( calculateYearlyInterests({ balance: vaultStakeCents, - interestPercentage: new BigNumber(vault.stakingAprPercentage), + interestPercentage: new BigNumber(vault.stakeAprPercentage), }), ); }); diff --git a/apps/evm/src/pages/Governance/VotingWallet/index.tsx b/apps/evm/src/pages/Governance/VotingWallet/index.tsx index 2657a79e0b..3bbc04a360 100644 --- a/apps/evm/src/pages/Governance/VotingWallet/index.tsx +++ b/apps/evm/src/pages/Governance/VotingWallet/index.tsx @@ -68,7 +68,7 @@ const VotingWallet: React.FC = ({ className }) => { }); const xvsVault = xvs && vaults.find(v => areTokensEqual(v.stakedToken, xvs)); - const userStakedMantissa = xvsVault?.userStakedMantissa || new BigNumber(0); + const userStakeBalanceMantissa = xvsVault?.userStakeBalanceMantissa || new BigNumber(0); const { mutateAsync: setVoteDelegation, isPending: isVoteDelegationLoading } = useSetVoteDelegate( { @@ -83,12 +83,12 @@ const VotingWallet: React.FC = ({ className }) => { const readableXvsLocked = useMemo( () => convertMantissaToTokens({ - value: userStakedMantissa, + value: userStakeBalanceMantissa, token: xvs, returnInReadableFormat: true, addSymbol: false, }), - [userStakedMantissa, xvs], + [userStakeBalanceMantissa, xvs], ); const readableVoteWeight = useMemo( @@ -109,7 +109,7 @@ const VotingWallet: React.FC = ({ className }) => { isVoteDelegationLoading; const previouslyDelegated = !!delegate; - const userHasLockedXVS = userStakedMantissa.isGreaterThan(0); + const userHasLockedXVS = userStakeBalanceMantissa.isGreaterThan(0); const showDepositXvs = !isDataLoading && isUserConnected && !userHasLockedXVS && voteProposalFeatureEnabled; const showDelegateButton = diff --git a/apps/evm/src/pages/Governance/__tests__/index.spec.tsx b/apps/evm/src/pages/Governance/__tests__/index.spec.tsx index 3b1f3b6745..934927b68e 100644 --- a/apps/evm/src/pages/Governance/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Governance/__tests__/index.spec.tsx @@ -184,7 +184,7 @@ describe('Governance', () => { it('prompts user to deposit XVS', async () => { const vaultsCopy = cloneDeep(vaults); - vaultsCopy[1].userStakedMantissa = new BigNumber(0); + vaultsCopy[1].userStakeBalanceMantissa = new BigNumber(0); (getCurrentVotes as Mock).mockImplementationOnce(() => ({ votesMantissa: new BigNumber(0), })); @@ -357,7 +357,7 @@ describe('Governance', () => { it('renders the delegate/redelegate button when voting is enabled', async () => { const vaultsCopy = cloneDeep(vaults); - vaultsCopy[1].userStakedMantissa = new BigNumber(1000); + vaultsCopy[1].userStakeBalanceMantissa = new BigNumber(1000); (useGetVestingVaults as Mock).mockImplementation(() => ({ data: vaultsCopy, isLoading: false, diff --git a/apps/evm/src/pages/Market/MarketHistory/Card/CapThreshold/index.tsx b/apps/evm/src/pages/Market/MarketHistory/Card/CapThreshold/index.tsx deleted file mode 100644 index b875567deb..0000000000 --- a/apps/evm/src/pages/Market/MarketHistory/Card/CapThreshold/index.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { theme } from '@venusprotocol/ui'; -import BigNumber from 'bignumber.js'; -import { ProgressCircle, Tooltip } from 'components'; -import { useTranslation } from 'libs/translations'; -import { useMemo } from 'react'; -import type { Token } from 'types'; - -import { - formatCentsToReadableValue, - formatPercentageToReadableValue, - formatTokensToReadableValue, -} from 'utilities'; - -const THRESHOLD_GRADIENT_ID = 'cap-threshold-gradient'; - -export interface CapThresholdProps { - token: Token; - type: 'supply' | 'borrow'; - tokenPriceCents: BigNumber; - capTokens: BigNumber; - limitTokens: BigNumber; - balanceTokens: BigNumber; -} - -export const CapThreshold: React.FC = ({ - type, - tokenPriceCents, - capTokens, - limitTokens, - balanceTokens, - token, -}) => { - const { t, Trans } = useTranslation(); - - const { - readableBalanceDollars, - readableBalanceTokens, - readableCapTokens, - readableLimitTokens, - readableLimitDollars, - readableDeltaDollars, - readableDeltaTokens, - readableThresholdPercentage, - thresholdPercentage, - } = useMemo(() => { - const balanceCents = balanceTokens.multipliedBy(tokenPriceCents); - - const tmpReadableBalanceDollars = formatCentsToReadableValue({ - value: balanceCents, - }); - - const tmpReadableBalanceTokens = formatTokensToReadableValue({ - value: balanceTokens, - token, - addSymbol: false, - }); - - const tmpReadableCapTokens = formatTokensToReadableValue({ - value: capTokens, - token, - }); - - const tmpReadableLimitTokens = formatTokensToReadableValue({ - value: limitTokens, - token, - }); - - const limitCents = limitTokens.multipliedBy(tokenPriceCents); - const tmpReadableLimitDollars = formatCentsToReadableValue({ value: limitCents }); - - const deltaAmountCents = limitCents.minus(balanceCents); - const tmpReadableDeltaDollars = formatCentsToReadableValue({ - value: deltaAmountCents.isLessThanOrEqualTo(0) ? 0 : deltaAmountCents, - }); - - const deltaTokens = limitTokens.minus(balanceTokens); - const tmpReadableDeltaTokens = formatTokensToReadableValue({ - value: deltaTokens.isLessThanOrEqualTo(0) ? new BigNumber(0) : deltaTokens, - token, - }); - - const thresholdPercentage = limitTokens.isEqualTo(0) - ? 100 - : balanceTokens.multipliedBy(100).div(limitTokens).toNumber(); - - const tmpReadableThresholdPercentage = formatPercentageToReadableValue(thresholdPercentage); - - return { - readableBalanceDollars: tmpReadableBalanceDollars, - readableBalanceTokens: tmpReadableBalanceTokens, - readableCapTokens: tmpReadableCapTokens, - readableLimitTokens: tmpReadableLimitTokens, - readableLimitDollars: tmpReadableLimitDollars, - readableDeltaDollars: tmpReadableDeltaDollars, - readableDeltaTokens: tmpReadableDeltaTokens, - readableThresholdPercentage: tmpReadableThresholdPercentage, - thresholdPercentage, - }; - }, [balanceTokens, capTokens, tokenPriceCents, token, limitTokens]); - - return ( -
- , - }} - /> - ) - } - > -
- - - - - } - /> - -

{readableThresholdPercentage}

-
-
- -
-

- {type === 'supply' - ? t('market.supplyCapThreshold.title') - : t('market.borrowCapThreshold.title')} -

- -

- {readableBalanceDollars} / {readableLimitDollars} -

- -

- {readableBalanceTokens} / {readableLimitTokens} -

-
-
- ); -}; diff --git a/apps/evm/src/pages/Market/MarketHistory/Card/index.tsx b/apps/evm/src/pages/Market/MarketHistory/Card/index.tsx index 85b013e62a..f8c96879a4 100644 --- a/apps/evm/src/pages/Market/MarketHistory/Card/index.tsx +++ b/apps/evm/src/pages/Market/MarketHistory/Card/index.tsx @@ -3,14 +3,17 @@ import { useMemo } from 'react'; import type { Address } from 'viem'; import type { MarketHistoryPeriodType } from 'clients/api'; -import { Apy, ButtonGroup, Spinner } from 'components'; +import { Apy, ButtonGroup, CapProgressCircle, Spinner } from 'components'; import { useIsFeatureEnabled } from 'hooks/useIsFeatureEnabled'; import { useTranslation } from 'libs/translations'; import { ApyChart, type ApyChartProps } from 'pages/Market/MarketHistory/Card/ApyChart'; import type { Asset } from 'types'; -import { formatPercentageToReadableValue } from 'utilities'; +import { + formatCentsToReadableValue, + formatPercentageToReadableValue, + formatTokensToReadableValue, +} from 'utilities'; import { MarketCard, type MarketCardProps } from '../../MarketCard'; -import { CapThreshold } from './CapThreshold'; export interface CardProps extends Omit { type: ApyChartProps['type']; @@ -107,22 +110,68 @@ export const Card: React.FC = ({ }, ]; + const capThresholdValueTokens = + type === 'supply' ? asset.supplyBalanceTokens : asset.borrowBalanceTokens; + const capThresholdLimitTokens = + type === 'supply' + ? asset.supplyCapTokens + : BigNumber.min(asset.supplyBalanceTokens, asset.borrowCapTokens); + + const capThresholdTooltip = useMemo(() => { + const deltaTokens = capThresholdLimitTokens.minus(capThresholdValueTokens); + const safeDeltaTokens = deltaTokens.isLessThanOrEqualTo(0) ? new BigNumber(0) : deltaTokens; + + const amountDollars = formatCentsToReadableValue({ + value: safeDeltaTokens.multipliedBy(asset.tokenPriceCents), + }); + const amountTokens = formatTokensToReadableValue({ + value: safeDeltaTokens, + token: asset.vToken.underlyingToken, + }); + + if (type === 'supply') { + return t('market.supplyCapThreshold.tooltip', { + amountDollars, + amountTokens, + }); + } + + const capTokens = formatTokensToReadableValue({ + value: asset.borrowCapTokens, + token: asset.vToken.underlyingToken, + }); + + return t('market.borrowCapThreshold.tooltip', { + amountDollars, + amountTokens, + capTokens, + }).replace(//g, '\n'); + }, [ + asset.borrowCapTokens, + asset.tokenPriceCents, + asset.vToken.underlyingToken, + capThresholdLimitTokens, + capThresholdValueTokens, + t, + type, + ]); + return ( 0 ? legends : undefined} topContent={ - {capThresholdTooltip}} token={asset.vToken.underlyingToken} /> } diff --git a/apps/evm/src/pages/Vaults/VaultList/hooks/useFilterOptions.tsx b/apps/evm/src/pages/Vaults/VaultList/hooks/useFilterOptions.tsx index 9e1e884af5..6619e37728 100644 --- a/apps/evm/src/pages/Vaults/VaultList/hooks/useFilterOptions.tsx +++ b/apps/evm/src/pages/Vaults/VaultList/hooks/useFilterOptions.tsx @@ -76,6 +76,15 @@ export const useFilterOptions = () => { ), value: 'pendle', }, + { + label: ( +
+ + CEFFU +
+ ), + value: 'ceffu', + }, ]; const statusOptions = [ @@ -107,6 +116,18 @@ export const useFilterOptions = () => { label: t('vault.filter.claim'), value: 'claim', }, + { + label: t('vault.filter.pending'), + value: 'pending', + }, + { + label: t('vault.filter.inactive'), + value: 'inactive', + }, + { + label: t('vault.filter.liquidated'), + value: 'liquidated', + }, ]; return { diff --git a/apps/evm/src/pages/Vaults/__tests__/__snapshots__/index.spec.tsx.snap b/apps/evm/src/pages/Vaults/__tests__/__snapshots__/index.spec.tsx.snap deleted file mode 100644 index 14b156b343..0000000000 --- a/apps/evm/src/pages/Vaults/__tests__/__snapshots__/index.spec.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`VaultsUi > renders vaults correctly 1`] = `"VaultYield made simple, returns made realAll categoriesAll managersAll statesVAIStablecoinsActiveAPR> 10,000%Daily emission144 XVS$144Total deposited415 VAI$415ManagerVENUSXVSGovernanceActiveAPR12.92%Daily emission144 XVS$144Total deposited400M XVS$400MPrime eligibility0 XVS / 0 XVS0%ManagerVENUS"`; diff --git a/apps/evm/src/pages/Vaults/__tests__/index.spec.tsx b/apps/evm/src/pages/Vaults/__tests__/index.spec.tsx index 38243fff95..beaa6397b1 100644 --- a/apps/evm/src/pages/Vaults/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Vaults/__tests__/index.spec.tsx @@ -1,27 +1,95 @@ +import { fireEvent } from '@testing-library/react'; import type { Mock } from 'vitest'; -import { vaults as fakeVaults } from '__mocks__/models/vaults'; +import { institutionalVault, vaults as venusVaults } from '__mocks__/models/vaults'; +import { en, t } from 'libs/translations'; import { renderComponent } from 'testUtils/render'; +import { type InstitutionalVault, VaultStatus } from 'types'; import { useGetVaults } from 'clients/api'; import Staking from '..'; -describe('VaultsUi', () => { +describe('Vaults', () => { + const fakeVaults = [institutionalVault, ...venusVaults]; + const titleSelector = 'p.truncate.text-b1s'; + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-04-05T00:00:00.000Z')); + (useGetVaults as Mock).mockImplementation(() => ({ data: fakeVaults, isLoading: false, })); }); - it('renders without crashing', async () => { - renderComponent(); + afterEach(() => { + vi.useRealTimers(); + }); + + it('renders vaults correctly', () => { + const { getByText } = renderComponent(); + + expect(getByText('USDC - CEFFU')).toBeInTheDocument(); + expect(getByText(en.vault.modals.depositPeriodEnds)).toBeInTheDocument(); + expect( + getByText(t('vault.timeline.textualWithTime', { date: institutionalVault.openEndDate })), + ).toBeInTheDocument(); + expect(getByText('VAI', { selector: titleSelector })).toBeInTheDocument(); + expect(getByText('XVS', { selector: titleSelector })).toBeInTheDocument(); + }); + + it('filters vaults from the url manager parameter', () => { + const { getByText, queryByText } = renderComponent(, { + routerInitialEntries: ['/?manager=ceffu'], + }); + + expect(getByText('USDC - CEFFU')).toBeInTheDocument(); + expect(queryByText('VAI', { selector: titleSelector })).not.toBeInTheDocument(); + expect(queryByText('XVS', { selector: titleSelector })).not.toBeInTheDocument(); + }); + + it('filters vaults from the url category parameter', () => { + const { getByText, queryByText } = renderComponent(, { + routerInitialEntries: ['/?category=governance'], + }); + + expect(getByText('XVS', { selector: titleSelector })).toBeInTheDocument(); + expect(queryByText('USDC - CEFFU')).not.toBeInTheDocument(); + expect(queryByText('VAI', { selector: titleSelector })).not.toBeInTheDocument(); + }); + + it('filters vaults by token symbol search', () => { + const { getByPlaceholderText, getByText, queryByText } = renderComponent(); + + fireEvent.change(getByPlaceholderText(en.vault.filter.inputPlaceholder), { + target: { value: 'xvs' }, + }); + + expect(getByText('XVS', { selector: titleSelector })).toBeInTheDocument(); + expect(queryByText('USDC - CEFFU')).not.toBeInTheDocument(); + expect(queryByText('VAI', { selector: titleSelector })).not.toBeInTheDocument(); }); - it('renders vaults correctly', async () => { - const { container } = renderComponent(); + it('filters vaults from the url status parameter', () => { + const liquidatedInstitutionalVault = { + ...institutionalVault, + status: VaultStatus.Liquidated, + } satisfies InstitutionalVault; + + (useGetVaults as Mock).mockImplementation(() => ({ + data: [liquidatedInstitutionalVault, ...venusVaults], + isLoading: false, + })); + + const { getAllByText, getByText, queryByText } = renderComponent(, { + routerInitialEntries: ['/?status=liquidated'], + }); - expect(container.textContent).toMatchSnapshot(); + expect(getByText('USDC - CEFFU')).toBeInTheDocument(); + expect(getAllByText(en.vault.filter.liquidated)).toHaveLength(2); + expect(queryByText('XVS', { selector: titleSelector })).not.toBeInTheDocument(); + expect(queryByText('VAI', { selector: titleSelector })).not.toBeInTheDocument(); }); }); diff --git a/apps/evm/src/pages/VoterLeaderboard/index.tsx b/apps/evm/src/pages/VoterLeaderboard/index.tsx index 1652620ba1..31d6b111df 100644 --- a/apps/evm/src/pages/VoterLeaderboard/index.tsx +++ b/apps/evm/src/pages/VoterLeaderboard/index.tsx @@ -52,7 +52,7 @@ const VoterLeaderboard: React.FC = () => { const totalStakedXvs = vestingVaults .filter(v => v.stakedToken.symbol === 'XVS') - .reduce((acc, v) => acc.plus(v.totalStakedMantissa), new BigNumber(0)); + .reduce((acc, v) => acc.plus(v.stakeBalanceMantissa), new BigNumber(0)); const { data: { voterAccounts, offset, total, limit } = { diff --git a/apps/evm/src/types/index.ts b/apps/evm/src/types/index.ts index 568db24790..dd0c0e35fe 100644 --- a/apps/evm/src/types/index.ts +++ b/apps/evm/src/types/index.ts @@ -478,17 +478,27 @@ export interface Transaction { export enum VaultStatus { Active = 'active', + Inactive = 'inactive', Deposit = 'deposit', Earning = 'earning', + Pending = 'pending', Refund = 'refund', Repaying = 'repaying', Claim = 'claim', + Liquidated = 'liquidated', Paused = 'paused', } export enum VaultManager { Venus = 'venus', Pendle = 'pendle', + Ceffu = 'ceffu', +} + +export enum VaultType { + Venus = 'venus', + Pendle = 'pendle', + Institutional = 'institutional', } export enum VaultCategory { @@ -498,6 +508,7 @@ export enum VaultCategory { } interface BaseVault { + vaultType: VaultType; category: VaultCategory; manager: VaultManager; managerIcon: IconName; @@ -508,12 +519,12 @@ interface BaseVault { rewardToken: Token; stakedTokenPriceCents: BigNumber; rewardTokenPriceCents: BigNumber; - stakingAprPercentage: number; - totalStakedMantissa: BigNumber; - totalStakedCents: number; + stakeAprPercentage: number; + stakeBalanceMantissa: BigNumber; + stakeBalanceCents: number; + userStakeBalanceMantissa?: BigNumber; + userStakeBalanceCents?: number; lockingPeriodMs?: number; - userStakedMantissa?: BigNumber; - userStakedCents?: number; poolIndex?: number; } @@ -525,17 +536,35 @@ export type VenusVault = BaseVault & { }; export type PendleVault = BaseVault & { + vaultAddress: Address; maturityDate: Date; liquidityCents: BigNumber; asset: Asset; - managerLink?: string; - vaultDeploymentDate?: Date; poolComptrollerContractAddress: Address; poolName: string; rewardToken: Token; + managerLink?: string; + vaultDeploymentDate?: Date; +}; + +export type InstitutionalVault = BaseVault & { + vaultAddress: Address; + reserveFactor: number; + stakeLimitMantissa: BigNumber; + stakeMinMantissa: BigNumber; + userRedeemLimitMantissa: BigNumber; + userYieldTokens?: BigNumber; + userWithdrawLimitMantissa: BigNumber; + userMinIndividualStakeMantissa?: BigNumber; + vaultDeploymentDate?: Date; + openEndDate?: Date; + lockEndDate?: Date; + maturityDate?: Date; + settlementDate?: Date; + managerLink?: string; }; -export type Vault = VenusVault | PendleVault; +export type Vault = VenusVault | PendleVault | InstitutionalVault; export interface VoterAccount { address: Address; diff --git a/apps/evm/src/utilities/convertDollarsToCents.ts b/apps/evm/src/utilities/convertDollarsToCents.ts index 0c6c601a5f..6c2117fe08 100644 --- a/apps/evm/src/utilities/convertDollarsToCents.ts +++ b/apps/evm/src/utilities/convertDollarsToCents.ts @@ -1,5 +1,5 @@ import BigNumber from 'bignumber.js'; -const convertDollarsToCents = (value: BigNumber) => new BigNumber(value).times(100); +const convertDollarsToCents = (value: BigNumber) => new BigNumber(value).shiftedBy(2); export default convertDollarsToCents; diff --git a/apps/evm/src/containers/PrimeStatusBanner/formatWaitingPeriod.ts b/apps/evm/src/utilities/formatWaitingPeriod/index.ts similarity index 100% rename from apps/evm/src/containers/PrimeStatusBanner/formatWaitingPeriod.ts rename to apps/evm/src/utilities/formatWaitingPeriod/index.ts diff --git a/apps/evm/src/utilities/index.ts b/apps/evm/src/utilities/index.ts index e5531e86df..714e908e4d 100755 --- a/apps/evm/src/utilities/index.ts +++ b/apps/evm/src/utilities/index.ts @@ -56,6 +56,7 @@ export * from './isCollateralActionDisabled'; export * from './getBoostedAssetSupplyApy'; export * from './buffer'; export * from './formatHealthFactorToReadableValue'; +export * from './formatWaitingPeriod'; export * from './debounce'; export * from './getDecimals'; export * from './calculateUserPoolValues'; @@ -64,6 +65,7 @@ export * from './clampToZero'; export * from './invalidatePendleVaultCaches'; export * from './isPendleVault'; export * from './isLegacyVenusVault'; +export * from './isInstitutionalVault'; export * from './calculateDailyInterests'; export * from './getVaultCategoryName'; export * from './getTransactionName'; diff --git a/apps/evm/src/utilities/isInstitutionalVault/index.ts b/apps/evm/src/utilities/isInstitutionalVault/index.ts new file mode 100644 index 0000000000..bcf285903c --- /dev/null +++ b/apps/evm/src/utilities/isInstitutionalVault/index.ts @@ -0,0 +1,4 @@ +import { type InstitutionalVault, type Vault, VaultType } from 'types'; + +export const isInstitutionalVault = (vault: Vault): vault is InstitutionalVault => + vault.vaultType === VaultType.Institutional; diff --git a/apps/evm/src/utilities/isLegacyVenusVault/index.ts b/apps/evm/src/utilities/isLegacyVenusVault/index.ts index b3761467e8..648afade15 100644 --- a/apps/evm/src/utilities/isLegacyVenusVault/index.ts +++ b/apps/evm/src/utilities/isLegacyVenusVault/index.ts @@ -1,4 +1,4 @@ -import { type Vault, VaultManager, type VenusVault } from 'types'; +import { type Vault, VaultType, type VenusVault } from 'types'; export const isLegacyVenusVault = (vault: Vault): vault is VenusVault => - vault.manager === VaultManager.Venus; + vault.vaultType === VaultType.Venus; diff --git a/apps/evm/src/utilities/isPendleVault/index.ts b/apps/evm/src/utilities/isPendleVault/index.ts index 24bd1013a5..c83aad7ed6 100644 --- a/apps/evm/src/utilities/isPendleVault/index.ts +++ b/apps/evm/src/utilities/isPendleVault/index.ts @@ -1,4 +1,4 @@ -import { type PendleVault, type Vault, VaultManager } from 'types'; +import { type PendleVault, type Vault, VaultType } from 'types'; export const isPendleVault = (vault: Vault): vault is PendleVault => - vault.manager === VaultManager.Pendle; + vault.vaultType === VaultType.Pendle; diff --git a/packages/chains/src/tokens/underlyingTokens/bscTestnet.ts b/packages/chains/src/tokens/underlyingTokens/bscTestnet.ts index 79a48b10b4..45e6dc83f0 100644 --- a/packages/chains/src/tokens/underlyingTokens/bscTestnet.ts +++ b/packages/chains/src/tokens/underlyingTokens/bscTestnet.ts @@ -453,4 +453,18 @@ export const bscTestnet: Token[] = [ symbol: 'XAUm', iconSrc: iconSrcs.xaum, }, + { + chainId: ChainId.BSC_TESTNET, + address: '0x312e39c7641cE64BEccDe53613f07952258fa810', + decimals: 6, + symbol: 'MOCK_USDC', + iconSrc: iconSrcs.usdc, + }, + { + chainId: ChainId.BSC_TESTNET, + address: '0xCC3933141a64E26C9317b19CE4BbB4ec2c333bc6', + decimals: 8, + symbol: 'MOCK_WBTC', + iconSrc: iconSrcs.wbtc, + }, ];