From 0b40f26d045dd4f252c333b049947175a97e1f3e Mon Sep 17 00:00:00 2001 From: therealemjy Date: Thu, 23 Jul 2026 17:19:52 +0200 Subject: [PATCH] feat: add support for Liquidity Hub transactions --- .changeset/forty-spies-enjoy.md | 5 ++ apps/evm/src/__mocks__/models/transactions.ts | 18 +++++ .../__tests__/index.spec.ts | 61 ++++++++++++++++ .../formatToLiquidityHubTransaction/index.ts | 73 +++++++++++++++++++ .../formatApiTransaction/index.ts | 21 +++++- .../getAccountTransactionHistory/index.ts | 9 +++ .../getAccountTransactionHistory/types.ts | 5 +- .../useGetAccountTransactionHistory.ts | 4 +- .../__tests__/index.spec.ts | 37 ++++++++++ .../formatToLiquidityHubTxEvents/index.ts | 35 +++++++++ .../TransactionRow/Events/index.tsx | 23 ++++-- .../TransactionsList/TransactionRow/index.tsx | 13 +--- apps/evm/src/constants/marketTxTypes.ts | 10 ++- .../libs/translations/translations/en.json | 4 +- .../libs/translations/translations/ja.json | 2 +- .../libs/translations/translations/th.json | 2 +- .../libs/translations/translations/tr.json | 2 +- .../libs/translations/translations/vi.json | 2 +- .../translations/translations/zh-Hans.json | 2 +- .../translations/translations/zh-Hant.json | 2 +- .../__snapshots__/index.spec.tsx.snap | 4 +- .../Transactions/__tests__/index.spec.tsx | 15 +++- .../pages/Dashboard/Transactions/index.tsx | 29 +++++++- apps/evm/src/types/index.ts | 18 ++++- .../__tests__/index.spec.ts | 33 +++++++++ .../src/utilities/getTransactionName/index.ts | 16 ++-- 26 files changed, 400 insertions(+), 45 deletions(-) create mode 100644 .changeset/forty-spies-enjoy.md create mode 100644 apps/evm/src/clients/api/queries/getAccountTransactionHistory/formatApiTransaction/formatToLiquidityHubTransaction/index.ts create mode 100644 apps/evm/src/components/TransactionsList/TransactionRow/Events/formatToLiquidityHubTxEvents/__tests__/index.spec.ts create mode 100644 apps/evm/src/components/TransactionsList/TransactionRow/Events/formatToLiquidityHubTxEvents/index.ts create mode 100644 apps/evm/src/utilities/getTransactionName/__tests__/index.spec.ts diff --git a/.changeset/forty-spies-enjoy.md b/.changeset/forty-spies-enjoy.md new file mode 100644 index 0000000000..de98af3461 --- /dev/null +++ b/.changeset/forty-spies-enjoy.md @@ -0,0 +1,5 @@ +--- +"@venusprotocol/evm": minor +--- + +add support for Liquidity Hub transactions diff --git a/apps/evm/src/__mocks__/models/transactions.ts b/apps/evm/src/__mocks__/models/transactions.ts index 6325be7b22..113f08e591 100644 --- a/apps/evm/src/__mocks__/models/transactions.ts +++ b/apps/evm/src/__mocks__/models/transactions.ts @@ -1,9 +1,27 @@ import BigNumber from 'bignumber.js'; import { usdt } from './tokens'; import { vUsdtCorePool } from './vTokens'; +import { vhXvs } from './vhTokens'; export const transactions = { transactions: [ + { + accountAddress: '0x3d759121234cd36F8124C21aFe1c6852d2bEd848', + amounts: [ + { + amountCents: 7150, + amountTokens: new BigNumber(10), + token: vhXvs.underlyingToken, + }, + ], + blockNumber: '41604852', + blockTimestamp: new Date('2024-08-24T04:17:09.000Z'), + chainId: 97, + contractAddress: vhXvs.address, + hash: '0xca13a0f45b10dabd21b863b6b602c6d8776edd4c6b10fe65a0881d491f468f35', + txType: 'supply', + vhToken: vhXvs, + }, { accountAddress: '0x3d759121234cd36F8124C21aFe1c6852d2bEd848', amounts: [ diff --git a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/__tests__/index.spec.ts b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/__tests__/index.spec.ts index c08d55cded..578a5406a3 100644 --- a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/__tests__/index.spec.ts +++ b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/__tests__/index.spec.ts @@ -1,7 +1,9 @@ import { ChainId } from '@venusprotocol/chains'; import fakeAddress from '__mocks__/models/address'; +import { liquidityHubs } from '__mocks__/models/liquidityHubs'; import { poolData } from '__mocks__/models/pools'; import { restService } from 'utilities'; +import type { Address } from 'viem'; import { type Mock, vi } from 'vitest'; import { type ApiAccountHistoricalTransaction, getAccountTransactionHistory } from '..'; import { convertToTxType } from '../formatApiTransaction/convertToTxType'; @@ -12,6 +14,7 @@ const fakeInput = { accountAddress: fakeAddress, chainId: ChainId.BSC_TESTNET, getPoolsData: { pools: poolData }, + liquidityHubs, contractAddress: undefined, page: 1, }; @@ -190,6 +193,64 @@ describe('getAccountTransactionHistory', () => { ]); }); + it('formats Liquidity Hub supply and withdrawal transactions', async () => { + const [liquidityHub] = liquidityHubs; + const commonTransactionFields = { + accountAddress: fakeAddress as Address, + amountUnderlyingMantissa: '1000000000000000000', + amountVTokenMantissa: '1000000000000000000', + blockNumber: '41604853', + chainId: ChainId.BSC_TESTNET, + contractAddress: liquidityHub.vhToken.address, + underlyingAddress: liquidityHub.vhToken.underlyingToken.address, + underlyingTokenPriceMantissa: '7000000000000000000', + ...fakeYieldPlusFields, + }; + + (restService as Mock).mockResolvedValue({ + data: { + count: '2', + results: [ + { + ...commonTransactionFields, + id: 'liquidity-hub-supply', + txHash: '0x3', + txIndex: 3, + txTimestamp: new Date('2024-08-25T04:17:09.000Z'), + txType: 'mint', + }, + { + ...commonTransactionFields, + id: 'liquidity-hub-withdraw', + txHash: '0x4', + txIndex: 4, + txTimestamp: new Date('2024-08-26T04:17:09.000Z'), + txType: 'redeem', + }, + ] satisfies ApiAccountHistoricalTransaction[], + }, + }); + + const response = await getAccountTransactionHistory(fakeInput); + + expect(response.transactions).toHaveLength(2); + expect(response.transactions).toEqual([ + expect.objectContaining({ + txType: 'supply', + vhToken: liquidityHub.vhToken, + }), + expect.objectContaining({ + txType: 'withdraw', + vhToken: liquidityHub.vhToken, + }), + ]); + + response.transactions.forEach(transaction => { + expect(transaction.amounts?.[0]?.amountTokens.isEqualTo(1)).toBe(true); + expect(transaction.amounts?.[0]?.amountCents).toBe(700); + }); + }); + it('throws on error in payload', async () => { (restService as Mock).mockResolvedValue({ data: { error: 'Some error' } }); diff --git a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/formatApiTransaction/formatToLiquidityHubTransaction/index.ts b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/formatApiTransaction/formatToLiquidityHubTransaction/index.ts new file mode 100644 index 0000000000..3427df64c9 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/formatApiTransaction/formatToLiquidityHubTransaction/index.ts @@ -0,0 +1,73 @@ +import type { LiquidityHubTx, LiquidityHubTxType } from 'types'; +import { + convertDollarsToCents, + convertMantissaToTokens, + convertPriceMantissaToDollars, +} from 'utilities'; +import type { Address } from 'viem'; +import type { ApiAccountHistoricalTransaction, VhTokenMapping } from '../../types'; + +export const formatToLiquidityHubTransaction = ({ + vhTokenMapping, + apiTransaction, + txType, +}: { + vhTokenMapping: VhTokenMapping; + apiTransaction: ApiAccountHistoricalTransaction; + txType: LiquidityHubTxType; +}): LiquidityHubTx | undefined => { + const { + txHash: hash, + txTimestamp: blockTimestamp, + blockNumber, + accountAddress, + contractAddress, + chainId, + amountUnderlyingMantissa, + underlyingTokenPriceMantissa, + } = apiTransaction; + + const vhToken = vhTokenMapping[contractAddress.toLowerCase() as Address]; + if (!vhToken) { + return undefined; + } + + const token = vhToken.underlyingToken; + const amountTokens = amountUnderlyingMantissa + ? convertMantissaToTokens({ + value: BigInt(amountUnderlyingMantissa), + token, + }) + : undefined; + + const tokenPriceDollars = underlyingTokenPriceMantissa + ? convertPriceMantissaToDollars({ + priceMantissa: underlyingTokenPriceMantissa, + decimals: token.decimals, + }) + : undefined; + + const tokenPriceCents = tokenPriceDollars ? convertDollarsToCents(tokenPriceDollars) : undefined; + const amounts = + amountTokens && tokenPriceCents + ? [ + { + token, + amountTokens, + amountCents: amountTokens.multipliedBy(tokenPriceCents).toNumber(), + }, + ] + : undefined; + + return { + txType, + hash, + blockTimestamp, + blockNumber, + accountAddress, + contractAddress, + chainId, + vhToken, + amounts, + }; +}; diff --git a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/formatApiTransaction/index.ts b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/formatApiTransaction/index.ts index 607dde17ed..c7b3e05e07 100644 --- a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/formatApiTransaction/index.ts +++ b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/formatApiTransaction/index.ts @@ -1,15 +1,18 @@ -import { TRADE_TX_TYPES } from 'constants/marketTxTypes'; -import type { MarketTxType, TradeTxType } from 'types'; -import type { ApiAccountHistoricalTransaction, VTokenAssetMapping } from '../types'; +import { LIQUIDITY_HUB_TX_TYPES, TRADE_TX_TYPES } from 'constants/marketTxTypes'; +import type { LiquidityHubTxType, MarketTxType, TradeTxType } from 'types'; +import type { ApiAccountHistoricalTransaction, VTokenAssetMapping, VhTokenMapping } from '../types'; import { convertToTxType } from './convertToTxType'; +import { formatToLiquidityHubTransaction } from './formatToLiquidityHubTransaction'; import { formatToMarketTransaction } from './formatToMarketTransaction'; import { formatToTradeTransaction } from './formatToTradeTransaction'; export const formatApiTransaction = ({ vTokenAssetMapping, + vhTokenMapping, apiTransaction, }: { vTokenAssetMapping: VTokenAssetMapping; + vhTokenMapping: VhTokenMapping; apiTransaction: ApiAccountHistoricalTransaction; }) => { const { txType: apiTxType } = apiTransaction; @@ -27,6 +30,18 @@ export const formatApiTransaction = ({ }); } + if (LIQUIDITY_HUB_TX_TYPES.some(t => t === txType)) { + const liquidityHubTransaction = formatToLiquidityHubTransaction({ + vhTokenMapping, + apiTransaction, + txType: txType as LiquidityHubTxType, + }); + + if (liquidityHubTransaction) { + return liquidityHubTransaction; + } + } + return formatToMarketTransaction({ vTokenAssetMapping, apiTransaction, diff --git a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/index.ts b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/index.ts index 3ddfe4bd35..35d2a07f81 100644 --- a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/index.ts +++ b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/index.ts @@ -8,6 +8,7 @@ import type { GetAccountTransactionHistoryInput, GetAccountTransactionHistoryOutput, VTokenAssetMapping, + VhTokenMapping, } from './types'; export * from './types'; @@ -27,6 +28,7 @@ export const getAccountTransactionHistory = async ({ contractAddress, positionAccountAddress, getPoolsData, + liquidityHubs, type, page, }: GetAccountTransactionHistoryInput): Promise => { @@ -70,11 +72,18 @@ export const getAccountTransactionHistory = async ({ return acc; }, {}); + const vhTokenMapping = liquidityHubs.reduce((acc, liquidityHub) => { + acc[liquidityHub.vhToken.address.toLowerCase() as Address] = liquidityHub.vhToken; + + return acc; + }, {}); + const formattedResponse = txsResponse.data.results.reduce< GetAccountTransactionHistoryOutput['transactions'] >((acc, apiTransaction) => { const formattedTransaction = formatApiTransaction({ vTokenAssetMapping, + vhTokenMapping, apiTransaction, }); diff --git a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/types.ts b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/types.ts index d2b0d95117..10f3019908 100644 --- a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/types.ts +++ b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/types.ts @@ -1,5 +1,5 @@ import type { ChainId } from '@venusprotocol/chains'; -import type { Asset, Tx, TxType } from 'types'; +import type { Asset, LiquidityHub, Tx, TxType, VhToken } from 'types'; import type { Address } from 'viem'; import type { GetPoolsOutput } from '../useGetPools/types'; @@ -65,6 +65,7 @@ export interface GetAccountTransactionHistoryInput { accountAddress: string; chainId: ChainId; getPoolsData: GetPoolsOutput | undefined; + liquidityHubs: LiquidityHub[]; positionAccountAddress?: Address; contractAddress?: Address; type?: TxType; @@ -82,3 +83,5 @@ export type VTokenAssetMapping = Record< poolName: string; } >; + +export type VhTokenMapping = Record; diff --git a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/useGetAccountTransactionHistory.ts b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/useGetAccountTransactionHistory.ts index 6665356bac..5d09c2088e 100644 --- a/apps/evm/src/clients/api/queries/getAccountTransactionHistory/useGetAccountTransactionHistory.ts +++ b/apps/evm/src/clients/api/queries/getAccountTransactionHistory/useGetAccountTransactionHistory.ts @@ -1,4 +1,5 @@ import { type QueryObserverOptions, useQuery } from '@tanstack/react-query'; +import { liquidityHubs } from '__mocks__/models/liquidityHubs'; import FunctionKey from 'constants/functionKey'; import { useChainId } from 'libs/wallet'; @@ -12,7 +13,7 @@ import { useGetPools } from '../useGetPools'; type TrimmedGetAccountTransactionHistoryInput = Omit< GetAccountTransactionHistoryInput, - 'chainId' | 'getPoolsData' + 'chainId' | 'getPoolsData' | 'liquidityHubs' >; type Options = QueryObserverOptions< @@ -37,6 +38,7 @@ export const useGetAccountTransactionHistory = ( const extendedParams = { ...params, getPoolsData, + liquidityHubs, // TODO: fetch from API chainId, }; diff --git a/apps/evm/src/components/TransactionsList/TransactionRow/Events/formatToLiquidityHubTxEvents/__tests__/index.spec.ts b/apps/evm/src/components/TransactionsList/TransactionRow/Events/formatToLiquidityHubTxEvents/__tests__/index.spec.ts new file mode 100644 index 0000000000..7608632cf2 --- /dev/null +++ b/apps/evm/src/components/TransactionsList/TransactionRow/Events/formatToLiquidityHubTxEvents/__tests__/index.spec.ts @@ -0,0 +1,37 @@ +import { liquidityHubs } from '__mocks__/models/liquidityHubs'; +import BigNumber from 'bignumber.js'; +import { t } from 'libs/translations'; +import type { LiquidityHubTx } from 'types'; +import { formatToLiquidityHubTxEvents } from '..'; + +describe('formatToLiquidityHubTxEvents', () => { + it('formats the underlying token amount and identifies Liquidity Hub as the source', () => { + const [liquidityHub] = liquidityHubs; + const transaction: LiquidityHubTx = { + accountAddress: '0x1000000000000000000000000000000000000000', + amounts: [ + { + amountCents: 7150, + amountTokens: new BigNumber(10), + token: liquidityHub.vhToken.underlyingToken, + }, + ], + blockNumber: '1', + blockTimestamp: new Date('2026-01-01T00:00:00.000Z'), + chainId: liquidityHub.vhToken.chainId, + contractAddress: liquidityHub.vhToken.address, + hash: '0x1', + txType: 'supply', + vhToken: liquidityHub.vhToken, + }; + + const [event] = formatToLiquidityHubTxEvents({ + transaction, + t, + }); + + expect(event.token).toBe(liquidityHub.vhToken.underlyingToken); + expect(event.title).toBe('10 XVS'); + expect(event.description).toBe('$71.5 • XVS • Liquidity Hub'); + }); +}); diff --git a/apps/evm/src/components/TransactionsList/TransactionRow/Events/formatToLiquidityHubTxEvents/index.ts b/apps/evm/src/components/TransactionsList/TransactionRow/Events/formatToLiquidityHubTxEvents/index.ts new file mode 100644 index 0000000000..f0408bfab3 --- /dev/null +++ b/apps/evm/src/components/TransactionsList/TransactionRow/Events/formatToLiquidityHubTxEvents/index.ts @@ -0,0 +1,35 @@ +import type { TFunction } from 'i18next'; +import type { LiquidityHubTx } from 'types'; +import { formatCentsToReadableValue, formatTokensToReadableValue } from 'utilities'; +import type { EventProps } from '../Event'; + +export const formatToLiquidityHubTxEvents = ({ + transaction, + t, +}: { + transaction: LiquidityHubTx; + t: TFunction; +}) => { + const { amounts, vhToken } = transaction; + const primaryAmount = amounts?.[0]; + const token = vhToken.underlyingToken; + const liquidityHubLabel = t('layouts.menu.markets.liquidityHub.label'); + + const title = formatTokensToReadableValue({ + token, + value: primaryAmount?.amountTokens, + }); + const description = primaryAmount + ? `${formatCentsToReadableValue({ + value: primaryAmount.amountCents, + })} • ${token.symbol} • ${liquidityHubLabel}` + : liquidityHubLabel; + + const event: EventProps = { + token, + title, + description, + }; + + return [event]; +}; diff --git a/apps/evm/src/components/TransactionsList/TransactionRow/Events/index.tsx b/apps/evm/src/components/TransactionsList/TransactionRow/Events/index.tsx index c880500bac..c0ae333fc9 100644 --- a/apps/evm/src/components/TransactionsList/TransactionRow/Events/index.tsx +++ b/apps/evm/src/components/TransactionsList/TransactionRow/Events/index.tsx @@ -1,7 +1,7 @@ -import { MARKET_TX_TYPES } from 'constants/marketTxTypes'; import { useTranslation } from 'libs/translations'; -import type { MarketTx, TradeTx, Tx } from 'types'; +import type { Tx } from 'types'; import { Event, type EventProps } from './Event'; +import { formatToLiquidityHubTxEvents } from './formatToLiquidityHubTxEvents'; import { formatToMarketTxEvents } from './formatToMarketTxEvents'; import { formatToTradeTxEvents } from './formatToTradeTxEvents'; @@ -10,13 +10,20 @@ export interface EventsProps { } export const Events: React.FC = ({ transaction }) => { - const { Trans } = useTranslation(); + const { t, Trans } = useTranslation(); - const events: EventProps[] = MARKET_TX_TYPES.some(t => t === transaction.txType) - ? formatToMarketTxEvents({ Trans, transaction: transaction as MarketTx }) - : formatToTradeTxEvents({ - transaction: transaction as TradeTx, - }); + let events: EventProps[]; + + if ('vhToken' in transaction) { + events = formatToLiquidityHubTxEvents({ + transaction, + t, + }); + } else if ('vToken' in transaction) { + events = formatToMarketTxEvents({ Trans, transaction }); + } else { + events = formatToTradeTxEvents({ transaction }); + } return (
diff --git a/apps/evm/src/components/TransactionsList/TransactionRow/index.tsx b/apps/evm/src/components/TransactionsList/TransactionRow/index.tsx index 0508f1e900..620fc718c1 100644 --- a/apps/evm/src/components/TransactionsList/TransactionRow/index.tsx +++ b/apps/evm/src/components/TransactionsList/TransactionRow/index.tsx @@ -1,7 +1,6 @@ import { cn } from '@venusprotocol/ui'; import { useTranslation } from 'libs/translations'; -import { useMemo } from 'react'; import type { Tx } from 'types'; import { generateExplorerUrl, getTransactionName } from 'utilities'; import { Delimiter } from '../../Delimiter'; @@ -18,14 +17,10 @@ export const TransactionRow: React.FC = ({ transaction, cla const { chainId, txType, hash, blockTimestamp } = transaction; const { t } = useTranslation(); - const transactionTitle = useMemo( - () => - getTransactionName({ - txType, - t, - }), - [t, txType], - ); + const transactionTitle = getTransactionName({ + transaction, + t, + }); return ( displays content correctly 1`] = `"All transactionsAll marketsAug 23, 2024Withdraw4:17 AM> 100T USDT$24.06K • USDT • MetaverseAug 6, 2024Supply2:18 AM> 100T USDT$24.01K • USDT • MetaverseAug 5, 2024Borrow2:18 AM> 100T USDT$24.01K • USDT • MetaverseAug 4, 2024Repay2:18 AM> 100T USDT$24.01K • USDT • MetaverseAug 3, 2024Enable collateral2:18 AMEnabled USDT as collateralMetaverseDisable collateral2:18 AMDisabled USDT as collateralMetaverse"`; +exports[`Transactions > displays content correctly 1`] = `"All transactionsAll sourcesAug 24, 2024Supply • Liquidity Hub4:17 AM10 XVS$71.5 • XVS • Liquidity HubAug 23, 2024Withdraw4:17 AM> 100T USDT$24.06K • USDT • MetaverseAug 6, 2024Supply2:18 AM> 100T USDT$24.01K • USDT • MetaverseAug 5, 2024Borrow2:18 AM> 100T USDT$24.01K • USDT • MetaverseAug 4, 2024Repay2:18 AM> 100T USDT$24.01K • USDT • MetaverseAug 3, 2024Enable collateral2:18 AMEnabled USDT as collateralMetaverseDisable collateral2:18 AMDisabled USDT as collateralMetaverse"`; -exports[`Transactions > displays placeholder when there are no transactions to display 1`] = `"All transactionsAll marketsNo transactions yetYour transactions will appear here"`; +exports[`Transactions > displays placeholder when there are no transactions to display 1`] = `"All transactionsAll sourcesNo transactions yetYour transactions will appear here"`; diff --git a/apps/evm/src/pages/Dashboard/Transactions/__tests__/index.spec.tsx b/apps/evm/src/pages/Dashboard/Transactions/__tests__/index.spec.tsx index 3fb8cd54dd..309c0b8d77 100644 --- a/apps/evm/src/pages/Dashboard/Transactions/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Dashboard/Transactions/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ import { waitFor } from '@testing-library/dom'; import fakeAccountAddress from '__mocks__/models/address'; +import { vhXvs } from '__mocks__/models/vhTokens'; import { useGetAccountTransactionHistory } from 'clients/api'; import { type UseIsFeatureEnabledInput, useIsFeatureEnabled } from 'hooks/useIsFeatureEnabled'; import { en } from 'libs/translations'; @@ -10,7 +11,19 @@ import { Transactions } from '..'; describe('Transactions', () => { beforeEach(() => { (useIsFeatureEnabled as Mock).mockImplementation( - ({ name }: UseIsFeatureEnabledInput) => name === 'transactionHistory', + ({ name }: UseIsFeatureEnabledInput) => + name === 'transactionHistory' || name === 'liquidityHub', + ); + }); + + it('includes Liquidity Hub contracts in the source filter', async () => { + const { getByText } = renderComponent(, { + accountAddress: fakeAccountAddress, + routerInitialEntries: [`/?contractAddress=${vhXvs.address}`], + }); + + await waitFor(() => + expect(getByText(`XVS - ${en.layouts.menu.markets.liquidityHub.label}`)).toBeInTheDocument(), ); }); it('displays content correctly', async () => { diff --git a/apps/evm/src/pages/Dashboard/Transactions/index.tsx b/apps/evm/src/pages/Dashboard/Transactions/index.tsx index ce795ab5f4..ca5ed34af7 100644 --- a/apps/evm/src/pages/Dashboard/Transactions/index.tsx +++ b/apps/evm/src/pages/Dashboard/Transactions/index.tsx @@ -1,3 +1,4 @@ +import { liquidityHubs } from '__mocks__/models/liquidityHubs'; import { useGetAccountTransactionHistory, useGetPools } from 'clients/api'; import { Pagination, @@ -13,6 +14,7 @@ import { useTranslation } from 'libs/translations'; import { useAccountAddress, useChainId } from 'libs/wallet'; import { useEffect, useMemo, useRef } from 'react'; import { useSearchParams } from 'react-router'; +import type { Token } from 'types'; import { getTransactionName } from 'utilities'; import { type Address, isAddress } from 'viem'; @@ -102,6 +104,9 @@ export const Transactions: React.FC = () => { const isTransactionHistoryFeatureEnabled = useIsFeatureEnabled({ name: 'transactionHistory', }); + const isLiquidityHubFeatureEnabled = useIsFeatureEnabled({ + name: 'liquidityHub', + }); const { data: historicalTxsData, isLoading: areHistoricalTxsLoading } = useGetAccountTransactionHistory( @@ -124,7 +129,7 @@ export const Transactions: React.FC = () => { const otherOptions: SelectOption[] = MARKET_TX_TYPES.map(type => ({ label: getTransactionName({ - txType: type, + transaction: type, t, }), value: type, @@ -148,7 +153,7 @@ export const Transactions: React.FC = () => { ) || []; const otherOptions: SelectOption[] = []; - const tokenOptions = allAssets + const tokenOptions: Token[] = allAssets .map(a => ({ symbol: `${a.vToken.underlyingToken.symbol} - ${a.poolName}`, iconSrc: a.vToken.underlyingToken.iconSrc, @@ -158,6 +163,24 @@ export const Transactions: React.FC = () => { })) .sort((a, b) => a.symbol.localeCompare(b.symbol)); + if (isLiquidityHubFeatureEnabled) { + const liquidityHubLabel = t('layouts.menu.markets.liquidityHub.label'); + + liquidityHubs.forEach(liquidityHub => { + const { vhToken } = liquidityHub; + + tokenOptions.push({ + symbol: `${vhToken.underlyingToken.symbol} - ${liquidityHubLabel}`, + iconSrc: vhToken.underlyingToken.iconSrc, + address: vhToken.address, + decimals: vhToken.decimals, + chainId: vhToken.chainId, + }); + }); + + tokenOptions.sort((a, b) => a.symbol.localeCompare(b.symbol)); + } + for (const tokenOption of tokenOptions) { otherOptions.push({ label: , @@ -166,7 +189,7 @@ export const Transactions: React.FC = () => { } return [allOption, ...otherOptions]; - }, [t, poolData]); + }, [t, poolData, isLiquidityHubFeatureEnabled]); // Reset contract address filter if the value in the URL is incorrect useEffect(() => { diff --git a/apps/evm/src/types/index.ts b/apps/evm/src/types/index.ts index be38927cba..0ee85d7b78 100644 --- a/apps/evm/src/types/index.ts +++ b/apps/evm/src/types/index.ts @@ -2,7 +2,11 @@ import type { Token as PSToken } from '@pancakeswap/sdk'; import type { ChainId, Token, VToken, VhToken } from '@venusprotocol/chains'; import type { Omit } from '@wagmi/core/internal'; import type BigNumber from 'bignumber.js'; -import type { MARKET_TX_TYPES, TRADE_TX_TYPES } from 'constants/marketTxTypes'; +import type { + LIQUIDITY_HUB_TX_TYPES, + MARKET_TX_TYPES, + TRADE_TX_TYPES, +} from 'constants/marketTxTypes'; import type { VError } from 'libs/errors'; import type { Address, ByteArray, Hex } from 'viem'; @@ -816,7 +820,9 @@ export type TradeTxType = (typeof TRADE_TX_TYPES)[number]; export type MarketTxType = (typeof MARKET_TX_TYPES)[number]; -export type TxType = TradeTxType | MarketTxType; +export type LiquidityHubTxType = (typeof LIQUIDITY_HUB_TX_TYPES)[number]; + +export type TxType = TradeTxType | MarketTxType | LiquidityHubTxType; export interface TxAmount { token: Token; @@ -839,12 +845,18 @@ export interface MarketTx extends BaseTx { poolName: string; vToken: VToken; } + +export interface LiquidityHubTx extends BaseTx { + txType: LiquidityHubTxType; + vhToken: VhToken; +} + export interface TradeTx extends BaseTx { txType: TradeTxType; cycleId: string; } -export type Tx = MarketTx | TradeTx; +export type Tx = MarketTx | LiquidityHubTx | TradeTx; export enum ApiOhlcInterval { '1m' = '1m', diff --git a/apps/evm/src/utilities/getTransactionName/__tests__/index.spec.ts b/apps/evm/src/utilities/getTransactionName/__tests__/index.spec.ts new file mode 100644 index 0000000000..e2803cde20 --- /dev/null +++ b/apps/evm/src/utilities/getTransactionName/__tests__/index.spec.ts @@ -0,0 +1,33 @@ +import { liquidityHubs } from '__mocks__/models/liquidityHubs'; +import { t } from 'libs/translations'; +import type { LiquidityHubTx } from 'types'; +import { getTransactionName } from '..'; + +describe('getTransactionName', () => { + it('identifies Liquidity Hub transactions while keeping filter labels generic', () => { + const [liquidityHub] = liquidityHubs; + const transaction: LiquidityHubTx = { + accountAddress: '0x1000000000000000000000000000000000000000', + blockNumber: '1', + blockTimestamp: new Date('2026-01-01T00:00:00.000Z'), + chainId: liquidityHub.vhToken.chainId, + contractAddress: liquidityHub.vhToken.address, + hash: '0x1', + txType: 'supply', + vhToken: liquidityHub.vhToken, + }; + + expect( + getTransactionName({ + transaction: 'supply', + t, + }), + ).toBe('Supply'); + expect( + getTransactionName({ + transaction, + t, + }), + ).toBe('Supply • Liquidity Hub'); + }); +}); diff --git a/apps/evm/src/utilities/getTransactionName/index.ts b/apps/evm/src/utilities/getTransactionName/index.ts index 6ce3cecc91..eca02af0a4 100644 --- a/apps/evm/src/utilities/getTransactionName/index.ts +++ b/apps/evm/src/utilities/getTransactionName/index.ts @@ -1,21 +1,27 @@ import type { TFunction } from 'i18next'; -import type { TxType } from 'types'; +import type { Tx, TxType } from 'types'; export interface GetTransactionNameInput { - txType: TxType; + transaction: Tx | TxType; t: TFunction; } -export const getTransactionName = ({ txType, t }: GetTransactionNameInput) => { +export const getTransactionName = ({ transaction, t }: GetTransactionNameInput) => { + const txType = typeof transaction === 'string' ? transaction : transaction.txType; + const liquidityHubSuffix = + typeof transaction !== 'string' && 'vhToken' in transaction + ? ` • ${t('layouts.menu.markets.liquidityHub.label')}` + : ''; + switch (txType) { case 'supply': - return t('account.transactions.txType.mint'); + return `${t('account.transactions.txType.mint')}${liquidityHubSuffix}`; case 'repay': return t('account.transactions.txType.repay'); case 'borrow': return t('account.transactions.txType.borrow'); case 'withdraw': - return t('account.transactions.txType.redeem'); + return `${t('account.transactions.txType.redeem')}${liquidityHubSuffix}`; case 'exitMarket': return t('account.transactions.txType.exitMarket'); case 'enterMarket':