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