Commit 56654f59 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #674 from blockscout/verified-contracts

verified contracts page
parents ac3317ec 97fd09af
......@@ -294,11 +294,11 @@ frontend:
# - "/(apps|auth/profile|account)"
- "/"
prefix:
# - "/(apps|auth/profile|account)"
- "/account"
- "/apps"
# - "/(apps|auth/profile|account)"
- "/_next"
- "/node-api"
- "/account"
- "/apps"
- "/static"
- "/auth/profile"
- "/txs"
......@@ -308,7 +308,15 @@ frontend:
- "/login"
- "/address"
- "/stats"
- "/search-results"
- "/token"
- "/tokens"
- "/accounts"
- "/visualize"
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
resources:
limits:
memory:
......
......@@ -343,25 +343,27 @@ frontend:
# - "/(apps|auth/profile|account)"
- "/"
prefix:
- "/apps"
- "/_next"
- "/node-api"
- "/account"
- "/apps"
- "/static"
- "/auth/profile"
- "/account"
- "/txs"
- "/tx"
- "/blocks"
- "/block"
- "/login"
- "/address"
- "/stats"
- "/search-results"
- "/tokens"
- "/token"
- "/tokens"
- "/accounts"
- "/visualize"
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
resources:
limits:
......
......@@ -17,11 +17,11 @@ frontend:
# - "/(apps|auth/profile|account)"
- "/"
prefix:
# - "/(apps|auth/profile|account)"
- "/account"
- "/apps"
# - "/(apps|auth/profile|account)"
- "/_next"
- "/node-api"
- "/account"
- "/apps"
- "/static"
- "/auth/profile"
- "/txs"
......@@ -38,6 +38,7 @@ frontend:
- "/visualize"
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
resources:
limits:
......
......@@ -17,11 +17,11 @@ frontend:
# - "/(apps|auth/profile|account)"
- "/"
prefix:
# - "/(apps|auth/profile|account)"
- "/account"
- "/apps"
# - "/(apps|auth/profile|account)"
- "/_next"
- "/node-api"
- "/account"
- "/apps"
- "/static"
- "/auth/profile"
- "/txs"
......@@ -38,6 +38,7 @@ frontend:
- "/visualize"
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
resources:
limits:
......
......@@ -17,6 +17,7 @@ import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters } from 'types/api/contracts';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
......@@ -258,6 +259,12 @@ export const RESOURCES = {
pathParams: [ 'hash' as const, 'method' as const ],
},
verified_contracts: {
path: '/api/v2/smart-contracts',
paginationFields: [ 'items_count' as const, 'smart_contract_id' as const ],
filterFields: [ 'q' as const, 'filter' as const ],
},
// TOKEN
token: {
path: '/api/v2/tokens/:hash',
......@@ -404,7 +411,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'search' |
'address_logs' | 'address_tokens' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
'token_instance_transfers';
'token_instance_transfers' |
'verified_contracts';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -462,6 +470,7 @@ Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> :
Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> :
Q extends 'verified_contracts' ? VerifiedContractsResponse :
Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
never;
......@@ -478,5 +487,6 @@ Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter :
Q extends 'search' ? SearchResultFilters :
Q extends 'tokens' ? TokensFilters :
Q extends 'verified_contracts' ? VerifiedContractsFilters :
never;
/* eslint-enable @typescript-eslint/indent */
......@@ -95,7 +95,7 @@ export default function useNavItems(): ReturnType {
};
const verifiedContracts =
// eslint-disable-next-line max-len
{ text: 'Verified contracts', nextRoute: { pathname: '/verified-contracts' as const }, icon: verifiedIcon, isActive: false, isNewUi: false };
{ text: 'Verified contracts', nextRoute: { pathname: '/verified-contracts' as const }, icon: verifiedIcon, isActive: pathname === '/verified-contracts', isNewUi: true };
if (appConfig.L2.isL2Network) {
blockchainNavItems = [
......
import type { VerifiedContract, VerifiedContractsResponse } from 'types/api/contracts';
export const contract1: VerifiedContract = {
address: {
hash: '0xef490030ac0d53B70E304b6Bc5bF657dc6780bEB',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'MockERC20',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
coin_balance: '2346534676900000008',
compiler_version: 'v0.8.17+commit.8df45f5f',
has_constructor_args: false,
language: 'solidity',
market_cap: null,
optimization_enabled: false,
tx_count: 7334224,
verified_at: '2022-09-16T18:49:29.605179Z',
};
export const contract2: VerifiedContract = {
address: {
hash: '0xB2218bdEbe8e90f80D04286772B0968ead666942',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'EternalStorageProxyWithSomeExternalLibrariesAndEvenMore',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
coin_balance: '9078234570352343999',
compiler_version: 'v0.3.1+commit.0463ea4c',
has_constructor_args: true,
language: 'vyper',
market_cap: null,
optimization_enabled: true,
tx_count: 440,
verified_at: '2021-09-07T20:01:56.076979Z',
};
export const baseResponse: VerifiedContractsResponse = {
items: [
contract1,
contract2,
],
next_page_params: {
items_count: '50',
smart_contract_id: '172',
},
};
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import VerifiedContracts from 'ui/pages/VerifiedContracts';
import Page from 'ui/shared/Page/Page';
const VerifiedContractsPage: NextPage = () => {
return null;
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Page>
<VerifiedContracts/>
</Page>
</>
);
};
export default VerifiedContractsPage;
export async function getServerSideProps() {
return {
notFound: true,
};
}
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { AddressParam } from './addressParams';
export interface VerifiedContract {
address: AddressParam;
coin_balance: string;
compiler_version: string;
language: 'vyper' | 'yul' | 'solidity';
has_constructor_args: boolean;
optimization_enabled: boolean;
tx_count: number | null;
verified_at: string;
market_cap: string | null;
}
export interface VerifiedContractsResponse {
items: Array<VerifiedContract>;
next_page_params: {
items_count: string;
smart_contract_id: string;
} | null;
}
export interface VerifiedContractsFilters {
q: string | undefined;
filter: 'vyper' | 'solidity' | undefined;
}
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as textAdMock from 'mocks/ad/textAd';
import * as verifiedContractsMock from 'mocks/contracts/index';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import VerifiedContracts from './VerifiedContracts';
const VERIFIED_CONTRACTS_API_URL = buildApiUrl('verified_contracts');
test.beforeEach(async({ page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: JSON.stringify(textAdMock.duck),
}));
await page.route(textAdMock.duck.ad.thumbnail, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
});
test('base view +@mobile', async({ mount, page }) => {
await page.route(VERIFIED_CONTRACTS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(verifiedContractsMock.baseResponse),
}));
const component = await mount(
<TestApp>
<VerifiedContracts/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Flex, Hide, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { VerifiedContractsFilters } from 'types/api/contracts';
import useDebounce from 'lib/hooks/useDebounce';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterInput from 'ui/shared/filters/FilterInput';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import Sort from 'ui/shared/sort/Sort';
import type { SortField, Sort as TSort } from 'ui/verifiedContracts/utils';
import { SORT_OPTIONS, sortFn, getNextSortValue } from 'ui/verifiedContracts/utils';
import VerifiedContractsFilter from 'ui/verifiedContracts/VerifiedContractsFilter';
import VerifiedContractsList from 'ui/verifiedContracts/VerifiedContractsList';
import VerifiedContractsTable from 'ui/verifiedContracts/VerifiedContractsTable';
const VerifiedContracts = () => {
const router = useRouter();
const [ searchTerm, setSearchTerm ] = React.useState(getQueryParamString(router.query.q) || undefined);
const [ type, setType ] = React.useState(getQueryParamString(router.query.filter) as VerifiedContractsFilters['filter'] || undefined);
const [ sort, setSort ] = React.useState<TSort>();
const debouncedSearchTerm = useDebounce(searchTerm || '', 300);
const { isError, isLoading, data, isPaginationVisible, pagination, onFilterChange } = useQueryWithPages({
resourceName: 'verified_contracts',
filters: { q: debouncedSearchTerm, filter: type },
});
const handleSearchTermChange = React.useCallback((value: string) => {
onFilterChange({ q: value, filter: type });
setSearchTerm(value);
}, [ type, onFilterChange ]);
const handleTypeChange = React.useCallback((value: string | Array<string>) => {
if (Array.isArray(value)) {
return;
}
if ((value === 'vyper' || value === 'solidity')) {
onFilterChange({ q: debouncedSearchTerm, filter: value });
setType(value);
return;
}
onFilterChange({ q: debouncedSearchTerm, filter: undefined });
setType(undefined);
}, [ debouncedSearchTerm, onFilterChange ]);
const handleSortToggle = React.useCallback((field: SortField) => {
return () => {
setSort(getNextSortValue(field));
};
}, []);
const typeFilter = <VerifiedContractsFilter onChange={ handleTypeChange } defaultValue={ type } isActive={ Boolean(type) }/>;
const filterInput = (
<FilterInput
w={{ base: '100%', lg: '350px' }}
size="xs"
onChange={ handleSearchTermChange }
placeholder="Search by contract name or address"
initialValue={ searchTerm }
/>
);
const sortButton = (
<Sort
options={ SORT_OPTIONS }
sort={ sort }
setSort={ setSort }
/>
);
const bar = (
<>
<Show below="lg" ssr={ false }>
<Flex columnGap={ 3 } mb={ 6 }>
{ typeFilter }
{ sortButton }
{ filterInput }
</Flex>
</Show>
<ActionBar mt={ -6 }>
<Hide below="lg" ssr={ false }>
<Flex columnGap={ 3 }>
{ typeFilter }
{ filterInput }
</Flex>
</Hide>
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar>
</>
);
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '50%', '130px', '130px', '50%', '80px', '110px' ] }/>
</Hide>
</>
);
}
if (data.items.length === 0 && !searchTerm && !type) {
return <Text as="span">There are no verified contracts</Text>;
}
if (data.items.length === 0) {
return <EmptySearchResult text={ `Couldn${ apos }t find any contract that matches your query.` }/>;
}
const sortedData = data.items.slice().sort(sortFn(sort));
return (
<>
<Show below="lg" ssr={ false }>
<VerifiedContractsList data={ sortedData }/>
</Show>
<Hide below="lg" ssr={ false }>
<VerifiedContractsTable data={ sortedData } sort={ sort } onSortToggle={ handleSortToggle }/>
</Hide>
</>
);
})();
return (
<Box>
<PageTitle text="Verified contracts" withTextAd/>
{ bar }
{ content }
</Box>
);
};
export default VerifiedContracts;
......@@ -37,7 +37,7 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi
justifyContent="space-between"
className={ className }
>
<Flex flexWrap="wrap" columnGap={ 3 } alignItems="center" width={ withTextAd ? 'unset' : '100%' }>
<Flex flexWrap="wrap" columnGap={ 3 } alignItems="center" width={ withTextAd ? 'unset' : '100%' } flexShrink={ 0 }>
<Grid
templateColumns={ [ backLinkUrl && 'auto', additionalsLeft && 'auto', '1fr' ].filter(Boolean).join(' ') }
columnGap={ 3 }
......
......@@ -9,45 +9,40 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import type { Sort } from 'types/client/txs-sort';
import SortButton from './SortButton';
import SortButton from 'ui/shared/SortButton';
interface Props {
isActive: boolean;
sorting: Sort;
setSorting: (val: Sort) => void;
export interface Option<Sort extends string> {
title: string;
id: Sort | undefined;
}
const SORT_OPTIONS = [
{ title: 'Default', id: '' },
{ title: 'Value ascending', id: 'val-asc' },
{ title: 'Value descending', id: 'val-desc' },
{ title: 'Fee ascending', id: 'fee-asc' },
{ title: 'Fee descending', id: 'fee-desc' },
];
interface Props<Sort extends string> {
options: Array<Option<Sort>>;
sort: Sort | undefined;
setSort: (value: Sort | undefined) => void;
}
const TxsSorting = ({ isActive, sorting, setSorting }: Props) => {
const Sort = <Sort extends string>({ sort, setSort, options }: Props<Sort>) => {
const { isOpen, onToggle } = useDisclosure();
const setSortingFromMenu = React.useCallback((val: string | Array<string>) => {
const value = val as Sort | Array<Sort>;
setSorting(Array.isArray(value) ? value[0] : value);
}, [ setSorting ]);
setSort(Array.isArray(value) ? value[0] : value);
}, [ setSort ]);
return (
<Menu>
<MenuButton>
<SortButton
isActive={ isOpen || isActive }
isActive={ isOpen || Boolean(sort) }
onClick={ onToggle }
/>
</MenuButton>
<MenuList minWidth="240px">
<MenuOptionGroup value={ sorting } title="Sort by" type="radio" onChange={ setSortingFromMenu }>
{ SORT_OPTIONS.map((option) => (
<MenuList minWidth="240px" zIndex="popover">
<MenuOptionGroup value={ sort } title="Sort by" type="radio" onChange={ setSortingFromMenu }>
{ options.map((option) => (
<MenuItemOption
key={ option.id }
key={ option.id || 'default' }
value={ option.id }
>
{ option.title }
......@@ -59,4 +54,4 @@ const TxsSorting = ({ isActive, sorting, setSorting }: Props) => {
);
};
export default chakra(TxsSorting);
export default React.memo(chakra(Sort)) as typeof Sort;
import { HStack, chakra } from '@chakra-ui/react';
import React from 'react';
import type { Sort } from 'types/client/txs-sort';
import type { Sort as TSort } from 'types/client/txs-sort';
// import FilterInput from 'ui/shared/filters/FilterInput';
import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import TxsSorting from 'ui/txs/TxsSorting';
import type { Option } from 'ui/shared/sort/Sort';
import Sort from 'ui/shared/sort/Sort';
// import TxsFilters from './TxsFilters';
const SORT_OPTIONS: Array<Option<TSort>> = [
{ title: 'Default', id: undefined },
{ title: 'Value ascending', id: 'val-asc' },
{ title: 'Value descending', id: 'val-desc' },
{ title: 'Fee ascending', id: 'fee-asc' },
{ title: 'Fee descending', id: 'fee-desc' },
];
type Props = {
sorting: Sort;
setSorting: (val: Sort) => void;
sorting: TSort;
setSorting: (val: TSort | undefined) => void;
paginationProps: PaginationProps;
className?: string;
showPagination?: boolean;
......@@ -26,10 +35,10 @@ const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps
<ActionBar className={ className }>
<HStack>
{ filterComponent }
<TxsSorting
isActive={ Boolean(sorting) }
setSorting={ setSorting }
sorting={ sorting }
<Sort
options={ SORT_OPTIONS }
setSort={ setSorting }
sort={ sorting }
/>
{ /* api is not implemented */ }
{ /* <FilterInput
......
......@@ -10,7 +10,7 @@ import sortTxs from 'lib/tx/sortTxs';
type HookResult = UseQueryResult<TxsResponse> & {
sorting: Sort;
setSortByField: (field: 'val' | 'fee') => () => void;
setSortByValue: (value: Sort) => void;
setSortByValue: (value: Sort | undefined) => void;
}
export default function useTxsSort(
......@@ -45,7 +45,7 @@ export default function useTxsSort(
});
}, [ ]);
const setSortByValue = React.useCallback((value: Sort) => {
const setSortByValue = React.useCallback((value: Sort | undefined) => {
setSorting((prevVal: Sort) => {
let newVal: Sort = '';
if (value !== prevVal) {
......
import {
Menu,
MenuButton,
MenuList,
MenuOptionGroup,
MenuItemOption,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import type { VerifiedContractsFilters } from 'types/api/contracts';
import FilterButton from 'ui/shared/filters/FilterButton';
interface Props {
isActive: boolean;
defaultValue: VerifiedContractsFilters['filter'] | undefined;
onChange: (nextValue: string | Array<string>) => void;
}
const VerifiedContractsFilter = ({ onChange, defaultValue, isActive }: Props) => {
const { isOpen, onToggle } = useDisclosure();
return (
<Menu>
<MenuButton>
<FilterButton
isActive={ isOpen || isActive }
onClick={ onToggle }
as="div"
/>
</MenuButton>
<MenuList zIndex="popover">
<MenuOptionGroup defaultValue={ defaultValue || 'all' } title="Filter" type="radio" onChange={ onChange }>
<MenuItemOption value="all">All</MenuItemOption>
<MenuItemOption value="solidity">Solidity</MenuItemOption>
<MenuItemOption value="vyper">Vyper</MenuItemOption>
</MenuOptionGroup>
</MenuList>
</Menu>
);
};
export default React.memo(VerifiedContractsFilter);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { VerifiedContract } from 'types/api/contracts';
import VerifiedContractsListItem from './VerifiedContractsListItem';
const VerifiedContractsList = ({ data }: { data: Array<VerifiedContract>}) => {
return (
<Box>
{ data.map((item) => <VerifiedContractsListItem key={ item.address.hash } data={ item }/>) }
</Box>
);
};
export default React.memo(VerifiedContractsList);
import { Box, Flex, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { VerifiedContract } from 'types/api/contracts';
import appConfig from 'configs/app/config';
import iconCheck from 'icons/check.svg';
import iconCross from 'icons/cross.svg';
import iconSuccess from 'icons/status/success.svg';
import dayjs from 'lib/date/dayjs';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
import ListItemMobile from 'ui/shared/ListItemMobile';
interface Props {
data: VerifiedContract;
}
const VerifiedContractsListItem = ({ data }: Props) => {
const balance = data.coin_balance && data.coin_balance !== '0' ?
BigNumber(data.coin_balance).div(10 ** appConfig.network.currency.decimals).dp(6).toFormat() :
'0';
return (
<ListItemMobile rowGap={ 3 }>
<Address columnGap={ 2 } overflow="hidden" w="100%">
<AddressIcon address={ data.address }/>
<AddressLink hash={ data.address.hash } type="address" alias={ data.address.name }/>
<Box color="text_secondary" ml="auto">
<HashStringShorten hash={ data.address.hash } isTooltipDisabled/>
</Box>
</Address>
<Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Balance { appConfig.network.currency.symbol }</Box>
<Box color="text_secondary">
{ balance }
</Box>
</Flex>
<Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Txs count</Box>
<Box color="text_secondary">
{ data.tx_count ? data.tx_count.toLocaleString() : '0' }
</Box>
</Flex>
<Flex columnGap={ 3 }>
<Box fontWeight={ 500 } flexShrink="0">Compiler</Box>
<Flex flexWrap="wrap">
<Box textTransform="capitalize">{ data.language }</Box>
<Box color="text_secondary" wordBreak="break-all" whiteSpace="pre-wrap"> ({ data.compiler_version })</Box>
</Flex>
</Flex>
<Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Optimization</Box>
{ data.optimization_enabled ?
<Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer"/> :
<Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer"/> }
</Flex>
<Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Constructor args</Box>
{ data.has_constructor_args ?
<Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer"/> :
<Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer"/> }
</Flex>
<Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Verified</Box>
<Flex alignItems="center" columnGap={ 2 }>
<Icon as={ iconSuccess } boxSize={ 4 } color="green.500"/>
<Box color="text_secondary">
{ dayjs(data.verified_at).fromNow() }
</Box>
</Flex>
</Flex>
{ /* <Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Market cap</Box>
<Box color="text_secondary">
N/A
</Box>
</Flex> */ }
</ListItemMobile>
);
};
export default React.memo(VerifiedContractsListItem);
import { Table, Tbody, Tr, Th, Link, Icon } from '@chakra-ui/react';
import React from 'react';
import type { VerifiedContract } from 'types/api/contracts';
import appConfig from 'configs/app/config';
import arrowIcon from 'icons/arrows/east.svg';
import { default as Thead } from 'ui/shared/TheadSticky';
import type { Sort, SortField } from './utils';
import VerifiedContractsTableItem from './VerifiedContractsTableItem';
interface Props {
data: Array<VerifiedContract>;
sort: Sort | undefined;
onSortToggle: (field: SortField) => () => void;
}
const VerifiedContractsTable = ({ data, sort, onSortToggle }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return (
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th width="50%">Contract</Th>
<Th width="130px" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('balance') } columnGap={ 1 }>
{ sort?.includes('balance') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Balance { appConfig.network.currency.symbol }
</Link>
</Th>
<Th width="130px" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('txs') } columnGap={ 1 }>
{ sort?.includes('txs') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Txs
</Link>
</Th>
<Th width="50%">Compiler / version</Th>
<Th width="80px">Settings</Th>
<Th width="110px">Verified</Th>
{ /* <Th width="120px">Market cap</Th> */ }
</Tr>
</Thead>
<Tbody>
{ data.map((item) => <VerifiedContractsTableItem key={ item.address.hash } data={ item }/>) }
</Tbody>
</Table>
);
};
export default React.memo(VerifiedContractsTable);
import { Tr, Td, Icon, Box, Flex, chakra, Tooltip } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { VerifiedContract } from 'types/api/contracts';
import appConfig from 'configs/app/config';
import iconCheck from 'icons/check.svg';
import iconCross from 'icons/cross.svg';
import iconSuccess from 'icons/status/success.svg';
import dayjs from 'lib/date/dayjs';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
interface Props {
data: VerifiedContract;
}
const VerifiedContractsTableItem = ({ data }: Props) => {
const balance = data.coin_balance && data.coin_balance !== '0' ?
BigNumber(data.coin_balance).div(10 ** appConfig.network.currency.decimals).dp(6).toFormat() :
'0';
return (
<Tr>
<Td>
<Flex columnGap={ 2 }>
<AddressIcon address={ data.address }/>
<Flex columnGap={ 2 } flexWrap="wrap" lineHeight={ 6 } w="calc(100% - 32px)">
<AddressLink hash={ data.address.hash } type="address" alias={ data.address.name }/>
<Box color="text_secondary">
<HashStringShorten hash={ data.address.hash } isTooltipDisabled/>
</Box>
</Flex>
</Flex>
</Td>
<Td isNumeric lineHeight={ 6 }>
{ balance }
</Td>
<Td isNumeric lineHeight={ 6 }>
{ data.tx_count ? data.tx_count.toLocaleString() : '0' }
</Td>
<Td lineHeight={ 6 }>
<Flex flexWrap="wrap" columnGap={ 2 }>
<chakra.span textTransform="capitalize">{ data.language }</chakra.span>
<chakra.span color="text_secondary" wordBreak="break-all">{ data.compiler_version }</chakra.span>
</Flex>
</Td>
<Td>
<Tooltip label="Optimization">
<span>
{ data.optimization_enabled ?
<Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer"/> :
<Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer"/> }
</span>
</Tooltip>
<Tooltip label="Constructor args">
<chakra.span ml={ 3 }>
{ data.has_constructor_args ?
<Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer"/> :
<Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer"/> }
</chakra.span>
</Tooltip>
</Td>
<Td lineHeight={ 6 }>
<Flex alignItems="center" columnGap={ 2 }>
<Icon as={ iconSuccess } boxSize={ 4 } color="green.500"/>
<chakra.span color="text_secondary">
{ dayjs(data.verified_at).fromNow() }
</chakra.span>
</Flex>
</Td>
{ /* <Td lineHeight={ 6 }>
N/A
</Td> */ }
</Tr>
);
};
export default React.memo(VerifiedContractsTableItem);
import type { VerifiedContract } from 'types/api/contracts';
import compareBns from 'lib/bigint/compareBns';
import type { Option } from 'ui/shared/sort/Sort';
export type SortField = 'balance' | 'txs';
export type Sort = `${ SortField }-asc` | `${ SortField }-desc`;
export const SORT_OPTIONS: Array<Option<Sort>> = [
{ title: 'Default', id: undefined },
{ title: 'Balance descending', id: 'balance-desc' },
{ title: 'Balance ascending', id: 'balance-asc' },
{ title: 'Txs count descending', id: 'txs-desc' },
{ title: 'Txs count ascending', id: 'txs-asc' },
];
const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = {
balance: [ 'balance-desc', 'balance-asc', undefined ],
txs: [ 'txs-desc', 'txs-asc', undefined ],
};
export const getNextSortValue = (field: SortField) => (prevValue: Sort | undefined) => {
const sequence = SORT_SEQUENCE[field];
const curIndex = sequence.findIndex((sort) => sort === prevValue);
const nextIndex = curIndex + 1 > sequence.length - 1 ? 0 : curIndex + 1;
return sequence[nextIndex];
};
export const sortFn = (sort: Sort | undefined) => (a: VerifiedContract, b: VerifiedContract) => {
switch (sort) {
case 'balance-asc':
case 'balance-desc': {
const result = compareBns(b.coin_balance, a.coin_balance) * (sort.includes('desc') ? 1 : -1);
return a.coin_balance === b.coin_balance ? 0 : result;
}
case 'txs-asc':
case 'txs-desc': {
const result = ((a.tx_count || 0) > (b.tx_count || 0) ? -1 : 1) * (sort.includes('desc') ? 1 : -1);
return a.tx_count === b.tx_count ? 0 : result;
}
default:
return 0;
}
};
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