Token transfers list page
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="m6.134 10.067 3.722-3.828a.97.97 0 0 1 1.337.06c.177.182.283.428.292.69a1.05 1.05 0 0 1-.234.704L9.243 9.765h14.371c.261 0 .513.107.7.299s.294.454.294.73c0 .274-.107.537-.294.729a.98.98 0 0 1-.7.299H6.831a.97.97 0 0 1-.55-.17 1 1 0 0 1-.368-.46 1.06 1.06 0 0 1 .22-1.126m18.723 6a.965.965 0 0 1 .55.17c.163.111.291.27.368.459a1.06 1.06 0 0 1-.22 1.125l-3.737 3.842-.006.008a1 1 0 0 1-.323.255.97.97 0 0 1-1.13-.197q-.146-.152-.224-.353a1.06 1.06 0 0 1 .281-1.16l.007-.007 2.022-2.086h-5.882c.104-.685.184-1.404 0-2.073z" clip-rule="evenodd"/>
|
||||
<path fill="currentColor" d="M10 19.513c-1.272 0-2.48-.276-3.395-.778C5.57 18.169 5 17.374 5 16.497c0-.878.57-1.672 1.605-2.239.917-.502 2.123-.778 3.395-.778s2.48.276 3.395.778C14.43 14.825 15 15.622 15 16.497s-.57 1.671-1.605 2.238c-.917.502-2.123.778-3.395.778m0-4.793c-1.052 0-2.073.228-2.8.626-.61.334-.96.753-.96 1.15 0 .398.35.818.96 1.151.727.397 1.746.626 2.8.626s2.073-.228 2.8-.626c.61-.333.96-.753.96-1.15 0-.398-.35-.817-.96-1.151-.727-.398-1.746-.626-2.8-.626"/>
|
||||
<path stroke="currentColor" stroke-width=".3" d="M10 19.513c-1.272 0-2.48-.276-3.395-.778C5.57 18.169 5 17.374 5 16.497c0-.878.57-1.672 1.605-2.239.917-.502 2.123-.778 3.395-.778s2.48.276 3.395.778C14.43 14.825 15 15.622 15 16.497s-.57 1.671-1.605 2.238c-.917.502-2.123.778-3.395.778Zm0-4.793c-1.052 0-2.073.228-2.8.626-.61.334-.96.753-.96 1.15 0 .398.35.818.96 1.151.727.397 1.746.626 2.8.626s2.073-.228 2.8-.626c.61-.333.96-.753.96-1.15 0-.398-.35-.817-.96-1.151-.727-.398-1.746-.626-2.8-.626Z"/>
|
||||
<path fill="currentColor" d="M10 23.962c-1.272 0-2.48-.276-3.395-.778C5.57 22.618 5 21.823 5 20.946v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .397.35.817.96 1.151.727.397 1.748.626 2.8.626 1.053 0 2.073-.229 2.8-.626.61-.334.96-.754.96-1.151v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .877-.57 1.672-1.605 2.238-.917.502-2.123.778-3.395.778"/>
|
||||
<path stroke="currentColor" stroke-width=".3" d="M10 23.962c-1.272 0-2.48-.276-3.395-.778C5.57 22.618 5 21.823 5 20.946v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .397.35.817.96 1.151.727.397 1.748.626 2.8.626 1.053 0 2.073-.229 2.8-.626.61-.334.96-.754.96-1.151v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .877-.57 1.672-1.605 2.238-.917.502-2.123.778-3.395.778Z"/>
|
||||
<path fill="currentColor" d="M10 21.738c-1.272 0-2.48-.277-3.395-.778C5.57 20.393 5 19.598 5 18.72a.62.62 0 1 1 1.24 0c0 .397.35.817.96 1.151.727.398 1.748.626 2.8.626 1.053 0 2.073-.228 2.8-.626.61-.334.96-.754.96-1.15a.62.62 0 1 1 1.24 0c0 .876-.57 1.671-1.605 2.238-.917.501-2.123.778-3.395.778"/>
|
||||
<path stroke="currentColor" stroke-width=".3" d="M10 21.738c-1.272 0-2.48-.277-3.395-.778C5.57 20.393 5 19.598 5 18.72a.62.62 0 1 1 1.24 0c0 .397.35.817.96 1.151.727.398 1.748.626 2.8.626 1.053 0 2.073-.228 2.8-.626.61-.334.96-.754.96-1.15a.62.62 0 1 1 1.24 0c0 .876-.57 1.671-1.605 2.238-.917.501-2.123.778-3.395.778Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -599,6 +599,12 @@ export const RESOURCES = {
|
||||
filterFields: [],
|
||||
},
|
||||
|
||||
// TOKEN TRANSFERS
|
||||
token_transfers_all: {
|
||||
path: '/api/v2/token-transfers',
|
||||
filterFields: [ 'type' as const ],
|
||||
},
|
||||
|
||||
// APP STATS
|
||||
stats: {
|
||||
path: '/api/v2/stats',
|
||||
@@ -1015,7 +1021,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
|
||||
'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' |
|
||||
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
|
||||
'watchlist' | 'private_tags_address' | 'private_tags_tx' |
|
||||
'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history';
|
||||
'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history' |
|
||||
'token_transfers_all';
|
||||
|
||||
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
|
||||
|
||||
@@ -1184,6 +1191,7 @@ Q extends 'address_mud_records' ? AddressMudRecords :
|
||||
Q extends 'address_mud_record' ? AddressMudRecord :
|
||||
Q extends 'withdrawals' ? WithdrawalsResponse :
|
||||
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
|
||||
Q extends 'token_transfers_all' ? TokenTransferResponse :
|
||||
never;
|
||||
/* eslint-enable @typescript-eslint/indent */
|
||||
|
||||
@@ -1218,6 +1226,7 @@ Q extends 'user_ops' ? UserOpsFilters :
|
||||
Q extends 'validators_stability' ? ValidatorsStabilityFilters :
|
||||
Q extends 'address_mud_tables' ? AddressMudTablesFilter :
|
||||
Q extends 'address_mud_records' ? AddressMudRecordsFilter :
|
||||
Q extends 'token_transfers_all' ? TokenTransferFilters :
|
||||
never;
|
||||
/* eslint-enable @typescript-eslint/indent */
|
||||
|
||||
|
||||
@@ -181,6 +181,21 @@ export default function useNavItems(): ReturnType {
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
const tokensNavItems = [
|
||||
{
|
||||
text: 'Tokens',
|
||||
nextRoute: { pathname: '/tokens' as const },
|
||||
icon: 'token',
|
||||
isActive: pathname.startsWith('/token'),
|
||||
},
|
||||
{
|
||||
text: 'Token transfers',
|
||||
nextRoute: { pathname: '/token-transfers' as const },
|
||||
icon: 'token-transfers',
|
||||
isActive: pathname === '/token-transfers',
|
||||
},
|
||||
];
|
||||
|
||||
const apiNavItems: Array<NavItem> = [
|
||||
config.features.restApiDocs.isEnabled ? {
|
||||
text: 'REST API',
|
||||
@@ -234,9 +249,9 @@ export default function useNavItems(): ReturnType {
|
||||
},
|
||||
{
|
||||
text: 'Tokens',
|
||||
nextRoute: { pathname: '/tokens' as const },
|
||||
icon: 'token',
|
||||
isActive: pathname.startsWith('/token'),
|
||||
isActive: tokensNavItems.flat().some(item => isInternalItem(item) && item.isActive),
|
||||
subItems: tokensNavItems,
|
||||
},
|
||||
config.features.marketplace.isEnabled ? {
|
||||
text: 'DApps',
|
||||
|
||||
@@ -51,6 +51,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/validators': 'Root page',
|
||||
'/gas-tracker': 'Root page',
|
||||
'/mud-worlds': 'Root page',
|
||||
'/token-transfers': 'Root page',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'Regular page',
|
||||
|
||||
@@ -55,6 +55,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/validators': DEFAULT_TEMPLATE,
|
||||
'/gas-tracker': DEFAULT_TEMPLATE,
|
||||
'/mud-worlds': DEFAULT_TEMPLATE,
|
||||
'/token-transfers': DEFAULT_TEMPLATE,
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': DEFAULT_TEMPLATE,
|
||||
|
||||
@@ -51,6 +51,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/validators': '%network_name% validators list',
|
||||
'/gas-tracker': '%network_name% gas tracker - Current gas fees',
|
||||
'/mud-worlds': '%network_name% MUD worlds list',
|
||||
'/token-transfers': '%network_name% token transfers',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': '%network_name% login',
|
||||
|
||||
@@ -49,6 +49,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/validators': 'Validators list',
|
||||
'/gas-tracker': 'Gas tracker',
|
||||
'/mud-worlds': 'MUD worlds',
|
||||
'/token-transfers': 'Token transfers',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'Login',
|
||||
|
||||
@@ -42,6 +42,7 @@ export const erc20: TokenTransfer = {
|
||||
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
|
||||
type: 'token_transfer',
|
||||
timestamp: '2022-10-10T14:34:30.000000Z',
|
||||
block_number: '12345',
|
||||
block_hash: '1',
|
||||
log_index: '1',
|
||||
method: 'updateSmartAsset',
|
||||
@@ -88,6 +89,7 @@ export const erc721: TokenTransfer = {
|
||||
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
|
||||
type: 'token_transfer',
|
||||
timestamp: '2022-10-10T14:34:30.000000Z',
|
||||
block_number: '12345',
|
||||
block_hash: '1',
|
||||
log_index: '1',
|
||||
method: 'updateSmartAsset',
|
||||
@@ -136,6 +138,7 @@ export const erc1155A: TokenTransfer = {
|
||||
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
|
||||
type: 'token_minting',
|
||||
timestamp: '2022-10-10T14:34:30.000000Z',
|
||||
block_number: '12345',
|
||||
block_hash: '1',
|
||||
log_index: '1',
|
||||
};
|
||||
@@ -214,6 +217,7 @@ export const erc404A: TokenTransfer = {
|
||||
type: 'token_transfer',
|
||||
method: 'swap',
|
||||
timestamp: '2022-10-10T14:34:30.000000Z',
|
||||
block_number: '12345',
|
||||
block_hash: '1',
|
||||
log_index: '1',
|
||||
};
|
||||
|
||||
@@ -59,6 +59,7 @@ declare module "nextjs-routes" {
|
||||
| StaticRoute<"/stats">
|
||||
| DynamicRoute<"/token/[hash]", { "hash": string }>
|
||||
| DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }>
|
||||
| StaticRoute<"/token-transfers">
|
||||
| StaticRoute<"/tokens">
|
||||
| DynamicRoute<"/tx/[hash]", { "hash": string }>
|
||||
| StaticRoute<"/txs">
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
const TokenTransfers = dynamic(() => import('ui/pages/TokenTransfers'), { ssr: false });
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/token-transfers">
|
||||
<TokenTransfers/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { base as getServerSideProps } from 'nextjs/getServerSideProps';
|
||||
@@ -149,6 +149,7 @@
|
||||
| "swap"
|
||||
| "testnet"
|
||||
| "token-placeholder"
|
||||
| "token-transfers"
|
||||
| "token"
|
||||
| "tokens"
|
||||
| "tokens/xdai"
|
||||
|
||||
@@ -91,6 +91,7 @@ export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenH
|
||||
|
||||
export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
|
||||
block_hash: BLOCK_HASH,
|
||||
block_number: '123456',
|
||||
from: ADDRESS_PARAMS,
|
||||
log_index: '4',
|
||||
method: 'addLiquidity',
|
||||
|
||||
@@ -51,6 +51,7 @@ interface TokenTransferBase {
|
||||
from: AddressParam;
|
||||
to: AddressParam;
|
||||
timestamp: string;
|
||||
block_number: string;
|
||||
block_hash: string;
|
||||
log_index: string;
|
||||
method?: string;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { mixTokens } from 'mocks/tokens/tokenTransfer';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import TokenTransfers from './TokenTransfers';
|
||||
|
||||
test('base view +@mobile', async({ render, mockTextAd, mockApiResponse }) => {
|
||||
await mockTextAd();
|
||||
await mockApiResponse('token_transfers_all', mixTokens, { queryParams: { type: [] } });
|
||||
const component = await render(<Box pt={{ base: '106px', lg: 0 }}> <TokenTransfers/> </Box>);
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Show, Hide } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { TokenType } from 'types/api/token';
|
||||
|
||||
import { getTokenTransfersStub } from 'stubs/token';
|
||||
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import PopoverFilter from 'ui/shared/filters/PopoverFilter';
|
||||
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
import { getTokenFilterValue } from 'ui/tokens/utils';
|
||||
import TokenTransfersListItem from 'ui/tokenTransfers/TokenTransfersListItem';
|
||||
import TokenTransfersTable from 'ui/tokenTransfers/TokenTransfersTable';
|
||||
const TokenTransfers = () => {
|
||||
const router = useRouter();
|
||||
const [ typeFilter, setTypeFilter ] = React.useState<Array<TokenType>>(getTokenFilterValue(router.query.type) || []);
|
||||
|
||||
const tokenTransfersQuery = useQueryWithPages({
|
||||
resourceName: 'token_transfers_all',
|
||||
filters: { type: typeFilter },
|
||||
options: {
|
||||
placeholderData: getTokenTransfersStub(),
|
||||
},
|
||||
});
|
||||
|
||||
const handleTokenTypesChange = React.useCallback((value: Array<TokenType>) => {
|
||||
tokenTransfersQuery.onFilterChange({ type: value });
|
||||
setTypeFilter(value);
|
||||
}, [ tokenTransfersQuery ]);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
{ tokenTransfersQuery.data?.items.map((item, index) => (
|
||||
<TokenTransfersListItem
|
||||
key={ item.block_number + item.log_index + (tokenTransfersQuery.isPlaceholderData ? index : '') }
|
||||
isLoading={ tokenTransfersQuery.isPlaceholderData }
|
||||
item={ item }
|
||||
/>
|
||||
)) }
|
||||
</Show>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<TokenTransfersTable
|
||||
items={ tokenTransfersQuery.data?.items }
|
||||
top={ tokenTransfersQuery.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
|
||||
isLoading={ tokenTransfersQuery.isPlaceholderData }
|
||||
/>
|
||||
</Hide>
|
||||
</>
|
||||
);
|
||||
|
||||
const filter = (
|
||||
<PopoverFilter contentProps={{ w: '200px' }} appliedFiltersNum={ typeFilter.length }>
|
||||
<TokenTypeFilter<TokenType> onChange={ handleTokenTypesChange } defaultValue={ typeFilter } nftOnly={ false }/>
|
||||
</PopoverFilter>
|
||||
);
|
||||
|
||||
const actionBar = (
|
||||
<ActionBar mt={ -6 }>
|
||||
{ filter }
|
||||
<Pagination { ...tokenTransfersQuery.pagination }/>
|
||||
</ActionBar>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title="Token Transfers"
|
||||
withTextAd
|
||||
/>
|
||||
<DataListDisplay
|
||||
isError={ tokenTransfersQuery.isError }
|
||||
items={ tokenTransfersQuery.data?.items }
|
||||
emptyText="There are no token transfers."
|
||||
content={ content }
|
||||
actionBar={ actionBar }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenTransfers;
|
||||
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 35 KiB |
@@ -99,7 +99,7 @@ test.describe('with tooltips', () => {
|
||||
|
||||
await component.locator('header').hover();
|
||||
await page.locator('div[aria-label="Expand/Collapse menu"]').click();
|
||||
await page.locator('a[aria-label="Tokens link"]').hover();
|
||||
await page.locator('a[aria-label="DApps link"]').hover();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,110 @@
|
||||
import { Flex, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TokenTransfer } from 'types/api/tokenTransfer';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import NftEntity from 'ui/shared/entities/nft/NftEntity';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = {
|
||||
item: TokenTransfer;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const TokenTransfersListItem = ({ item, isLoading }: Props) => {
|
||||
|
||||
const { valueStr } = 'value' in item.total && item.total.value !== null ? getCurrencyValue({
|
||||
value: item.total.value,
|
||||
exchangeRate: item.token.exchange_rate,
|
||||
accuracy: 8,
|
||||
accuracyUsd: 2,
|
||||
decimals: item.total.decimals || '0',
|
||||
}) : { valueStr: null };
|
||||
|
||||
return (
|
||||
<ListItemMobileGrid.Container>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn hash</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<TxEntity hash={ item.tx_hash } isLoading={ isLoading } truncation="constant_long" noIcon/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.timestamp }
|
||||
enableIncrement
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
{ item.method && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Method</ListItemMobileGrid.Label><ListItemMobileGrid.Value>
|
||||
<Tag isLoading={ isLoading }>{ item.method }</Tag>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Block</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<BlockEntity number={ item.block_number } isLoading={ isLoading } noIcon/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>From</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<AddressEntity address={ item.from } isLoading={ isLoading } truncation="constant"/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>To</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<AddressEntity address={ item.to } isLoading={ isLoading } truncation="constant"/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
{ 'token_id' in item.total && (NFT_TOKEN_TYPE_IDS.includes(item.token.type)) && item.total.token_id !== null && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Token ID</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value overflow="hidden">
|
||||
<NftEntity
|
||||
hash={ item.token.address }
|
||||
id={ item.total.token_id }
|
||||
isLoading={ isLoading }
|
||||
noIcon
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ valueStr && (item.token.type === 'ERC-20' || item.token.type === 'ERC-1155') && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Amount</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Flex gap={ 2 } overflow="hidden">
|
||||
<Skeleton isLoaded={ !isLoading } wordBreak="break-all">
|
||||
{ valueStr }
|
||||
</Skeleton>
|
||||
<TokenEntity
|
||||
token={ item.token }
|
||||
isLoading={ isLoading }
|
||||
onlySymbol
|
||||
noCopy
|
||||
width="auto"
|
||||
minW="auto"
|
||||
maxW="100px"
|
||||
/>
|
||||
</Flex>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
</ListItemMobileGrid.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenTransfersListItem;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TokenTransfer } from 'types/api/tokenTransfer';
|
||||
|
||||
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
import TokenTransferTableItem from 'ui/tokenTransfers/TokenTransfersTableItem';
|
||||
|
||||
interface Props {
|
||||
items?: Array<TokenTransfer>;
|
||||
top: number;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TokenTransferTable = ({ items, top, isLoading }: Props) => {
|
||||
return (
|
||||
<AddressHighlightProvider>
|
||||
<Table variant="simple" size="sm" minW="950px" style={{ tableLayout: 'auto' }}>
|
||||
<Thead top={ top }>
|
||||
<Tr>
|
||||
<Th>Txn hash</Th>
|
||||
<Th>Method</Th>
|
||||
<Th>Block</Th>
|
||||
<Th>From/To</Th>
|
||||
<Th>Token ID</Th>
|
||||
<Th isNumeric>Amount</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ items?.map((item, index) => (
|
||||
<TokenTransferTableItem
|
||||
key={ item.block_number + item.log_index + (isLoading ? index : '') }
|
||||
item={ item }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
)) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
</AddressHighlightProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TokenTransferTable);
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Tr, Td, Flex, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TokenTransfer } from 'types/api/tokenTransfer';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
|
||||
import AddressFromTo from 'ui/shared/address/AddressFromTo';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import NftEntity from 'ui/shared/entities/nft/NftEntity';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = {
|
||||
item: TokenTransfer;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TokenTransferTableItem = ({ item, isLoading }: Props) => {
|
||||
const { valueStr } = 'value' in item.total && item.total.value !== null ? getCurrencyValue({
|
||||
value: item.total.value,
|
||||
exchangeRate: item.token.exchange_rate,
|
||||
accuracy: 8,
|
||||
accuracyUsd: 2,
|
||||
decimals: item.total.decimals || '0',
|
||||
}) : { valueStr: null };
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td>
|
||||
<TxEntity
|
||||
hash={ item.tx_hash }
|
||||
isLoading={ isLoading }
|
||||
fontWeight={ 600 }
|
||||
noIcon
|
||||
truncation="constant_long"
|
||||
/>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.timestamp }
|
||||
enableIncrement
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
fontWeight="400"
|
||||
display="inline-block"
|
||||
/>
|
||||
</Td>
|
||||
<Td maxW="120px">
|
||||
{ item.method && <Tag isLoading={ isLoading }>{ item.method }</Tag> }
|
||||
</Td>
|
||||
<Td>
|
||||
<BlockEntity number={ item.block_number } isLoading={ isLoading } noIcon/>
|
||||
</Td>
|
||||
<Td>
|
||||
<AddressFromTo
|
||||
maxW={{ lg: '220px', xl: '320px' }}
|
||||
from={ item.from }
|
||||
to={ item.to }
|
||||
isLoading={ isLoading }
|
||||
mode={{ lg: 'compact', xl: 'long' }}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
{ 'token_id' in item.total && (NFT_TOKEN_TYPE_IDS.includes(item.token.type)) && item.total.token_id !== null ? (
|
||||
<NftEntity
|
||||
hash={ item.token.address }
|
||||
id={ item.total.token_id }
|
||||
isLoading={ isLoading }
|
||||
maxW="140px"
|
||||
/>
|
||||
) : '-' }
|
||||
</Td>
|
||||
<Td isNumeric verticalAlign="top">
|
||||
{ valueStr ? (
|
||||
<Flex gap={ 2 } overflow="hidden" justifyContent="flex-end">
|
||||
<Skeleton isLoaded={ !isLoading } wordBreak="break-all">
|
||||
{ valueStr }
|
||||
</Skeleton>
|
||||
<TokenEntity
|
||||
token={ item.token }
|
||||
isLoading={ isLoading }
|
||||
onlySymbol
|
||||
noCopy
|
||||
width="auto"
|
||||
minW="auto"
|
||||
maxW="100px"
|
||||
/>
|
||||
</Flex>
|
||||
) : '-'
|
||||
}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TokenTransferTableItem);
|
||||
@@ -112,7 +112,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement,
|
||||
</Td>
|
||||
) }
|
||||
{ !config.UI.views.tx.hiddenFields?.tx_fee && (
|
||||
<Td isNumeric>
|
||||
<Td isNumeric maxW="220px">
|
||||
<TxFee
|
||||
tx={ tx }
|
||||
accuracy={ 8 }
|
||||
|
||||