Skip to content

Commit

Permalink
Merge pull request #1940 from blockscout/fe-1860
Browse files Browse the repository at this point in the history
add certified conract label
  • Loading branch information
isstuev authored May 26, 2024
2 parents 1b977de + 9c194a1 commit 8f9971e
Show file tree
Hide file tree
Showing 32 changed files with 132 additions and 50 deletions.
2 changes: 1 addition & 1 deletion configs/envs/.env.eth_sepolia
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io
NEXT_PUBLIC_IS_TESTNET=true

# 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=/

# ui config
Expand Down
File renamed without changes
5 changes: 5 additions & 0 deletions mocks/contract/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export const verified: SmartContract = {
minimal_proxy_address_hash: null,
};

export const certified: SmartContract = {
...verified,
certified: true,
};

export const withMultiplePaths: SmartContract = {
...verified,
file_path: './simple_storage.sol',
Expand Down
1 change: 1 addition & 0 deletions mocks/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const contract2: VerifiedContract = {
watchlist_names: [],
ens_domain_name: null,
},
certified: true,
coin_balance: '9078234570352343999',
compiler_version: 'v0.3.1+commit.0463ea4c',
has_constructor_args: true,
Expand Down
9 changes: 9 additions & 0 deletions mocks/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ export const contract1: SearchResultAddressOrContract = {
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 = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'utko',
Expand Down
2 changes: 1 addition & 1 deletion public/icons/name.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
| "brands/safe"
| "brands/solidity_scan"
| "burger"
| "certified"
| "check"
| "clock-light"
| "clock"
Expand Down Expand Up @@ -146,7 +147,6 @@
| "user_op_slim"
| "user_op"
| "validator"
| "verified_token"
| "verified"
| "verify-contract"
| "wallet"
Expand Down
1 change: 1 addition & 0 deletions types/api/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface SmartContract {
minimal_proxy_address_hash: string | null;
language: string | null;
license_type: SmartContractLicenseType | null;
certified?: boolean;
}

export type SmartContractDecodedConstructorArg = [
Expand Down
1 change: 1 addition & 0 deletions types/api/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { SmartContractLicenseType } from './contract';

export interface VerifiedContract {
address: AddressParam;
certified?: boolean;
coin_balance: string;
compiler_version: string;
language: 'vyper' | 'yul' | 'solidity';
Expand Down
1 change: 1 addition & 0 deletions types/api/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface SearchResultAddressOrContract {
name: string | null;
address: string;
is_smart_contract_verified: boolean;
certified?: true;
url?: string; // not used by the frontend, we build the url ourselves
ens_info?: {
address_hash: string;
Expand Down
7 changes: 7 additions & 0 deletions ui/address/contract/ContractCode.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ test('with proxy address alert +@mobile', async({ render, mockApiResponse }) =>
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 }) => {
await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } });
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
Expand Down
10 changes: 9 additions & 1 deletion ui/address/contract/ContractCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getResourceKey } from 'lib/api/useApiQuery';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import Hint from 'ui/shared/Hint';
Expand Down Expand Up @@ -195,6 +196,13 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
return null;
})();

const contractNameWithCertifiedIcon = data?.is_verified ? (
<Flex alignItems="center">
{ data.name }
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } ml={ 2 }/> }
</Flex>
) : null;

