Commit 7b8bcdf5 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #1179 from blockscout/token-id-holders

add token id erc-155 holders
parents 408092b8 4f69c96b
...@@ -2,15 +2,40 @@ import type { TokenHolders } from 'types/api/token'; ...@@ -2,15 +2,40 @@ import type { TokenHolders } from 'types/api/token';
import { withName, withoutName } from 'mocks/address/address'; import { withName, withoutName } from 'mocks/address/address';
export const tokenHolders: TokenHolders = { import { tokenInfoERC1155a, tokenInfoERC20a } from './tokenInfo';
export const tokenHoldersERC20: TokenHolders = {
items: [
{
address: withName,
token: tokenInfoERC20a,
value: '107014805905725000000',
},
{
address: withoutName,
token: tokenInfoERC20a,
value: '207014805905725000000',
},
],
next_page_params: {
value: '50',
items_count: 50,
},
};
export const tokenHoldersERC1155: TokenHolders = {
items: [ items: [
{ {
address: withName, address: withName,
token: tokenInfoERC1155a,
value: '107014805905725000000', value: '107014805905725000000',
token_id: '12345',
}, },
{ {
address: withoutName, address: withoutName,
token: tokenInfoERC1155a,
value: '207014805905725000000', value: '207014805905725000000',
token_id: '12345',
}, },
], ],
next_page_params: { next_page_params: {
......
...@@ -18,7 +18,7 @@ export const tokenCounters: TokenCounters = { ...@@ -18,7 +18,7 @@ export const tokenCounters: TokenCounters = {
transfers_count: '88282281', transfers_count: '88282281',
}; };
export const tokenInfoERC20a: TokenInfo = { export const tokenInfoERC20a: TokenInfo<'ERC-20'> = {
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456', address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
circulating_market_cap: '117268489.23970924', circulating_market_cap: '117268489.23970924',
decimals: '18', decimals: '18',
...@@ -31,7 +31,7 @@ export const tokenInfoERC20a: TokenInfo = { ...@@ -31,7 +31,7 @@ export const tokenInfoERC20a: TokenInfo = {
icon_url: 'https://example.com/token-icon.png', icon_url: 'https://example.com/token-icon.png',
}; };
export const tokenInfoERC20b: TokenInfo = { export const tokenInfoERC20b: TokenInfo<'ERC-20'> = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7', address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
circulating_market_cap: '115060192.36105014', circulating_market_cap: '115060192.36105014',
decimals: '6', decimals: '6',
...@@ -44,7 +44,7 @@ export const tokenInfoERC20b: TokenInfo = { ...@@ -44,7 +44,7 @@ export const tokenInfoERC20b: TokenInfo = {
icon_url: null, icon_url: null,
}; };
export const tokenInfoERC20c: TokenInfo = { export const tokenInfoERC20c: TokenInfo<'ERC-20'> = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7', address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
circulating_market_cap: null, circulating_market_cap: null,
decimals: '18', decimals: '18',
...@@ -57,7 +57,7 @@ export const tokenInfoERC20c: TokenInfo = { ...@@ -57,7 +57,7 @@ export const tokenInfoERC20c: TokenInfo = {
icon_url: null, icon_url: null,
}; };
export const tokenInfoERC20d: TokenInfo = { export const tokenInfoERC20d: TokenInfo<'ERC-20'> = {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195', address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
circulating_market_cap: null, circulating_market_cap: null,
decimals: '18', decimals: '18',
...@@ -70,7 +70,7 @@ export const tokenInfoERC20d: TokenInfo = { ...@@ -70,7 +70,7 @@ export const tokenInfoERC20d: TokenInfo = {
icon_url: null, icon_url: null,
}; };
export const tokenInfoERC20LongSymbol: TokenInfo = { export const tokenInfoERC20LongSymbol: TokenInfo<'ERC-20'> = {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195', address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
circulating_market_cap: '112855875.75888918', circulating_market_cap: '112855875.75888918',
decimals: '18', decimals: '18',
...@@ -83,7 +83,7 @@ export const tokenInfoERC20LongSymbol: TokenInfo = { ...@@ -83,7 +83,7 @@ export const tokenInfoERC20LongSymbol: TokenInfo = {
icon_url: null, icon_url: null,
}; };
export const tokenInfoERC721a: TokenInfo = { export const tokenInfoERC721a: TokenInfo<'ERC-721'> = {
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0', address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
circulating_market_cap: null, circulating_market_cap: null,
decimals: null, decimals: null,
...@@ -96,7 +96,7 @@ export const tokenInfoERC721a: TokenInfo = { ...@@ -96,7 +96,7 @@ export const tokenInfoERC721a: TokenInfo = {
icon_url: null, icon_url: null,
}; };
export const tokenInfoERC721b: TokenInfo = { export const tokenInfoERC721b: TokenInfo<'ERC-721'> = {
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F', address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
circulating_market_cap: null, circulating_market_cap: null,
decimals: null, decimals: null,
...@@ -109,7 +109,7 @@ export const tokenInfoERC721b: TokenInfo = { ...@@ -109,7 +109,7 @@ export const tokenInfoERC721b: TokenInfo = {
icon_url: null, icon_url: null,
}; };
export const tokenInfoERC721c: TokenInfo = { export const tokenInfoERC721c: TokenInfo<'ERC-721'> = {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992', address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
circulating_market_cap: null, circulating_market_cap: null,
decimals: null, decimals: null,
...@@ -122,7 +122,7 @@ export const tokenInfoERC721c: TokenInfo = { ...@@ -122,7 +122,7 @@ export const tokenInfoERC721c: TokenInfo = {
icon_url: null, icon_url: null,
}; };
export const tokenInfoERC721LongSymbol: TokenInfo = { export const tokenInfoERC721LongSymbol: TokenInfo<'ERC-721'> = {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992', address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
circulating_market_cap: null, circulating_market_cap: null,
decimals: null, decimals: null,
...@@ -135,7 +135,7 @@ export const tokenInfoERC721LongSymbol: TokenInfo = { ...@@ -135,7 +135,7 @@ export const tokenInfoERC721LongSymbol: TokenInfo = {
icon_url: null, icon_url: null,
}; };
export const tokenInfoERC1155a: TokenInfo = { export const tokenInfoERC1155a: TokenInfo<'ERC-1155'> = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e', address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
circulating_market_cap: null, circulating_market_cap: null,
decimals: null, decimals: null,
...@@ -148,7 +148,7 @@ export const tokenInfoERC1155a: TokenInfo = { ...@@ -148,7 +148,7 @@ export const tokenInfoERC1155a: TokenInfo = {
icon_url: null, icon_url: null,
}; };
export const tokenInfoERC1155b: TokenInfo = { export const tokenInfoERC1155b: TokenInfo<'ERC-1155'> = {
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc', address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
circulating_market_cap: null, circulating_market_cap: null,
decimals: null, decimals: null,
...@@ -161,7 +161,7 @@ export const tokenInfoERC1155b: TokenInfo = { ...@@ -161,7 +161,7 @@ export const tokenInfoERC1155b: TokenInfo = {
icon_url: null, icon_url: null,
}; };
export const tokenInfoERC1155WithoutName: TokenInfo = { export const tokenInfoERC1155WithoutName: TokenInfo<'ERC-1155'> = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e', address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
circulating_market_cap: null, circulating_market_cap: null,
decimals: null, decimals: null,
......
...@@ -36,8 +36,16 @@ export const TOKEN_COUNTERS: TokenCounters = { ...@@ -36,8 +36,16 @@ export const TOKEN_COUNTERS: TokenCounters = {
transfers_count: '123456', transfers_count: '123456',
}; };
export const TOKEN_HOLDER: TokenHolder = { export const TOKEN_HOLDER_ERC_20: TokenHolder = {
address: ADDRESS_PARAMS, address: ADDRESS_PARAMS,
token: TOKEN_INFO_ERC_20,
value: '1021378038331138520',
};
export const TOKEN_HOLDER_ERC_1155: TokenHolder = {
address: ADDRESS_PARAMS,
token: TOKEN_INFO_ERC_1155,
token_id: '12345',
value: '1021378038331138520', value: '1021378038331138520',
}; };
......
...@@ -26,11 +26,22 @@ export interface TokenHolders { ...@@ -26,11 +26,22 @@ export interface TokenHolders {
next_page_params: TokenHoldersPagination | null; next_page_params: TokenHoldersPagination | null;
} }
export type TokenHolder = { export type TokenHolder = TokenHolderERC20ERC721 | TokenHolderERC1155;
export type TokenHolderBase = {
address: AddressParam; address: AddressParam;
value: string; value: string;
} }
export type TokenHolderERC20ERC721 = TokenHolderBase & {
token: TokenInfo<'ERC-20'> | TokenInfo<'ERC-721'>;
}
export type TokenHolderERC1155 = TokenHolderBase & {
token: TokenInfo<'ERC-1155'>;
token_id: string;
}
export type TokenHoldersPagination = { export type TokenHoldersPagination = {
items_count: number; items_count: number;
value: string; value: string;
......
...@@ -157,7 +157,8 @@ const TokenPageContent = () => { ...@@ -157,7 +157,8 @@ const TokenPageContent = () => {
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(hashString && tab === 'holders' && hasData), enabled: Boolean(hashString && tab === 'holders' && hasData),
placeholderData: generateListStub<'token_holders'>(tokenStubs.TOKEN_HOLDER, 50, { next_page_params: null }), placeholderData: generateListStub<'token_holders'>(
tokenQuery.data?.type === 'ERC-1155' ? tokenStubs.TOKEN_HOLDER_ERC_1155 : tokenStubs.TOKEN_HOLDER_ERC_20, 50, { next_page_params: null }),
}, },
}); });
......
...@@ -71,7 +71,8 @@ const TokenInstanceContent = () => { ...@@ -71,7 +71,8 @@ const TokenInstanceContent = () => {
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(hash && tab === 'holders' && shouldFetchHolders), enabled: Boolean(hash && tab === 'holders' && shouldFetchHolders),
placeholderData: generateListStub<'token_instance_holders'>(tokenStubs.TOKEN_HOLDER, 10, { next_page_params: null }), placeholderData: generateListStub<'token_instance_holders'>(
tokenInstanceQuery.data?.token.type === 'ERC-1155' ? tokenStubs.TOKEN_HOLDER_ERC_1155 : tokenStubs.TOKEN_HOLDER_ERC_20, 10, { next_page_params: null }),
}, },
}); });
......
import { test, expect, devices } from '@playwright/experimental-ct-react'; import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { tokenHolders } from 'mocks/tokens/tokenHolders'; import { tokenHoldersERC20, tokenHoldersERC1155 } from 'mocks/tokens/tokenHolders';
import { tokenInfo } from 'mocks/tokens/tokenInfo'; import { tokenInfo, tokenInfoERC1155a } from 'mocks/tokens/tokenInfo';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import TokenHoldersList from './TokenHoldersList'; import TokenHoldersList from './TokenHoldersList';
test.use({ viewport: devices['iPhone 13 Pro'].viewport }); test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ mount }) => { test('base view without IDs', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TokenHoldersList data={ tokenHolders.items } token={ tokenInfo }/> <TokenHoldersList data={ tokenHoldersERC20.items } token={ tokenInfo }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('base view with IDs', async({ mount }) => {
const component = await mount(
<TestApp>
<TokenHoldersList data={ tokenHoldersERC1155.items } token={ tokenInfoERC1155a }/>
</TestApp>, </TestApp>,
); );
......
import { Box, Flex, Skeleton } from '@chakra-ui/react'; import { Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/token'; import type { TokenHolder, TokenInfo } from 'types/api/token';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
interface Props { interface Props {
...@@ -18,30 +18,50 @@ const TokenHoldersListItem = ({ holder, token, isLoading }: Props) => { ...@@ -18,30 +18,50 @@ const TokenHoldersListItem = ({ holder, token, isLoading }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).dp(6).toFormat(); const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).dp(6).toFormat();
return ( return (
<ListItemMobile rowGap={ 3 }> <ListItemMobileGrid.Container>
<AddressEntity <ListItemMobileGrid.Label isLoading={ isLoading }>Address</ListItemMobileGrid.Label>
address={ holder.address } <ListItemMobileGrid.Value>
isLoading={ isLoading } <AddressEntity
fontWeight="700" address={ holder.address }
maxW="100%" isLoading={ isLoading }
/> fontWeight="700"
<Flex justifyContent="space-between" alignItems="center" width="100%"> maxW="100%"
<Skeleton isLoaded={ !isLoading } display="inline-block" width="100%"> />
<Box as="span" wordBreak="break-word" mr={ 6 }> </ListItemMobileGrid.Value>
{ quantity }
</Box> { token.type === 'ERC-1155' && 'token_id' in holder && (
{ token.total_supply && ( <>
<ListItemMobileGrid.Label isLoading={ isLoading }>ID#</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ holder.token_id }
</Skeleton>
</ListItemMobileGrid.Value>
</>
) }
<ListItemMobileGrid.Label isLoading={ isLoading }>Quantity</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ quantity }
</Skeleton>
</ListItemMobileGrid.Value>
{ token.total_supply && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Percentage</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Utilization <Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() } value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green" colorScheme="green"
isLoading={ isLoading } isLoading={ isLoading }
display="inline-flex" display="inline-flex"
float="right"
/> />
) } </ListItemMobileGrid.Value>
</Skeleton> </>
</Flex> ) }
</ListItemMobile>
</ListItemMobileGrid.Container>
); );
}; };
......
...@@ -2,17 +2,28 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,17 +2,28 @@ import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { tokenHolders } from 'mocks/tokens/tokenHolders'; import { tokenHoldersERC20, tokenHoldersERC1155 } from 'mocks/tokens/tokenHolders';
import { tokenInfo } from 'mocks/tokens/tokenInfo'; import { tokenInfo, tokenInfoERC1155a } from 'mocks/tokens/tokenInfo';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import TokenHoldersTable from './TokenHoldersTable'; import TokenHoldersTable from './TokenHoldersTable';
test('base view', async({ mount }) => { test('base view without IDs', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Box h="128px"/> <Box h="128px"/>
<TokenHoldersTable data={ tokenHolders.items } token={ tokenInfo } top={ 80 }/> <TokenHoldersTable data={ tokenHoldersERC20.items } token={ tokenInfo } top={ 80 }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('base view with IDs', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h="128px"/>
<TokenHoldersTable data={ tokenHoldersERC1155.items } token={ tokenInfoERC1155a } top={ 80 }/>
</TestApp>, </TestApp>,
); );
......
...@@ -19,6 +19,7 @@ const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => { ...@@ -19,6 +19,7 @@ const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => {
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>Holder</Th> <Th>Holder</Th>
{ token.type === 'ERC-1155' && <Th>ID#</Th> }
<Th isNumeric>Quantity</Th> <Th isNumeric>Quantity</Th>
{ token.total_supply && <Th isNumeric width="175px">Percentage</Th> } { token.total_supply && <Th isNumeric width="175px">Percentage</Th> }
</Tr> </Tr>
......
...@@ -26,6 +26,13 @@ const TokenTransferTableItem = ({ holder, token, isLoading }: Props) => { ...@@ -26,6 +26,13 @@ const TokenTransferTableItem = ({ holder, token, isLoading }: Props) => {
fontWeight="700" fontWeight="700"
/> />
</Td> </Td>
{ token.type === 'ERC-1155' && 'token_id' in holder && (
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ 'token_id' in holder && holder.token_id }
</Skeleton>
</Td>
) }
<Td verticalAlign="middle" isNumeric> <Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block" wordBreak="break-word"> <Skeleton isLoaded={ !isLoading } display="inline-block" wordBreak="break-word">
{ quantity } { quantity }
......
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