Commit 9f333f82 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #591 from blockscout/token-instance-transfers

token instance transfers
parents 428ec75e c1504241
......@@ -23,8 +23,15 @@ import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders, TokenInventoryResponse, TokenInstance, TokenInstanceTransfersCount } from 'types/api/token';
import type { TokensResponse, TokensFilters } from 'types/api/tokens';
import type {
TokenCounters,
TokenInfo,
TokenHolders,
TokenInventoryResponse,
TokenInstance,
TokenInstanceTransfersCount,
} from 'types/api/token';
import type { TokensResponse, TokensFilters, TokenInstanceTransferResponse } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
......@@ -293,6 +300,12 @@ export const RESOURCES = {
path: '/api/v2/tokens/:hash/instances/:id/transfers-count',
pathParams: [ 'hash' as const, 'id' as const ],
},
token_instance_transfers: {
path: '/api/v2/tokens/:hash/instances/:id/transfers',
pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
filterFields: [],
},
// HOMEPAGE
homepage_stats: {
......@@ -376,7 +389,8 @@ 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_transfers' | 'token_holders' | 'token_inventory' | 'tokens';
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
'token_instance_transfers';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -425,6 +439,7 @@ Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders :
Q extends 'token_instance' ? TokenInstance :
Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount :
Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult :
......
......@@ -112,7 +112,7 @@ export const erc1155: TokenTransfer = {
exchange_rate: null,
holders: '1',
name: null,
symbol: null,
symbol: 'MY_SYMBOL_IS_VERY_LONG',
type: 'ERC-1155',
total_supply: '0',
},
......
......@@ -46,7 +46,6 @@ export type TokenTransferPagination = {
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
}
export interface TokenTransferResponse {
......
import type { TokenInfo, TokenType } from './token';
import type { TokenTransfer } from './tokenTransfer';
export type TokensResponse = {
items: Array<TokenInfo>;
......@@ -10,3 +11,15 @@ export type TokensResponse = {
}
export type TokensFilters = { filter: string; type: Array<TokenType> | undefined };
export interface TokenInstanceTransferResponse {
items: Array<TokenTransfer>;
next_page_params: TokenInstanceTransferPagination | null;
}
export interface TokenInstanceTransferPagination {
block_number: number;
index: number;
items_count: number;
token_id: string;
}
......@@ -84,7 +84,7 @@ const TokenPageContent = () => {
});
const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
];
......
import { Box, Icon, Link, chakra } from '@chakra-ui/react';
import { Box, Icon, chakra } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
hash: string;
id: string;
className?: string;
isDisabled?: boolean;
}
const TokenTransferNft = ({ hash, id, className }: Props) => {
const TokenTransferNft = ({ hash, id, className, isDisabled }: Props) => {
const Component = isDisabled ? Box : LinkInternal;
return (
<Link
href={ route({ pathname: '/token/[hash]/instance/[id]', query: { hash, id } }) }
<Component
href={ isDisabled ? undefined : route({ pathname: '/token/[hash]/instance/[id]', query: { hash, id } }) }
overflow="hidden"
whiteSpace="nowrap"
display="flex"
......@@ -26,7 +30,7 @@ const TokenTransferNft = ({ hash, id, className }: Props) => {
<Box maxW="calc(100% - 34px)">
<HashStringShortenDynamic hash={ id } fontWeight={ 500 }/>
</Box>
</Link>
</Component>
);
};
......
......@@ -2,7 +2,6 @@ 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';
......@@ -13,7 +12,6 @@ test('erc20 +@mobile', async({ mount }) => {
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={ tokenInfo }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
......@@ -38,7 +36,6 @@ test('erc721 +@mobile', async({ 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={{
......@@ -63,7 +60,6 @@ test('erc1155 +@mobile', async({ mount }) => {
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={{ ...tokenInfo, type: 'ERC-1155', symbol: tokenTransferMock.erc1155multiple.token.symbol }}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
......
......@@ -4,7 +4,6 @@ import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/token';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
......@@ -23,14 +22,14 @@ 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;
};
tokenId?: string;
}
const TokenTransfer = ({ transfersQuery, token }: Props) => {
const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
const isMobile = useIsMobile();
const router = useRouter();
const { isError, isLoading, data, pagination, isPaginationVisible } = transfersQuery;
......@@ -92,12 +91,10 @@ const TokenTransfer = ({ transfersQuery, token }: Props) => {
<TokenTransferTable
data={ items }
top={ isPaginationVisible ? 80 : 0 }
// 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 }
tokenId={ tokenId }
/>
</Hide>
<Show below="lg" ssr={ false }>
......@@ -110,7 +107,7 @@ const TokenTransfer = ({ transfersQuery, token }: Props) => {
borderBottomRadius={ 0 }
/>
) }
<TokenTransferList data={ items }/>
<TokenTransferList data={ items } tokenId={ tokenId }/>
</Show>
</>
);
......
......@@ -7,15 +7,17 @@ import TokenTransferListItem from 'ui/token/TokenTransfer/TokenTransferListItem'
interface Props {
data: Array<TokenTransfer>;
tokenId?: string;
}
const TokenTransferList = ({ data }: Props) => {
const TokenTransferList = ({ data, tokenId }: Props) => {
return (
<Box>
{ data.map((item, index) => (
<TokenTransferListItem
key={ index }
{ ...item }
tokenId={ tokenId }
/>
)) }
</Box>
......
......@@ -14,7 +14,7 @@ import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer;
type Props = TokenTransfer & {tokenId?: string};
const TokenTransferListItem = ({
token,
......@@ -24,6 +24,7 @@ const TokenTransferListItem = ({
to,
method,
timestamp,
tokenId,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
......@@ -78,7 +79,7 @@ const TokenTransferListItem = ({
</Flex>
) }
{ 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') &&
<TokenTransferNft hash={ token.address } id={ total.token_id }/> }
<TokenTransferNft hash={ token.address } id={ total.token_id } isDisabled={ Boolean(tokenId && tokenId === total.token_id) }/> }
</ListItemMobile>
);
};
......
import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
......@@ -12,24 +11,27 @@ import TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableIte
interface Props {
data: Array<TokenTransfer>;
top: number;
token: TokenInfo;
showSocketInfo: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
tokenId?: string;
}
const TokenTransferTable = ({ data, top, token, showSocketInfo, socketInfoAlert, socketInfoNum }: Props) => {
const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socketInfoNum, tokenId }: Props) => {
const tokenType = data[0].token.type;
const tokenSymbol = data[0].token.symbol;
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width={ token.type === 'ERC-1155' ? '60%' : '80%' }>Txn hash</Th>
<Th width="60%">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 { trimTokenSymbol(token.symbol) }</Th> }
{ (tokenType === 'ERC-721' || tokenType === 'ERC-1155') && <Th width="20%" isNumeric={ tokenType === 'ERC-721' }>Token ID</Th> }
{ (tokenType === 'ERC-20' || tokenType === 'ERC-1155') && <Th width="20%" isNumeric whiteSpace="nowrap">Value { trimTokenSymbol(tokenSymbol) }</Th> }
</Tr>
</Thead>
<Tbody>
......@@ -48,7 +50,7 @@ const TokenTransferTable = ({ data, top, token, showSocketInfo, socketInfoAlert,
</Tr>
) }
{ data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item }/>
<TokenTransferTableItem key={ index } { ...item } tokenId={ tokenId }/>
)) }
</Tbody>
</Table>
......
......@@ -11,7 +11,7 @@ import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer
type Props = TokenTransfer & { tokenId?: string }
const TokenTransferTableItem = ({
token,
......@@ -21,6 +21,7 @@ const TokenTransferTableItem = ({
to,
method,
timestamp,
tokenId,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
......@@ -84,7 +85,9 @@ const TokenTransferTableItem = ({
<TokenTransferNft
hash={ token.address }
id={ total.token_id }
justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' }/>
justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' }
isDisabled={ Boolean(tokenId && tokenId === total.token_id) }
/>
) : '-'
}
</Td>
......
......@@ -7,14 +7,18 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import AdBanner from 'ui/shared/ad/AdBanner';
import TextAd from 'ui/shared/ad/TextAd';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenLogo from 'ui/shared/TokenLogo';
import TokenInstanceDetails from 'ui/tokenInstance/TokenInstanceDetails';
import TokenInstanceSkeleton from 'ui/tokenInstance/TokenInstanceSkeleton';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
import TokenInstanceDetails from './TokenInstanceDetails';
import TokenInstanceSkeleton from './TokenInstanceSkeleton';
export type TokenTabs = 'token_transfers' | 'holders'
......@@ -25,6 +29,7 @@ const TokenInstanceContent = () => {
const hash = router.query.hash?.toString();
const id = router.query.id?.toString();
const tab = router.query.tab?.toString();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance');
......@@ -35,9 +40,19 @@ const TokenInstanceContent = () => {
queryOptions: { enabled: Boolean(hash && id) },
});
const transfersQuery = useQueryWithPages({
resourceName: 'token_instance_transfers',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && (!tab || tab === 'token_transfers') && tokenInstanceQuery.data),
},
});
const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <span>Token transfers</span> },
{ id: 'holders', title: 'Holders', component: <span>Holders</span> },
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> },
// there is no api for this tab yet
// { id: 'holders', title: 'Holders', component: <span>Holders</span> },
{ id: 'metadata', title: 'Metadata', component: <span>Metadata</span> },
];
......@@ -81,6 +96,8 @@ const TokenInstanceContent = () => {
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && transfersQuery.isPaginationVisible ? <Pagination { ...transfersQuery.pagination }/> : null }
stickyEnabled={ !isMobile }
/>
</>
);
......
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