Commit c3953467 authored by isstuev's avatar isstuev

token holders

parent a8fde650
......@@ -21,7 +21,7 @@ import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { Stats, Charts, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo } from 'types/api/tokenInfo';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
......@@ -176,13 +176,10 @@ export const RESOURCES = {
token_counters: {
path: '/api/v2/tokens/:hash/counters',
},
// TOKEN
token: {
path: '/api/v2/tokens/:hash',
},
token_counters: {
path: '/api/v2/tokens/:hash/counters',
token_holders: {
path: '/api/v2/tokens/:hash/holders',
paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [],
},
// HOMEPAGE
......@@ -240,7 +237,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'address_logs';
'address_logs' |
'token_holders';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -283,6 +281,7 @@ Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters :
Q extends 'token_holders' ? TokenHolders :
Q extends 'config_json_rpc' ? JsonRpcUrlResponse :
Q extends 'contract' ? SmartContract :
never;
......
......@@ -4,7 +4,7 @@ import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { animateScroll, scroller } from 'react-scroll';
import { animateScroll } from 'react-scroll';
import type { PaginatedResources, PaginatedResponse, PaginationFilters } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources';
......@@ -16,7 +16,7 @@ interface Params<Resource extends PaginatedResources> {
options?: UseApiQueryParams<Resource>['queryOptions'];
pathParams?: UseApiQueryParams<Resource>['pathParams'];
filters?: PaginationFilters<Resource>;
scroll?: { elem: string; offset: number };
scrollRef?: React.RefObject<HTMLDivElement>;
}
export default function useQueryWithPages<Resource extends PaginatedResources>({
......@@ -24,7 +24,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
filters,
options,
pathParams,
scroll,
scrollRef,
}: Params<Resource>) {
const resource = RESOURCES[resourceName];
const queryClient = useQueryClient();
......@@ -46,8 +46,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryParams = { ...filters, ...pageParams[page] };
const scrollToTop = useCallback(() => {
scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 });
}, [ scroll ]);
scrollRef?.current ? scrollRef.current.scrollIntoView(true) : animateScroll.scrollToTop({ duration: 0 });
}, [ scrollRef ]);
const queryResult = useApiQuery(resourceName, {
pathParams,
......@@ -77,10 +77,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
nextPageQuery.page = String(page + 1);
setHasPagination(true);
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
scrollToTop();
});
scrollToTop();
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true });
}, [ data?.next_page_params, page, router, scrollToTop ]);
const onPrevPageClick = useCallback(() => {
......@@ -96,9 +95,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
nextPageQuery.page = String(page - 1);
}
scrollToTop();
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
scrollToTop();
setPage(prev => prev - 1);
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
});
......@@ -108,9 +107,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop();
router.push({ pathname: router.pathname, query: omit(router.query, resource.paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop();
setPage(1);
setPageParams({});
canGoBackwards.current = true;
......@@ -133,6 +132,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}
});
}
scrollToTop();
router.push(
{
pathname: router.pathname,
......@@ -143,7 +144,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
).then(() => {
setPage(1);
setPageParams({});
scrollToTop();
});
}, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]);
......
import type { TokenHolders } from 'types/api/tokenInfo';
import { withName, withoutName } from 'mocks/address/address';
export const tokenHolders: TokenHolders = {
items: [
{
address: withName,
value: '107014805905725000000',
},
{
address: withoutName,
value: '207014805905725000000',
},
],
next_page_params: {
value: '50',
items_count: 50,
},
};
import type { AddressParam } from './addressParams';
export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155';
export interface TokenInfo {
......@@ -17,3 +19,18 @@ export interface TokenCounters {
}
export type TokenInfoGeneric<Type extends TokenType> = Omit<TokenInfo, 'type'> & { type: Type };
export interface TokenHolders {
items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination;
}
export type TokenHolder = {
address: AddressParam;
value: string;
}
export type TokenHoldersPagination = {
items_count: number;
value: string;
}
......@@ -2,7 +2,6 @@ import { Text, Show, Hide } from '@chakra-ui/react';
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
......@@ -21,12 +20,9 @@ import Pagination from 'ui/shared/Pagination';
import AddressTxsFilter from './AddressTxsFilter';
import AddressIntTxsList from './internals/AddressIntTxsList';
const SCROLL_ELEM = 'address-internas-txs';
const SCROLL_OFFSET = -100;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressInternalTxs = () => {
const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
......@@ -38,7 +34,7 @@ const AddressInternalTxs = () => {
resourceName: 'address_internal_txs',
pathParams: { id: queryIdStr },
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
scrollRef,
});
const handleFilterChange = React.useCallback((val: string | Array<string>) => {
......@@ -83,7 +79,7 @@ const AddressInternalTxs = () => {
}
return (
<Element name={ SCROLL_ELEM }>
<>
<ActionBar mt={ -6 }>
<AddressTxsFilter
defaultFilter={ filterValue }
......@@ -93,7 +89,7 @@ const AddressInternalTxs = () => {
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar>
{ content }
</Element>
</>
);
};
......
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
......@@ -10,19 +9,14 @@ import LogItem from 'ui/shared/logs/LogItem';
import LogSkeleton from 'ui/shared/logs/LogSkeleton';
import Pagination from 'ui/shared/Pagination';
const SCROLL_PARAMS = {
elem: 'address-logs',
offset: -100,
};
const AddressLogs = () => {
const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const addressHash = String(router.query?.id);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_logs',
pathParams: { id: addressHash },
scroll: SCROLL_PARAMS,
scrollRef,
});
if (isError) {
......@@ -50,10 +44,10 @@ const AddressLogs = () => {
}
return (
<Element name={ SCROLL_PARAMS.elem }>
<>
{ bar }
{ data.items.map((item, index) => <LogItem key={ index } { ...item } type="address"/>) }
</Element>
</>
);
};
......
......@@ -3,7 +3,7 @@ import React from 'react';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
const AddressTokenTransfers = () => {
const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const hash = router.query.id;
......@@ -13,6 +13,7 @@ const AddressTokenTransfers = () => {
pathParams={{ id: hash?.toString() }}
baseAddress={ typeof hash === 'string' ? hash : undefined }
enableTimeIncrement
scrollRef={ scrollRef }
/>
);
};
......
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
......@@ -17,10 +16,7 @@ import AddressTxsFilter from './AddressTxsFilter';
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const SCROLL_ELEM = 'address-txs';
const SCROLL_OFFSET = -100;
const AddressTxs = () => {
const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const isMobile = useIsMobile();
......@@ -31,7 +27,7 @@ const AddressTxs = () => {
resourceName: 'address_txs',
pathParams: { id: castArray(router.query.id)[0] },
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
scrollRef,
});
const handleFilterChange = React.useCallback((val: string | Array<string>) => {
......@@ -50,7 +46,7 @@ const AddressTxs = () => {
);
return (
<Element name={ SCROLL_ELEM }>
<>
{ !isMobile && (
<ActionBar mt={ -6 }>
{ filter }
......@@ -64,7 +60,7 @@ const AddressTxs = () => {
currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined }
enableTimeIncrement
/>
</Element>
</>
);
};
......
import { Flex, Skeleton, Tag } from '@chakra-ui/react';
import { Flex, Skeleton, Tag, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -33,6 +33,8 @@ const CONTRACT_TABS = [
const AddressPageContent = () => {
const router = useRouter();
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const addressQuery = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
......@@ -48,15 +50,15 @@ const AddressPageContent = () => {
const tabs: Array<RoutedTab> = React.useMemo(() => {
return [
{ id: 'txs', title: 'Transactions', component: <AddressTxs/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> },
{ id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> },
{ id: 'tokens', title: 'Tokens', component: null },
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs/> },
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> },
// temporary show this tab in all address
// later api will return info about available tabs
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated/> },
isContract ? { id: 'logs', title: 'Logs', component: <AddressLogs/> } : undefined,
isContract ? { id: 'logs', title: 'Logs', component: <AddressLogs scrollRef={ tabsScrollRef }/> } : undefined,
isContract ? {
id: 'contract',
title: 'Contract',
......@@ -80,6 +82,8 @@ const AddressPageContent = () => {
/>
) }
<AddressDetails addressQuery={ addressQuery }/>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> }
</Page>
);
......
import { Skeleton } from '@chakra-ui/react';
import { Skeleton, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
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';
export type TokenTabs = 'token_transfers' | 'holders'
const TokenPageContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenQuery = useApiQuery('token', {
pathParams: { hash: router.query.hash?.toString() },
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 holdersQuery = useQueryWithPages({
resourceName: 'token_holders',
pathParams: { hash: router.query.hash?.toString() },
scrollRef,
options: {
enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data),
},
});
const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: null },
{ id: 'holders', title: 'Holders', component: null },
{ 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 === 'holders') {
hasPagination = holdersQuery.isPaginationVisible;
pagination = holdersQuery.pagination;
}
return (
<Page>
{ tokenQuery.isLoading ?
......@@ -34,7 +69,16 @@ const TokenPageContent = () => {
<PageTitle text={ `${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token` }/> }
<TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/>
<Element name="token-tabs"><RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/></Element>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
stickyEnabled={ !isMobile }
/>
</Page>
);
};
......
import { Hide, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
......@@ -28,9 +27,6 @@ import { TOKEN_TYPE } from './helpers';
const TOKEN_TYPES = TOKEN_TYPE.map(i => i.id);
const SCROLL_ELEM = 'token-transfers';
const SCROLL_OFFSET = -100;
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
......@@ -43,6 +39,7 @@ interface Props<Resource extends 'tx_token_transfers' | 'address_token_transfers
txHash?: string;
enableTimeIncrement?: boolean;
pathParams?: UseApiQueryParams<Resource>['pathParams'];
scrollRef?: React.RefObject<HTMLDivElement>;
}
type State = {
......@@ -58,6 +55,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
showTxInfo = true,
enableTimeIncrement,
pathParams,
scrollRef,
}: Props<Resource>) => {
const router = useRouter();
const [ filters, setFilters ] = React.useState<State>(
......@@ -69,7 +67,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
pathParams,
options: { enabled: !isDisabled },
filters: filters as PaginationFilters<Resource>,
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
scrollRef,
});
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
......@@ -129,7 +127,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
})();
return (
<Element name={ SCROLL_ELEM }>
<>
{ !isActionBarHidden && (
<ActionBar mt={ -6 }>
<TokenTransferFilter
......@@ -144,7 +142,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
</ActionBar>
) }
{ content }
</Element>
</>
);
};
......
import { Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { TokenHolders, TokenInfo } from 'types/api/tokenInfo';
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 TokenHoldersList from './TokenHoldersList';
import TokenHoldersTable from './TokenHoldersTable';
type Props = {
tokenQuery: UseQueryResult<TokenInfo>;
holdersQuery: UseQueryResult<TokenHolders> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
const isMobile = useIsMobile();
if (holdersQuery.isError || tokenQuery.isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && holdersQuery.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...holdersQuery.pagination }/>
</ActionBar>
);
if (holdersQuery.isLoading || tokenQuery.isLoading) {
return (
<>
{ bar }
{ isMobile && <SkeletonList/> }
{ !isMobile && (
<SkeletonTable columns={ [ '100%', '300px', '175px' ] }/>
) }
</>
);
}
const items = holdersQuery.data.items;
if (!items?.length) {
return <Text as="span">There are no holders for this token.</Text>;
}
return (
<>
{ bar }
{ !isMobile && <TokenHoldersTable data={ items } token={ tokenQuery.data }/> }
{ isMobile && <TokenHoldersList data={ items } token={ tokenQuery.data }/> }
</>
);
};
export default TokenHoldersContent;
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 TestApp from 'playwright/TestApp';
import TokenHoldersList from './TokenHoldersList';
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ mount }) => {
const component = await mount(
<TestApp>
<TokenHoldersList data={ tokenHolders.items } token={ tokenInfo }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import TokenHoldersListItem from './TokenHoldersListItem';
interface Props {
data: Array<TokenHolder>;
token: TokenInfo;
}
const TokenHoldersList = ({ data, token }: Props) => {
return (
<Box>
{ data.map((item) => (
<TokenHoldersListItem
key={ item.address.hash }
token={ token }
holder={ item }
/>
)) }
</Box>
);
};
export default TokenHoldersList;
import { Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
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 Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
holder: TokenHolder;
token: TokenInfo;
}
const TokenHoldersListItem = ({ holder, token }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).dp(6).toFormat();
return (
<ListItemMobile rowGap={ 3 }>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/>
<AddressLink ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
</Address>
<Flex justifyContent="space-between" alignItems="center" width="100%">
{ quantity }
{ token.total_supply && (
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green"
ml={ 6 }
/>
) }
</Flex>
</ListItemMobile>
);
};
export default TokenHoldersListItem;
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 TestApp from 'playwright/TestApp';
import TokenHoldersTable from './TokenHoldersTable';
test('base view', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h="128px"/>
<TokenHoldersTable data={ tokenHolders.items } token={ tokenInfo }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokenHoldersTableItem from 'ui/token/TokenHolders/TokenHoldersTableItem';
interface Props {
data: Array<TokenHolder>;
token: TokenInfo;
}
const TokenHoldersTable = ({ data, token }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th>Holder</Th>
<Th isNumeric width="300px">Quantity</Th>
{ token.total_supply && <Th isNumeric width="175px">Percentage</Th> }
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TokenHoldersTableItem key={ item.address.hash } holder={ item } token={ token }/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(TokenHoldersTable);
import { Tr, Td } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Utilization from 'ui/shared/Utilization/Utilization';
type Props = {
holder: TokenHolder;
token: TokenInfo;
}
const TokenTransferTableItem = ({ holder, token }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).toFormat();
return (
<Tr>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/>
<AddressLink ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
</Address>
</Td>
<Td isNumeric>
{ quantity }
</Td>
{ token.total_supply && (
<Td isNumeric>
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green"
display="inline-flex"
/>
</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