return (
<>
<Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}>
Expand Down Expand Up @@ -248,7 +256,7 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
</Flex>
{ data?.is_verified && (
<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.evm_version && <InfoItem label="EVM version" content={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ licenseLink && (
Expand Down
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.
2 changes: 1 addition & 1 deletion ui/pages/SearchResults.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test('search by address hash +@mobile', async({ render, mockApiResponse }) => {
},
};
const data = {
items: [ searchMock.address1 ],
items: [ searchMock.address1, searchMock.contract2 ],
next_page_params: null,
};
await mockApiResponse('search', data, { queryParams: { q: searchMock.address1.address } });
Expand Down
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 14 additions & 8 deletions ui/searchResults/SearchResultListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index';
import { saveToRecentKeywords } from 'lib/recentSearchKeywords';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity';
import * as BlockEntity from 'ui/shared/entities/block/BlockEntity';
Expand Down Expand Up @@ -69,7 +70,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
textOverflow="ellipsis"
/>
</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>
);
}
Expand Down Expand Up @@ -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() })` : '';

return addressName ? (
<>
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(addressName) : highlightText(addressName, searchTerm) }}/>
{ data.ens_info &&
(
<Flex alignItems="center">
<Text
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
<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 } ml={ 1 }/> }
</Flex>
) :
null;
}
Expand Down
30 changes: 21 additions & 9 deletions ui/searchResults/SearchResultTableItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index';
import { saveToRecentKeywords } from 'lib/recentSearchKeywords';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity';
import * as BlockEntity from 'ui/shared/entities/block/BlockEntity';
Expand All @@ -24,6 +25,7 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils';

interface Props {
data: SearchResultItem | SearchResultAppItem;
searchTerm: string;
Expand Down Expand Up @@ -69,7 +71,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}
/>
</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>
</Td>
<Td fontSize="sm" verticalAlign="middle">
Expand Down Expand Up @@ -128,14 +130,24 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
</Td>
{ addressName && (
<Td colSpan={ 2 } fontSize="sm" verticalAlign="middle">
<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>
)
}
<Flex alignItems="center">
<Text
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
<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>
) }
</>
Expand Down
21 changes: 21 additions & 0 deletions ui/shared/ContractCertifiedLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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);
2 changes: 1 addition & 1 deletion ui/shared/Page/specs/DefaultView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const DefaultView = () => {

const contentAfter = (
<>
<IconSvg name="verified_token" color="green.500" boxSize={ 6 } cursor="pointer"/>
<IconSvg name="certified" color="green.500" boxSize={ 6 } cursor="pointer"/>
<EntityTags
tags={ [
{ slug: 'example', name: 'Example label', tagType: 'custom' },
Expand Down
2 changes: 1 addition & 1 deletion ui/shared/Page/specs/LongNameAndManyTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const LongNameAndManyTags = () => {

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
tags={ [
{ slug: 'example', name: 'Example with long name', tagType: 'custom' },
Expand Down
1 change: 1 addition & 0 deletions ui/snippets/searchBar/SearchBar.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
const apiUrl = await mockApiResponse('quick_search', [
searchMock.contract1,
searchMock.contract2,
searchMock.address2,
], { queryParams: { q: 'o' } });

Expand Down
26 changes: 14 additions & 12 deletions ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { SearchResultAddressOrContract } from 'types/api/search';
import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';

Expand Down Expand Up @@ -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 nameEl = addressName && (
<Text
variant="secondary"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
<chakra.span fontWeight={ 500 } dangerouslySetInnerHTML={{ __html: highlightText(addressName, searchTerm) }}/>
{ data.ens_info &&
(
<Flex alignItems="center">
<Text
variant="secondary"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
<chakra.span fontWeight={ 500 } dangerouslySetInnerHTML={{ __html: highlightText(addressName, searchTerm) }}/>
{ data.ens_info && (
data.ens_info.names_count > 1 ?
<span> ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` })</span> :
<span>{ expiresText }</span>
)
}
</Text>
) }
</Text>
{ data.certified && <ContractCertifiedLabel boxSize={ 5 } iconSize={ 5 } ml={ 1 }/> }
</Flex>
);
const addressEl = <HashStringShortenDynamic hash={ data.address } isTooltipDisabled/>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface Props {

const SearchBarSuggestToken = ({ data, isMobile, searchTerm }: Props) => {
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 = (
<Text
fontWeight={ 700 }
Expand Down
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.
2 changes: 1 addition & 1 deletion ui/token/TokenPageTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const TokenPageTitle = ({ tokenQuery, addressQuery, hash }: Props) => {
{ verifiedInfoQuery.data?.tokenAddress && (
<Tooltip label={ `Information on this token has been verified by ${ config.chain.name }` }>
<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>
</Tooltip>
) }
Expand Down
16 changes: 10 additions & 6 deletions ui/verifiedContracts/VerifiedContractsListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs';
import { currencyUnits } from 'lib/units';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShorten from 'ui/shared/HashStringShorten';
Expand Down Expand Up @@ -36,12 +37,15 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => {
return (
<ListItemMobile rowGap={ 3 }>
<Flex w="100%">
<AddressEntity
isLoading={ isLoading }
address={ data.address }
query={{ tab: 'contract' }}
noCopy
/>
<Flex alignItems="center" overflow="hidden">
<AddressEntity
isLoading={ isLoading }
address={ data.address }
query={{ tab: 'contract' }}
noCopy
/>
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } mx={ 2 }/> }
</Flex>
<Skeleton isLoaded={ !isLoading } color="text_secondary" ml="auto">
<HashStringShorten hash={ data.address.hash } isTooltipDisabled/>
</Skeleton>
Expand Down
Loading

0 comments on commit 8f9971e

Please sign in to comment.