Commit 8f9971e6 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #1940 from blockscout/fe-1860

add certified conract label
parents 1b977de2 9c194a17
...@@ -18,7 +18,7 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io ...@@ -18,7 +18,7 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
# api configuration # api configuration
NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
# ui config # ui config
......
...@@ -51,6 +51,11 @@ export const verified: SmartContract = { ...@@ -51,6 +51,11 @@ export const verified: SmartContract = {
minimal_proxy_address_hash: null, minimal_proxy_address_hash: null,
}; };
export const certified: SmartContract = {
...verified,
certified: true,
};
export const withMultiplePaths: SmartContract = { export const withMultiplePaths: SmartContract = {
...verified, ...verified,
file_path: './simple_storage.sol', file_path: './simple_storage.sol',
......
...@@ -35,6 +35,7 @@ export const contract2: VerifiedContract = { ...@@ -35,6 +35,7 @@ export const contract2: VerifiedContract = {
watchlist_names: [], watchlist_names: [],
ens_domain_name: null, ens_domain_name: null,
}, },
certified: true,
coin_balance: '9078234570352343999', coin_balance: '9078234570352343999',
compiler_version: 'v0.3.1+commit.0463ea4c', compiler_version: 'v0.3.1+commit.0463ea4c',
has_constructor_args: true, has_constructor_args: true,
......
...@@ -96,6 +96,15 @@ export const contract1: SearchResultAddressOrContract = { ...@@ -96,6 +96,15 @@ export const contract1: SearchResultAddressOrContract = {
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
}; };
export const contract2: SearchResultAddressOrContract = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'Super utko',
type: 'contract' as const,
is_smart_contract_verified: true,
certified: true,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};
export const label1: SearchResultLabel = { export const label1: SearchResultLabel = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'utko', name: 'utko',
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
| "brands/safe" | "brands/safe"
| "brands/solidity_scan" | "brands/solidity_scan"
| "burger" | "burger"
| "certified"
| "check" | "check"
| "clock-light" | "clock-light"
| "clock" | "clock"
...@@ -146,7 +147,6 @@ ...@@ -146,7 +147,6 @@
| "user_op_slim" | "user_op_slim"
| "user_op" | "user_op"
| "validator" | "validator"
| "verified_token"
| "verified" | "verified"
| "verify-contract" | "verify-contract"
| "wallet" | "wallet"
......
...@@ -62,6 +62,7 @@ export interface SmartContract { ...@@ -62,6 +62,7 @@ export interface SmartContract {
minimal_proxy_address_hash: string | null; minimal_proxy_address_hash: string | null;
language: string | null; language: string | null;
license_type: SmartContractLicenseType | null; license_type: SmartContractLicenseType | null;
certified?: boolean;
} }
export type SmartContractDecodedConstructorArg = [ export type SmartContractDecodedConstructorArg = [
......
...@@ -3,6 +3,7 @@ import type { SmartContractLicenseType } from './contract'; ...@@ -3,6 +3,7 @@ import type { SmartContractLicenseType } from './contract';
export interface VerifiedContract { export interface VerifiedContract {
address: AddressParam; address: AddressParam;
certified?: boolean;
coin_balance: string; coin_balance: string;
compiler_version: string; compiler_version: string;
language: 'vyper' | 'yul' | 'solidity'; language: 'vyper' | 'yul' | 'solidity';
......
...@@ -22,6 +22,7 @@ export interface SearchResultAddressOrContract { ...@@ -22,6 +22,7 @@ export interface SearchResultAddressOrContract {
name: string | null; name: string | null;
address: string; address: string;
is_smart_contract_verified: boolean; is_smart_contract_verified: boolean;
certified?: true;
url?: string; // not used by the frontend, we build the url ourselves url?: string; // not used by the frontend, we build the url ourselves
ens_info?: { ens_info?: {
address_hash: string; address_hash: string;
......
...@@ -109,6 +109,13 @@ test('with proxy address alert +@mobile', async({ render, mockApiResponse }) => ...@@ -109,6 +109,13 @@ test('with proxy address alert +@mobile', async({ render, mockApiResponse }) =>
await expect(component.getByRole('alert')).toHaveScreenshot(); await expect(component.getByRole('alert')).toHaveScreenshot();
}); });
test('with certified icon +@mobile', async({ render, mockApiResponse, page }) => {
await mockApiResponse('contract', contractMock.certified, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractCode/>, { hooksConfig });
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 120 } });
});
test('non verified', async({ render, mockApiResponse }) => { test('non verified', async({ render, mockApiResponse }) => {
await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } }); await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } });
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true }); const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
......
...@@ -16,6 +16,7 @@ import { getResourceKey } from 'lib/api/useApiQuery'; ...@@ -16,6 +16,7 @@ import { getResourceKey } from 'lib/api/useApiQuery';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
...@@ -195,6 +196,13 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => { ...@@ -195,6 +196,13 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
return null; return null;
})(); })();
const contractNameWithCertifiedIcon = data?.is_verified ? (
<Flex alignItems="center">
{ data.name }
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } ml={ 2 }/> }
</Flex>
) : null;
return ( return (
<> <>
<Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}> <Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}>
...@@ -248,7 +256,7 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => { ...@@ -248,7 +256,7 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
</Flex> </Flex>
{ data?.is_verified && ( { data?.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }> <Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" content={ data.name } isLoading={ isPlaceholderData }/> } { data.name && <InfoItem label="Contract name" content={ contractNameWithCertifiedIcon } isLoading={ isPlaceholderData }/> }
{ data.compiler_version && <InfoItem label="Compiler version" content={ data.compiler_version } isLoading={ isPlaceholderData }/> } { data.compiler_version && <InfoItem label="Compiler version" content={ data.compiler_version } isLoading={ isPlaceholderData }/> }
{ data.evm_version && <InfoItem label="EVM version" content={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> } { data.evm_version && <InfoItem label="EVM version" content={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ licenseLink && ( { licenseLink && (
......
...@@ -42,7 +42,7 @@ test('search by address hash +@mobile', async({ render, mockApiResponse }) => { ...@@ -42,7 +42,7 @@ test('search by address hash +@mobile', async({ render, mockApiResponse }) => {
}, },
}; };
const data = { const data = {
items: [ searchMock.address1 ], items: [ searchMock.address1, searchMock.contract2 ],
next_page_params: null, next_page_params: null,
}; };
await mockApiResponse('search', data, { queryParams: { q: searchMock.address1.address } }); await mockApiResponse('search', data, { queryParams: { q: searchMock.address1.address } });
......
...@@ -11,6 +11,7 @@ import highlightText from 'lib/highlightText'; ...@@ -11,6 +11,7 @@ import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import { saveToRecentKeywords } from 'lib/recentSearchKeywords'; import { saveToRecentKeywords } from 'lib/recentSearchKeywords';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity'; import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity';
import * as BlockEntity from 'ui/shared/entities/block/BlockEntity'; import * as BlockEntity from 'ui/shared/entities/block/BlockEntity';
...@@ -69,7 +70,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -69,7 +70,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
textOverflow="ellipsis" textOverflow="ellipsis"
/> />
</LinkInternal> </LinkInternal>
{ data.is_verified_via_admin_panel && <IconSvg name="verified_token" boxSize={ 4 } ml={ 1 } color="green.500"/> } { data.is_verified_via_admin_panel && <IconSvg name="certified" boxSize={ 4 } ml={ 1 } color="green.500"/> }
</Flex> </Flex>
); );
} }
...@@ -346,16 +347,21 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -346,16 +347,21 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : ''; const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : '';
return addressName ? ( return addressName ? (
<> <Flex alignItems="center">
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(addressName) : highlightText(addressName, searchTerm) }}/> <Text
{ data.ens_info && overflow="hidden"
( whiteSpace="nowrap"
textOverflow="ellipsis"
>
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(addressName) : highlightText(addressName, searchTerm) }}/>
{ data.ens_info && (
data.ens_info.names_count > 1 ? data.ens_info.names_count > 1 ?
<chakra.span color="text_secondary"> ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` })</chakra.span> : <chakra.span color="text_secondary"> ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` })</chakra.span> :
<chakra.span color="text_secondary">{ expiresText }</chakra.span> <chakra.span color="text_secondary">{ expiresText }</chakra.span>
) ) }
} </Text>
</> { data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } ml={ 1 }/> }
</Flex>
) : ) :
null; null;
} }
......
...@@ -11,6 +11,7 @@ import highlightText from 'lib/highlightText'; ...@@ -11,6 +11,7 @@ import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import { saveToRecentKeywords } from 'lib/recentSearchKeywords'; import { saveToRecentKeywords } from 'lib/recentSearchKeywords';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity'; import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity';
import * as BlockEntity from 'ui/shared/entities/block/BlockEntity'; import * as BlockEntity from 'ui/shared/entities/block/BlockEntity';
...@@ -24,6 +25,7 @@ import LinkExternal from 'ui/shared/LinkExternal'; ...@@ -24,6 +25,7 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import type { SearchResultAppItem } from 'ui/shared/search/utils'; import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils'; import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils';
interface Props { interface Props {
data: SearchResultItem | SearchResultAppItem; data: SearchResultItem | SearchResultAppItem;
searchTerm: string; searchTerm: string;
...@@ -69,7 +71,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -69,7 +71,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }} dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}
/> />
</LinkInternal> </LinkInternal>
{ data.is_verified_via_admin_panel && <IconSvg name="verified_token" boxSize={ 4 } ml={ 1 } color="green.500"/> } { data.is_verified_via_admin_panel && <IconSvg name="certified" boxSize={ 4 } ml={ 1 } color="green.500"/> }
</Flex> </Flex>
</Td> </Td>
<Td fontSize="sm" verticalAlign="middle"> <Td fontSize="sm" verticalAlign="middle">
...@@ -128,14 +130,24 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -128,14 +130,24 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
</Td> </Td>
{ addressName && ( { addressName && (
<Td colSpan={ 2 } fontSize="sm" verticalAlign="middle"> <Td colSpan={ 2 } fontSize="sm" verticalAlign="middle">
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(addressName) : highlightText(addressName, searchTerm) }}/> <Flex alignItems="center">
{ data.ens_info && <Text
( overflow="hidden"
data.ens_info.names_count > 1 ? whiteSpace="nowrap"
<chakra.span color="text_secondary"> ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` })</chakra.span> : textOverflow="ellipsis"
<chakra.span color="text_secondary">{ expiresText }</chakra.span> >
) <span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(addressName) : highlightText(addressName, searchTerm) }}/>
} { data.ens_info && (
data.ens_info.names_count > 1 ? (
<chakra.span color="text_secondary">
{ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }
</chakra.span>
) :
<chakra.span color="text_secondary">{ expiresText }</chakra.span>
) }
</Text>
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } mx={ 1 }/> }
</Flex>
</Td> </Td>
) } ) }
</> </>
......
import { Box, Tooltip, chakra } from '@chakra-ui/react';
import React from 'react';
import IconSvg from './IconSvg';
type Props = {
iconSize: number;
className?: string;
}
const ContractCertifiedLabel = ({ iconSize, className }: Props) => {
return (
<Tooltip label="This contract has been certified by the chain developers">
<Box className={ className }>
<IconSvg name="certified" color="green.500" boxSize={ iconSize } cursor="pointer"/>
</Box>
</Tooltip>
);
};
export default chakra(ContractCertifiedLabel);
...@@ -32,7 +32,7 @@ const DefaultView = () => { ...@@ -32,7 +32,7 @@ const DefaultView = () => {
const contentAfter = ( const contentAfter = (
<> <>
<IconSvg name="verified_token" color="green.500" boxSize={ 6 } cursor="pointer"/> <IconSvg name="certified" color="green.500" boxSize={ 6 } cursor="pointer"/>
<EntityTags <EntityTags
tags={ [ tags={ [
{ slug: 'example', name: 'Example label', tagType: 'custom' }, { slug: 'example', name: 'Example label', tagType: 'custom' },
......
...@@ -28,7 +28,7 @@ const LongNameAndManyTags = () => { ...@@ -28,7 +28,7 @@ const LongNameAndManyTags = () => {
const contentAfter = ( const contentAfter = (
<> <>
<IconSvg name="verified_token" color="green.500" boxSize={ 6 } cursor="pointer" flexShrink={ 0 }/> <IconSvg name="certified" color="green.500" boxSize={ 6 } cursor="pointer" flexShrink={ 0 }/>
<EntityTags <EntityTags
tags={ [ tags={ [
{ slug: 'example', name: 'Example with long name', tagType: 'custom' }, { slug: 'example', name: 'Example with long name', tagType: 'custom' },
......
...@@ -30,6 +30,7 @@ test('search by token name +@mobile +@dark-mode', async({ render, page, mockApi ...@@ -30,6 +30,7 @@ test('search by token name +@mobile +@dark-mode', async({ render, page, mockApi
test('search by contract name +@mobile +@dark-mode', async({ render, page, mockApiResponse }) => { test('search by contract name +@mobile +@dark-mode', async({ render, page, mockApiResponse }) => {
const apiUrl = await mockApiResponse('quick_search', [ const apiUrl = await mockApiResponse('quick_search', [
searchMock.contract1, searchMock.contract1,
searchMock.contract2,
searchMock.address2, searchMock.address2,
], { queryParams: { q: 'o' } }); ], { queryParams: { q: 'o' } });
......
...@@ -6,6 +6,7 @@ import type { SearchResultAddressOrContract } from 'types/api/search'; ...@@ -6,6 +6,7 @@ import type { SearchResultAddressOrContract } from 'types/api/search';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
...@@ -34,21 +35,22 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => { ...@@ -34,21 +35,22 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => {
const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : ''; const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : '';
const nameEl = addressName && ( const nameEl = addressName && (
<Text <Flex alignItems="center">
variant="secondary" <Text
overflow="hidden" variant="secondary"
whiteSpace="nowrap" overflow="hidden"
textOverflow="ellipsis" whiteSpace="nowrap"
> textOverflow="ellipsis"
<chakra.span fontWeight={ 500 } dangerouslySetInnerHTML={{ __html: highlightText(addressName, searchTerm) }}/> >
{ data.ens_info && <chakra.span fontWeight={ 500 } dangerouslySetInnerHTML={{ __html: highlightText(addressName, searchTerm) }}/>
( { data.ens_info && (
data.ens_info.names_count > 1 ? data.ens_info.names_count > 1 ?
<span> ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` })</span> : <span> ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` })</span> :
<span>{ expiresText }</span> <span>{ expiresText }</span>
) ) }
} </Text>
</Text> { data.certified && <ContractCertifiedLabel boxSize={ 5 } iconSize={ 5 } ml={ 1 }/> }
</Flex>
); );
const addressEl = <HashStringShortenDynamic hash={ data.address } isTooltipDisabled/>; const addressEl = <HashStringShortenDynamic hash={ data.address } isTooltipDisabled/>;
......
...@@ -16,7 +16,7 @@ interface Props { ...@@ -16,7 +16,7 @@ interface Props {
const SearchBarSuggestToken = ({ data, isMobile, searchTerm }: Props) => { const SearchBarSuggestToken = ({ data, isMobile, searchTerm }: Props) => {
const icon = <TokenEntity.Icon token={{ ...data, type: data.token_type }}/>; const icon = <TokenEntity.Icon token={{ ...data, type: data.token_type }}/>;
const verifiedIcon = <IconSvg name="verified_token" boxSize={ 4 } color="green.500" ml={ 1 }/>; const verifiedIcon = <IconSvg name="certified" boxSize={ 4 } color="green.500" ml={ 1 }/>;
const name = ( const name = (
<Text <Text
fontWeight={ 700 } fontWeight={ 700 }
......
...@@ -98,7 +98,7 @@ const TokenPageTitle = ({ tokenQuery, addressQuery, hash }: Props) => { ...@@ -98,7 +98,7 @@ const TokenPageTitle = ({ tokenQuery, addressQuery, hash }: Props) => {
{ verifiedInfoQuery.data?.tokenAddress && ( { verifiedInfoQuery.data?.tokenAddress && (
<Tooltip label={ `Information on this token has been verified by ${ config.chain.name }` }> <Tooltip label={ `Information on this token has been verified by ${ config.chain.name }` }>
<Box boxSize={ 6 }> <Box boxSize={ 6 }>
<IconSvg name="verified_token" color="green.500" boxSize={ 6 } cursor="pointer"/> <IconSvg name="certified" color="green.500" boxSize={ 6 } cursor="pointer"/>
</Box> </Box>
</Tooltip> </Tooltip>
) } ) }
......
...@@ -8,6 +8,7 @@ import config from 'configs/app'; ...@@ -8,6 +8,7 @@ import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
...@@ -36,12 +37,15 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => { ...@@ -36,12 +37,15 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => {
return ( return (
<ListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Flex w="100%"> <Flex w="100%">
<AddressEntity <Flex alignItems="center" overflow="hidden">
isLoading={ isLoading } <AddressEntity
address={ data.address } isLoading={ isLoading }
query={{ tab: 'contract' }} address={ data.address }
noCopy query={{ tab: 'contract' }}
/> noCopy
/>
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } mx={ 2 }/> }
</Flex>
<Skeleton isLoaded={ !isLoading } color="text_secondary" ml="auto"> <Skeleton isLoaded={ !isLoading } color="text_secondary" ml="auto">
<HashStringShorten hash={ data.address.hash } isTooltipDisabled/> <HashStringShorten hash={ data.address.hash } isTooltipDisabled/>
</Skeleton> </Skeleton>
......
...@@ -7,6 +7,7 @@ import type { VerifiedContract } from 'types/api/contracts'; ...@@ -7,6 +7,7 @@ import type { VerifiedContract } from 'types/api/contracts';
import config from 'configs/app'; import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
...@@ -34,13 +35,15 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => { ...@@ -34,13 +35,15 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => {
return ( return (
<Tr> <Tr>
<Td> <Td>
<AddressEntity <Flex alignItems="center" mt={ 1 }>
address={ data.address } <AddressEntity
isLoading={ isLoading } address={ data.address }
query={{ tab: 'contract' }} isLoading={ isLoading }
noCopy query={{ tab: 'contract' }}
mt={ 1 } noCopy
/> />
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } ml={ 2 }/> }
</Flex>
<Flex alignItems="center" ml={ 7 }> <Flex alignItems="center" ml={ 7 }>
<Skeleton isLoaded={ !isLoading } color="text_secondary" my={ 1 }> <Skeleton isLoaded={ !isLoading } color="text_secondary" my={ 1 }>
<HashStringShorten hash={ data.address.hash } isTooltipDisabled/> <HashStringShorten hash={ data.address.hash } isTooltipDisabled/>
......
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