Skip to content

Commit

Permalink
Merge pull request #1179 from blockscout/token-id-holders
Browse files Browse the repository at this point in the history
add token id erc-155 holders
  • Loading branch information
isstuev authored Sep 14, 2023
2 parents 408092b + 4f69c96 commit 7b8bcdf
Show file tree
Hide file tree
Showing 16 changed files with 140 additions and 45 deletions.
27 changes: 26 additions & 1 deletion mocks/tokens/tokenHolders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
24 changes: 12 additions & 12 deletions mocks/tokens/tokenInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion stubs/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand Down
13 changes: 12 additions & 1 deletion types/api/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion ui/pages/Token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
},
});

Expand Down
3 changes: 2 additions & 1 deletion ui/pages/TokenInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
},
});

Expand Down
18 changes: 14 additions & 4 deletions ui/token/TokenHolders/TokenHoldersList.pw.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
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>,
);

Expand Down
60 changes: 40 additions & 20 deletions ui/token/TokenHolders/TokenHoldersListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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 {
Expand All @@ -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 }>
<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 }>
{ quantity }
</Box>
{ token.total_supply && (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>Address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity
address={ holder.address }
isLoading={ isLoading }
fontWeight="700"
maxW="100%"
/>
</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 }
</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"
/>
) }
</Skeleton>
</Flex>
</ListItemMobile>
</ListItemMobileGrid.Value>
</>
) }

</ListItemMobileGrid.Container>
);
};

Expand Down
19 changes: 15 additions & 4 deletions ui/token/TokenHolders/TokenHoldersTable.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
);

Expand Down
1 change: 1 addition & 0 deletions ui/token/TokenHolders/TokenHoldersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
7 changes: 7 additions & 0 deletions ui/token/TokenHolders/TokenHoldersTableItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 7b8bcdf

Please sign in to comment.