Commit 4d737288 authored by isstuev's avatar isstuev

token transfers

parent 1c9cd34b
......@@ -208,13 +208,10 @@ export const RESOURCES = {
paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [],
},
// TOKEN
token: {
path: '/api/v2/tokens/:hash',
},
token_counters: {
path: '/api/v2/tokens/:hash/counters',
token_transfers: {
path: '/api/v2/tokens/:hash/transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
// HOMEPAGE
......@@ -286,7 +283,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' |
'address_logs' | 'address_tokens' |
'token_holders';
'token_transfers' | 'token_holders';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -331,6 +328,7 @@ Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters :
Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders :
Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract :
......@@ -346,6 +344,7 @@ export type PaginationFilters<Q extends PaginatedResources> =
Q extends 'blocks' ? BlockFilters :
Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter :
......
......@@ -2,6 +2,7 @@ import { Skeleton, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
......@@ -15,6 +16,7 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
export type TokenTabs = 'token_transfers' | 'holders'
......@@ -29,13 +31,14 @@ const TokenPageContent = () => {
queryOptions: { enabled: Boolean(router.query.hash) },
});
// const transfersQuery = useQueryWithPages({
// resourceName: 'token_transfers',
// pathParams: { hash: router.query.hash?.toString() },
// options: {
// enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data),
// },
// });
const transfersQuery = useQueryWithPages({
resourceName: 'token_transfers',
pathParams: { hash: router.query.hash?.toString() },
scrollRef,
options: {
enabled: Boolean(router.query.hash && (!router.query.tab || router.query.tab === 'token_transfers') && tokenQuery.data),
},
});
const holdersQuery = useQueryWithPages({
resourceName: 'token_holders',
......@@ -47,16 +50,18 @@ const TokenPageContent = () => {
});
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 as TokenInfo }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
];
let hasPagination;
let pagination;
// if (router.query.tab === 'token_transfers') {
// hasPagination = transfersQuery.isPaginationVisible;
// pagination = transfersQuery.pagination;
// }
if (!router.query.tab || router.query.tab === 'token_transfers') {
hasPagination = transfersQuery.isPaginationVisible;
pagination = transfersQuery.pagination;
}
if (router.query.tab === 'holders') {
hasPagination = holdersQuery.isPaginationVisible;
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 nftPlaceholder from 'icons/nft_shield.svg';
......@@ -8,11 +8,20 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
hash: string;
id: string;
className?: string;
}
const TokenTransferNft = ({ hash, id }: Props) => {
const TokenTransferNft = ({ hash, id, className }: Props) => {
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"/>
<Box maxW="calc(100% - 34px)">
<HashStringShortenDynamic hash={ id } fontWeight={ 500 }/>
......@@ -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,
}}
/>
</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,
}}
/>
</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,
}}
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import useIsMobile from 'lib/hooks/useIsMobile';
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 { 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 { isError, isLoading, data, pagination, isPaginationVisible } = transfersQuery;
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={ token }/>
</Hide>
<Show below="lg">
<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 { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer;
const TokenTransferListItem = ({
token,
total,
tx_hash: txHash,
from,
to,
type,
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>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</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 } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableItem';
interface Props {
data: Array<TokenTransfer>;
top: number;
token: TokenInfo;
}
const TokenTransferTable = ({ data, top, token }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="40%">Txn hash</Th>
{ /* replace with method??? */ }
<Th width="164px">Type</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>
{ 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 { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer
const TokenTransferTableItem = ({
token,
total,
tx_hash: txHash,
from,
to,
type,
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>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</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