Commit 8969dbd0 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #505 from blockscout/token-transfers

Token transfers
parents ea5897b7 4478730c
...@@ -208,6 +208,11 @@ export const RESOURCES = { ...@@ -208,6 +208,11 @@ export const RESOURCES = {
paginationFields: [ 'items_count' as const, 'value' as const ], paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [], filterFields: [],
}, },
token_transfers: {
path: '/api/v2/tokens/:hash/transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
// HOMEPAGE // HOMEPAGE
homepage_stats: { homepage_stats: {
...@@ -278,7 +283,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -278,7 +283,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_logs' | 'address_tokens' |
'token_holders'; 'token_transfers' | 'token_holders';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -323,6 +328,7 @@ Q extends 'address_logs' ? LogsResponseAddress : ...@@ -323,6 +328,7 @@ Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse : Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'token' ? TokenInfo : Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters : Q extends 'token_counters' ? TokenCounters :
Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders : Q extends 'token_holders' ? TokenHolders :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
...@@ -338,6 +344,7 @@ export type PaginationFilters<Q extends PaginatedResources> = ...@@ -338,6 +344,7 @@ export type PaginationFilters<Q extends PaginatedResources> =
Q extends 'blocks' ? BlockFilters : Q extends 'blocks' ? BlockFilters :
Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters : Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters : Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters : Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters : Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter : Q extends 'address_tokens' ? AddressTokensFilter :
......
...@@ -18,6 +18,7 @@ SocketMessage.AddressCoinBalance | ...@@ -18,6 +18,7 @@ SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs | SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending | SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer | SocketMessage.AddressTokenTransfer |
SocketMessage.TokenTransfers |
SocketMessage.Unknown; SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
...@@ -42,5 +43,6 @@ export namespace SocketMessage { ...@@ -42,5 +43,6 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -40,6 +40,7 @@ export const erc20: TokenTransfer = { ...@@ -40,6 +40,7 @@ export const erc20: TokenTransfer = {
timestamp: '2022-10-10T14:34:30.000000Z', timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1', block_hash: '1',
log_index: '1', log_index: '1',
method: 'updateSmartAsset',
}; };
export const erc721: TokenTransfer = { export const erc721: TokenTransfer = {
...@@ -81,6 +82,7 @@ export const erc721: TokenTransfer = { ...@@ -81,6 +82,7 @@ export const erc721: TokenTransfer = {
timestamp: '2022-10-10T14:34:30.000000Z', timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1', block_hash: '1',
log_index: '1', log_index: '1',
method: 'updateSmartAsset',
}; };
export const erc1155: TokenTransfer = { export const erc1155: TokenTransfer = {
......
...@@ -39,6 +39,7 @@ interface TokenTransferBase { ...@@ -39,6 +39,7 @@ interface TokenTransferBase {
timestamp: string; timestamp: string;
block_hash: string; block_hash: string;
log_index: string; log_index: string;
method?: string;
} }
export type TokenTransferPagination = { export type TokenTransferPagination = {
......
...@@ -10,6 +10,7 @@ import Token from './Token'; ...@@ -10,6 +10,7 @@ import Token from './Token';
const TOKEN_API_URL = buildApiUrl('token', { hash: '1' }); const TOKEN_API_URL = buildApiUrl('token', { hash: '1' });
const TOKEN_COUNTERS_API_URL = buildApiUrl('token_counters', { hash: '1' }); const TOKEN_COUNTERS_API_URL = buildApiUrl('token_counters', { hash: '1' });
const TOKEN_TRANSFERS_API_URL = buildApiUrl('token_transfers', { hash: '1' });
const ADDRESS_API_URL = buildApiUrl('address', { id: '1' }); const ADDRESS_API_URL = buildApiUrl('address', { id: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
...@@ -33,6 +34,10 @@ test('base view +@dark-mode', async({ mount, page }) => { ...@@ -33,6 +34,10 @@ test('base view +@dark-mode', async({ mount, page }) => {
status: 200, status: 200,
body: JSON.stringify(tokenCounters), body: JSON.stringify(tokenCounters),
})); }));
await page.route(TOKEN_TRANSFERS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({}),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
......
...@@ -15,6 +15,7 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; ...@@ -15,6 +15,7 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenContractInfo from 'ui/token/TokenContractInfo'; import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails'; import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders'; import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
export type TokenTabs = 'token_transfers' | 'holders' export type TokenTabs = 'token_transfers' | 'holders'
...@@ -29,13 +30,14 @@ const TokenPageContent = () => { ...@@ -29,13 +30,14 @@ const TokenPageContent = () => {
queryOptions: { enabled: Boolean(router.query.hash) }, queryOptions: { enabled: Boolean(router.query.hash) },
}); });
// const transfersQuery = useQueryWithPages({ const transfersQuery = useQueryWithPages({
// resourceName: 'token_transfers', resourceName: 'token_transfers',
// pathParams: { hash: router.query.hash?.toString() }, pathParams: { hash: router.query.hash?.toString() },
// options: { scrollRef,
// enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data), options: {
// }, enabled: Boolean(router.query.hash && (!router.query.tab || router.query.tab === 'token_transfers') && tokenQuery.data),
// }); },
});
const holdersQuery = useQueryWithPages({ const holdersQuery = useQueryWithPages({
resourceName: 'token_holders', resourceName: 'token_holders',
...@@ -47,16 +49,18 @@ const TokenPageContent = () => { ...@@ -47,16 +49,18 @@ const TokenPageContent = () => {
}); });
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: null }, { id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> }, { id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
]; ];
let hasPagination; let hasPagination;
let pagination; let pagination;
// if (router.query.tab === 'token_transfers') {
// hasPagination = transfersQuery.isPaginationVisible; if (!router.query.tab || router.query.tab === 'token_transfers') {
// pagination = transfersQuery.pagination; hasPagination = transfersQuery.isPaginationVisible;
// } pagination = transfersQuery.pagination;
}
if (router.query.tab === 'holders') { if (router.query.tab === 'holders') {
hasPagination = holdersQuery.isPaginationVisible; hasPagination = holdersQuery.isPaginationVisible;
pagination = holdersQuery.pagination; pagination = holdersQuery.pagination;
......
import { Box, Icon, Link } from '@chakra-ui/react'; import { Box, Icon, Link, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg'; import nftPlaceholder from 'icons/nft_shield.svg';
...@@ -8,11 +8,20 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -8,11 +8,20 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { interface Props {
hash: string; hash: string;
id: string; id: string;
className?: string;
} }
const TokenTransferNft = ({ hash, id }: Props) => { const TokenTransferNft = ({ hash, id, className }: Props) => {
return ( return (
<Link href={ link('token_instance_item', { hash, id }) } overflow="hidden" whiteSpace="nowrap" display="flex" alignItems="center" w="100%"> <Link
href={ link('token_instance_item', { hash, id }) }
overflow="hidden"
whiteSpace="nowrap"
display="flex"
alignItems="center"
w="100%"
className={ className }
>
<Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/> <Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/>
<Box maxW="calc(100% - 34px)"> <Box maxW="calc(100% - 34px)">
<HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/>
...@@ -21,4 +30,4 @@ const TokenTransferNft = ({ hash, id }: Props) => { ...@@ -21,4 +30,4 @@ const TokenTransferNft = ({ hash, id }: Props) => {
); );
}; };
export default React.memo(TokenTransferNft); export default React.memo(chakra(TokenTransferNft));
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import TokenTransfer from './TokenTransfer';
test('erc20 +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={ tokenInfo }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
data: {
items: [ tokenTransferMock.erc20 ],
next_page_params: null,
},
isPaginationVisible: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
pagination: { page: 1 },
}}
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('erc721 +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={{ ...tokenInfo, type: 'ERC-721' }}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
data: {
items: [ tokenTransferMock.erc721 ],
next_page_params: null,
},
isPaginationVisible: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
pagination: { page: 1 },
}}
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('erc1155 +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={{ ...tokenInfo, type: 'ERC-1155' }}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
data: {
items: [ tokenTransferMock.erc1155, tokenTransferMock.erc1155multiple ],
next_page_params: null,
},
isPaginationVisible: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
pagination: { page: 1 },
}}
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable';
type Props = {
token?: TokenInfo;
transfersQuery: UseQueryResult<TokenTransferResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokenTransfer = ({ transfersQuery, token }: Props) => {
const isMobile = useIsMobile();
const router = useRouter();
const { isError, isLoading, data, pagination, isPaginationVisible } = transfersQuery;
const [ newItemsCount, setNewItemsCount ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const handleNewTransfersMessage: SocketMessage.TokenTransfers['handler'] = (payload) => {
setNewItemsCount(payload.token_transfer);
};
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please refresh the page to load new token transfers.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new token transfers. Please refresh the page.');
}, []);
const channel = useSocketChannel({
topic: `tokens:${ router.query.hash?.toString().toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: isLoading || isError || pagination.page !== 1,
});
useSocketMessage({
channel,
event: 'token_transfer',
handler: handleNewTransfersMessage,
});
const content = (() => {
if (isLoading) {
return (
<>
<Hide below="lg">
<SkeletonTable columns={ [ '45%', '15%', '36px', '15%', '25%' ] }
/>
</Hide>
<Show below="lg">
<SkeletonList/>
</Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (!data.items?.length) {
return <Text as="span">There are no token transfers</Text>;
}
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<TokenTransferTable
data={ items }
top={ 80 }
// token transfers query depends on token data
// so if we are here, we definitely have token data
token={ token as TokenInfo }
showSocketInfo={ pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
/>
</Hide>
<Show below="lg">
{ pagination.page === 1 && (
<SocketNewItemsNotice
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="token_transfer"
borderBottomRadius={ 0 }
/>
) }
<TokenTransferList data={ items }/>
</Show>
</>
);
})();
return (
<>
{ isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ content }
</>
);
};
export default React.memo(TokenTransfer);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import TokenTransferListItem from 'ui/token/TokenTransfer/TokenTransferListItem';
interface Props {
data: Array<TokenTransfer>;
}
const TokenTransferList = ({ data }: Props) => {
return (
<Box>
{ data.map((item, index) => (
<TokenTransferListItem
key={ index }
{ ...item }
/>
)) }
</Box>
);
};
export default TokenTransferList;
import { Text, Flex, Tag, Icon, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import eastArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer;
const TokenTransferListItem = ({
token,
total,
tx_hash: txHash,
from,
to,
method,
timestamp,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
return null;
}
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const timeAgo = useTimeAgoIncrement(timestamp, true);
return (
<ListItemMobile rowGap={ 3 } isAnimated>
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Flex>
<Icon
as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color={ iconColor }
/>
<Address width="100%">
<AddressLink
hash={ txHash }
type="transaction"
fontWeight="700"
truncation="constant"
/>
</Address>
</Flex>
{ timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> }
</Flex>
{ method && <Tag colorScheme="gray">{ method }</Tag> }
<Flex w="100%" columnGap={ 3 }>
<Address width="50%">
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<Address width="50%">
<AddressIcon address={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash }/>
</Address>
</Flex>
{ value && (
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text>
<Text variant="secondary">{ value }</Text>
<Text>{ token.symbol }</Text>
</Flex>
) }
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
</ListItemMobile>
);
};
export default React.memo(TokenTransferListItem);
import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableItem';
interface Props {
data: Array<TokenTransfer>;
top: number;
token: TokenInfo;
showSocketInfo: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
}
const TokenTransferTable = ({ data, top, token, showSocketInfo, socketInfoAlert, socketInfoNum }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="40%">Txn hash</Th>
<Th width="164px">Method</Th>
<Th width="148px">From</Th>
<Th width="36px" px={ 0 }/>
<Th width="218px" >To</Th>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && <Th width="20%" isNumeric={ token.type === 'ERC-721' }>Token ID</Th> }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && <Th width="20%" isNumeric>Value { token.symbol }</Th> }
</Tr>
</Thead>
<Tbody>
{ showSocketInfo && (
<Tr>
<Td colSpan={ 10 } p={ 0 }>
<SocketNewItemsNotice
borderRadius={ 0 }
pl="10px"
url={ window.location.href }
alert={ socketInfoAlert }
num={ socketInfoNum }
type="token_transfer"
/>
</Td>
</Tr>
) }
{ data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item }/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(TokenTransferTable);
import { Tr, Td, Tag, Text, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import eastArrowIcon from 'icons/arrows/east.svg';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer
const TokenTransferTableItem = ({
token,
total,
tx_hash: txHash,
from,
to,
method,
timestamp,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
return '-';
}
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
const timeAgo = useTimeAgoIncrement(timestamp, true);
return (
<Tr alignItems="top">
<Td>
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px">
<AddressLink type="transaction" hash={ txHash }/>
</Address>
{ timestamp && <Text color="gray.500" fontWeight="400" mt="10px">{ timeAgo }</Text> }
</Td>
<Td>
{ method ? <Tag colorScheme="gray">{ method }</Tag> : '-' }
</Td>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } truncation="constant"/>
</Address>
</Td>
<Td px={ 0 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 } truncation="constant"/>
</Address>
</Td>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
<Td lineHeight="30px">
{ 'token_id' in total ? (
<TokenTransferNft
hash={ token.address }
id={ total.token_id }
justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' }/>
) : '-'
}
</Td>
) }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Td isNumeric verticalAlign="top" lineHeight="30px">
{ value || '-' }
</Td>
) }
</Tr>
);
};
export default React.memo(TokenTransferTableItem);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment