Skip to content

Commit c8b5be8

Browse files
feat: add transactions tab
1 parent fc56a7e commit c8b5be8

File tree

6 files changed

+504
-0
lines changed

6 files changed

+504
-0
lines changed

apps/evm/src/libs/translations/translations/en.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,47 @@
7777
"tabs": {
7878
"pools": "Pools",
7979
"settings": "Settings",
80+
"transactions": "Transactions",
8081
"vaults": "Vaults"
8182
},
83+
"transactions": {
84+
"today": "Today",
85+
"yesterday": "Yesterday",
86+
"txText": {
87+
"approve": "Approved spending {{tokenAmount}}",
88+
"enterMarket": "<Styled>Enabled</Styled> {{vTokenSymbol}} as collateral",
89+
"exitMarket": "<Styled>Disabled</Styled> {{vTokenSymbol}} as collateral"
90+
},
91+
"txType": {
92+
"mint": "Supply",
93+
"borrow": "Borrow",
94+
"redeem": "Withdraw",
95+
"repay": "Repayment",
96+
"approve": "Approval",
97+
"enterMarket": "Enabled collateral",
98+
"exitMarket": "Disabled collateral"
99+
},
100+
"selects": {
101+
"txType": {
102+
"all": "All transactions",
103+
"mint": "Supplies",
104+
"borrow": "Borrows",
105+
"redeem": "Withdrawals",
106+
"repay": "Borrow repayments",
107+
"approve": "Approvals",
108+
"enterMarket": "Enabled collateral",
109+
"exitMarket": "Disabled collateral"
110+
},
111+
"source": {
112+
"all": "All markets"
113+
}
114+
},
115+
"placeholder": {
116+
"description": "Your transactions will appear here",
117+
"title": "No transactions yet"
118+
},
119+
"view": "View"
120+
},
82121
"vaults": {
83122
"placeholder": {
84123
"description": "Your vault positions will appear here",
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { cn } from '@venusprotocol/ui';
2+
import { type TransactionWithAsset, TxType } from 'clients/api';
3+
import { useTranslation } from 'libs/translations';
4+
import { useMemo } from 'react';
5+
import { convertMantissaToTokens, formatTokensToReadableValue } from 'utilities';
6+
7+
export interface TransactionDetailsProps {
8+
transactionData: TransactionWithAsset;
9+
className?: string;
10+
}
11+
12+
export const TransactionDetails: React.FC<TransactionDetailsProps> = ({
13+
transactionData,
14+
className,
15+
}) => {
16+
const { transaction, asset } = transactionData;
17+
const { underlyingToken } = asset.vToken;
18+
const { Trans } = useTranslation();
19+
20+
const transactionText = useMemo(() => {
21+
switch (transaction.txType) {
22+
case TxType.Approve:
23+
return (
24+
<Trans
25+
i18nKey="account.transactions.txText.approve"
26+
values={{
27+
tokenAmount: formatTokensToReadableValue({
28+
token: underlyingToken,
29+
value: convertMantissaToTokens({
30+
token: underlyingToken,
31+
value: BigInt(transaction.amountUnderlyingMantissa || 0),
32+
}),
33+
}),
34+
}}
35+
/>
36+
);
37+
case TxType.EnterMarket:
38+
return (
39+
<Trans
40+
i18nKey="account.transactions.txText.enterMarket"
41+
components={{ Styled: <span className="text-green" /> }}
42+
values={{ vTokenSymbol: asset.vToken.symbol }}
43+
/>
44+
);
45+
case TxType.ExitMarket:
46+
return (
47+
<Trans
48+
i18nKey="account.transactions.txText.exitMarket"
49+
components={{ Styled: <span className="text-red" /> }}
50+
values={{ vTokenSymbol: asset.vToken.symbol }}
51+
/>
52+
);
53+
default:
54+
return formatTokensToReadableValue({
55+
token: underlyingToken,
56+
value: convertMantissaToTokens({
57+
token: underlyingToken,
58+
value: BigInt(transaction.amountUnderlyingMantissa || 0),
59+
}),
60+
});
61+
}
62+
}, [asset, Trans, transaction, underlyingToken]);
63+
64+
const transactionSecondaryText = useMemo(() => {
65+
switch (transaction.txType) {
66+
case TxType.EnterMarket:
67+
case TxType.ExitMarket:
68+
return asset.poolName;
69+
default:
70+
return `${asset.vToken.symbol}${asset.poolName}`;
71+
}
72+
}, [transaction, asset]);
73+
74+
return (
75+
<div className={cn('flex flex-col self-start', className)}>
76+
<span className="text-sm">{transactionText}</span>
77+
<span className="text-grey text-xs">{transactionSecondaryText}</span>
78+
</div>
79+
);
80+
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { cn } from '@venusprotocol/ui';
2+
import { type TransactionWithAsset, TxType } from 'clients/api';
3+
import { Icon, type IconName, TokenIcon } from 'components';
4+
import { format } from 'date-fns';
5+
import { useTranslation } from 'libs/translations';
6+
import { useMemo } from 'react';
7+
import { generateExplorerUrl } from 'utilities';
8+
import { TransactionDetails } from '../TransactionDetails';
9+
10+
export interface TransactionRowProps {
11+
transactionData: TransactionWithAsset;
12+
className?: string;
13+
}
14+
15+
const getTransactionIcon = (txType: TxType): IconName => {
16+
switch (txType) {
17+
case TxType.Mint:
18+
case TxType.Repay:
19+
return 'transactionIn';
20+
case TxType.Borrow:
21+
case TxType.Redeem:
22+
return 'transactionOut';
23+
default:
24+
return 'transactionCollateral';
25+
}
26+
};
27+
28+
export const TransactionRow: React.FC<TransactionRowProps> = ({ transactionData, className }) => {
29+
const { transaction, asset } = transactionData;
30+
const { underlyingToken } = asset.vToken;
31+
const { t } = useTranslation();
32+
33+
const transactionTitle = useMemo(() => {
34+
switch (transaction.txType) {
35+
case TxType.Mint:
36+
return t('account.transactions.txType.mint');
37+
case TxType.Repay:
38+
return t('account.transactions.txType.repay');
39+
case TxType.Borrow:
40+
return t('account.transactions.txType.borrow');
41+
case TxType.Redeem:
42+
return t('account.transactions.txType.redeem');
43+
case TxType.Approve:
44+
return t('account.transactions.txType.approve');
45+
case TxType.ExitMarket:
46+
return t('account.transactions.txType.exitMarket');
47+
default:
48+
return t('account.transactions.txType.enterMarket');
49+
}
50+
}, [t, transaction]);
51+
52+
return (
53+
<li
54+
className={cn(
55+
'flex flex-col px-4 py-3 md:p-0 bg-cards rounded-xl md:rounded-none w-full md:flex-row items-center justify-evenly',
56+
className,
57+
)}
58+
>
59+
<a
60+
className="flex flex-col md:flex-row w-full"
61+
href={generateExplorerUrl({
62+
hash: transaction.txHash,
63+
urlType: 'tx',
64+
chainId: transaction.chainId,
65+
})}
66+
target="_blank"
67+
rel="noreferrer"
68+
>
69+
<div className="flex w-full md:w-70 items-center">
70+
<div className="p-[6px] md:p-2 mr-2 md:mr-3 flex items-center justify-center bg-lightGrey rounded-full">
71+
<Icon className="h-3 w-3 md:h-4 md:w-4" name={getTransactionIcon(transaction.txType)} />
72+
</div>
73+
<div className="flex flex-row w-full md:w-auto items-center md:items-start justify-between md:flex-col">
74+
<span className="text-sm">{transactionTitle}</span>
75+
<span className="text-grey text-xs">{format(transaction.txTimestamp, 'hh:mm aa')}</span>
76+
</div>
77+
</div>
78+
79+
<hr className="block w-full text-lightGrey my-3 md:hidden" />
80+
81+
<div className="flex w-full items-center">
82+
<TokenIcon className="mr-2 self-start" token={underlyingToken} />
83+
<TransactionDetails className="flex-1" transactionData={transactionData} />
84+
<a
85+
className="hidden md:flex items-center self-start text-grey space-x-1 mt-1"
86+
href={generateExplorerUrl({
87+
hash: transaction.txHash,
88+
urlType: 'tx',
89+
chainId: transaction.chainId,
90+
})}
91+
target="_blank"
92+
rel="noreferrer"
93+
>
94+
<span className="hidden lg:block text-sm underline">
95+
{t('account.transactions.view')}
96+
</span>
97+
<Icon name="transactionLink" />
98+
</a>
99+
</div>
100+
</a>
101+
</li>
102+
);
103+
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Spinner, cn } from '@venusprotocol/ui';
2+
import type { GetAccountHistoricalTransactionsOutput, TransactionWithAsset } from 'clients/api';
3+
import { Pagination } from 'components';
4+
import { format, isToday, isYesterday } from 'date-fns';
5+
import { useTranslation } from 'libs/translations';
6+
import { Placeholder } from 'pages/Account/Placeholder';
7+
import { useMemo } from 'react';
8+
import { TransactionRow } from '../TransactionRow';
9+
10+
const INITIAL_PAGE_INDEX = 1;
11+
const ITEMS_PER_PAGE_COUNT = 20;
12+
13+
export interface TransactionsListProps {
14+
transactions: GetAccountHistoricalTransactionsOutput['transactions'];
15+
transactionsCount: number;
16+
onPageChange: (newPage: number) => void;
17+
isLoading: boolean;
18+
className?: string;
19+
}
20+
21+
export const TransactionsList: React.FC<TransactionsListProps> = ({
22+
transactions,
23+
transactionsCount,
24+
isLoading,
25+
onPageChange,
26+
className,
27+
}) => {
28+
const { t } = useTranslation();
29+
30+
const transactionsGroupedByDate = useMemo(() => {
31+
const groupedByDate = transactions.reduce<Record<string, TransactionWithAsset[]>>(
32+
(acc, txData) => {
33+
const isTodayGroup = isToday(txData.transaction.txTimestamp);
34+
const isYesterdayGroup = isYesterday(txData.transaction.txTimestamp);
35+
36+
let dayGroup = format(txData.transaction.txTimestamp, 'MMM dd, yyyy');
37+
38+
if (isTodayGroup) {
39+
dayGroup = t('account.transactions.today');
40+
}
41+
42+
if (isYesterdayGroup) {
43+
dayGroup = t('account.transactions.yesterday');
44+
}
45+
46+
const dayTxs = acc[dayGroup] ? [...acc[dayGroup], txData] : [txData];
47+
48+
return {
49+
...acc,
50+
[dayGroup]: dayTxs,
51+
};
52+
},
53+
{},
54+
);
55+
56+
return groupedByDate;
57+
}, [transactions, t]);
58+
59+
if (isLoading) {
60+
return <Spinner />;
61+
}
62+
63+
if (transactions.length === 0) {
64+
return (
65+
<Placeholder
66+
iconName="transactionFile"
67+
title={t('account.transactions.placeholder.title')}
68+
description={t('account.transactions.placeholder.description')}
69+
/>
70+
);
71+
}
72+
73+
return (
74+
<>
75+
<ul
76+
className={cn(
77+
'flex flex-col w-full items-center justify-evenly space-y-6 md:bg-cards md:p-4 md:rounded-xl',
78+
className,
79+
)}
80+
>
81+
{Object.keys(transactionsGroupedByDate).map(day => (
82+
<div className="flex flex-col w-full space-y-3">
83+
<span className="text-base font-semibold">{day}</span>
84+
{transactionsGroupedByDate[day].map(transactionData => (
85+
<TransactionRow transactionData={transactionData} />
86+
))}
87+
</div>
88+
))}
89+
</ul>
90+
<Pagination
91+
initialPageIndex={INITIAL_PAGE_INDEX}
92+
itemsCount={transactionsCount}
93+
itemsPerPageCount={ITEMS_PER_PAGE_COUNT}
94+
onChange={onPageChange}
95+
/>
96+
</>
97+
);
98+
};

0 commit comments

Comments
 (0)