diff --git a/configs/envs/.env.eth_sepolia b/configs/envs/.env.eth_sepolia index 0330c5cadd..7e257c4d5b 100644 --- a/configs/envs/.env.eth_sepolia +++ b/configs/envs/.env.eth_sepolia @@ -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 diff --git a/icons/verified_token.svg b/icons/certified.svg similarity index 100% rename from icons/verified_token.svg rename to icons/certified.svg diff --git a/mocks/contract/info.ts b/mocks/contract/info.ts index 4ff831e168..99f87f8742 100644 --- a/mocks/contract/info.ts +++ b/mocks/contract/info.ts @@ -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', diff --git a/mocks/contracts/index.ts b/mocks/contracts/index.ts index bc3b4ecfb2..e5bf5e6de1 100644 --- a/mocks/contracts/index.ts +++ b/mocks/contracts/index.ts @@ -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, diff --git a/mocks/search/index.ts b/mocks/search/index.ts index 571eb24976..34015b5157 100644 --- a/mocks/search/index.ts +++ b/mocks/search/index.ts @@ -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', diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index 82909a5a3b..01a59cb9d7 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -24,6 +24,7 @@ | "brands/safe" | "brands/solidity_scan" | "burger" + | "certified" | "check" | "clock-light" | "clock" @@ -146,7 +147,6 @@ | "user_op_slim" | "user_op" | "validator" - | "verified_token" | "verified" | "verify-contract" | "wallet" diff --git a/types/api/contract.ts b/types/api/contract.ts index 50f4098d27..41fa0b5c4f 100644 --- a/types/api/contract.ts +++ b/types/api/contract.ts @@ -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 = [ diff --git a/types/api/contracts.ts b/types/api/contracts.ts index 65fe537568..75998004e9 100644 --- a/types/api/contracts.ts +++ b/types/api/contracts.ts @@ -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'; diff --git a/types/api/search.ts b/types/api/search.ts index bb9330a8a0..7d738e7cbd 100644 --- a/types/api/search.ts +++ b/types/api/search.ts @@ -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; diff --git a/ui/address/contract/ContractCode.pw.tsx b/ui/address/contract/ContractCode.pw.tsx index 3bcb857674..86fe6eef5c 100644 --- a/ui/address/contract/ContractCode.pw.tsx +++ b/ui/address/contract/ContractCode.pw.tsx @@ -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(, { 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(, { hooksConfig }, { withSocket: true }); diff --git a/ui/address/contract/ContractCode.tsx b/ui/address/contract/ContractCode.tsx index fddb9c8fd8..4403b3633f 100644 --- a/ui/address/contract/ContractCode.tsx +++ b/ui/address/contract/ContractCode.tsx @@ -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'; @@ -195,6 +196,13 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => { return null; })(); + const contractNameWithCertifiedIcon = data?.is_verified ? ( + + { data.name } + { data.certified && } + + ) : null; + return ( <> @@ -248,7 +256,7 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => { { data?.is_verified && ( - { data.name && } + { data.name && } { data.compiler_version && } { data.evm_version && } { licenseLink && ( diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-certified-icon-mobile-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-certified-icon-mobile-1.png new file mode 100644 index 0000000000..f9660a84e5 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-certified-icon-mobile-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-certified-icon-mobile-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-certified-icon-mobile-1.png new file mode 100644 index 0000000000..964b95edd1 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-certified-icon-mobile-1.png differ diff --git a/ui/pages/SearchResults.pw.tsx b/ui/pages/SearchResults.pw.tsx index e496266474..4d4881a5dc 100644 --- a/ui/pages/SearchResults.pw.tsx +++ b/ui/pages/SearchResults.pw.tsx @@ -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 } }); diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-address-hash-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-address-hash-mobile-1.png index 5c101dfdfb..2158de6556 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-address-hash-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-address-hash-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-address-hash-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-address-hash-mobile-1.png index 7fc3218999..46092da7a2 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-address-hash-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-address-hash-mobile-1.png differ diff --git a/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png index 19ed4740df..f93346107e 100644 Binary files a/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png index 509e3a8b7b..5ea07a2053 100644 Binary files a/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/searchResults/SearchResultListItem.tsx b/ui/searchResults/SearchResultListItem.tsx index d09cb77983..38e12e81db 100644 --- a/ui/searchResults/SearchResultListItem.tsx +++ b/ui/searchResults/SearchResultListItem.tsx @@ -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'; @@ -69,7 +70,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { textOverflow="ellipsis" /> - { data.is_verified_via_admin_panel && } + { data.is_verified_via_admin_panel && } ); } @@ -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 ? ( - <> - - { data.ens_info && - ( + + + + { data.ens_info && ( data.ens_info.names_count > 1 ? ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : { expiresText } - ) - } - + ) } + + { data.certified && } + ) : null; } diff --git a/ui/searchResults/SearchResultTableItem.tsx b/ui/searchResults/SearchResultTableItem.tsx index d9a1cf6d74..1fa0c61646 100644 --- a/ui/searchResults/SearchResultTableItem.tsx +++ b/ui/searchResults/SearchResultTableItem.tsx @@ -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'; @@ -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; @@ -69,7 +71,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }} /> - { data.is_verified_via_admin_panel && } + { data.is_verified_via_admin_panel && } @@ -128,14 +130,24 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { { addressName && ( - - { data.ens_info && - ( - data.ens_info.names_count > 1 ? - ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : - { expiresText } - ) - } + + + + { data.ens_info && ( + data.ens_info.names_count > 1 ? ( + + { data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` } + + ) : + { expiresText } + ) } + + { data.certified && } + ) } diff --git a/ui/shared/ContractCertifiedLabel.tsx b/ui/shared/ContractCertifiedLabel.tsx new file mode 100644 index 0000000000..98e79f7cbb --- /dev/null +++ b/ui/shared/ContractCertifiedLabel.tsx @@ -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 ( + + + + + + ); +}; + +export default chakra(ContractCertifiedLabel); diff --git a/ui/shared/Page/specs/DefaultView.tsx b/ui/shared/Page/specs/DefaultView.tsx index 0c117d1411..b347b5e32f 100644 --- a/ui/shared/Page/specs/DefaultView.tsx +++ b/ui/shared/Page/specs/DefaultView.tsx @@ -32,7 +32,7 @@ const DefaultView = () => { const contentAfter = ( <> - + { const contentAfter = ( <> - + { const apiUrl = await mockApiResponse('quick_search', [ searchMock.contract1, + searchMock.contract2, searchMock.address2, ], { queryParams: { q: 'o' } }); diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx index 0a69e99525..d1fe5b0ece 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx @@ -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'; @@ -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 && ( - - - { data.ens_info && - ( + + + + { data.ens_info && ( data.ens_info.names_count > 1 ? ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : { expiresText } - ) - } - + ) } + + { data.certified && } + ); const addressEl = ; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx index 7635e40ddd..67e31f239c 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx @@ -16,7 +16,7 @@ interface Props { const SearchBarSuggestToken = ({ data, isMobile, searchTerm }: Props) => { const icon = ; - const verifiedIcon = ; + const verifiedIcon = ; const name = ( { { verifiedInfoQuery.data?.tokenAddress && ( - + ) } diff --git a/ui/verifiedContracts/VerifiedContractsListItem.tsx b/ui/verifiedContracts/VerifiedContractsListItem.tsx index 04765654a5..c0830fd8db 100644 --- a/ui/verifiedContracts/VerifiedContractsListItem.tsx +++ b/ui/verifiedContracts/VerifiedContractsListItem.tsx @@ -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'; @@ -36,12 +37,15 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => { return ( - + + + { data.certified && } + diff --git a/ui/verifiedContracts/VerifiedContractsTableItem.tsx b/ui/verifiedContracts/VerifiedContractsTableItem.tsx index 3bafd4446e..5ba5053b5a 100644 --- a/ui/verifiedContracts/VerifiedContractsTableItem.tsx +++ b/ui/verifiedContracts/VerifiedContractsTableItem.tsx @@ -7,6 +7,7 @@ import type { VerifiedContract } from 'types/api/contracts'; import config from 'configs/app'; import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import dayjs from 'lib/date/dayjs'; +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'; @@ -34,13 +35,15 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => { return ( - + + + { data.certified && } +