Skip to content

Commit

Permalink
Merge pull request #1598 from blockscout/fe-1563
Browse files Browse the repository at this point in the history
audits info and form
  • Loading branch information
isstuev authored Feb 19, 2024
2 parents 2139c6c + 6e118f2 commit 5c2d06f
Show file tree
Hide file tree
Showing 31 changed files with 780 additions and 39 deletions.
1 change: 1 addition & 0 deletions configs/app/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const UI = Object.freeze({
ides: {
items: parseEnvJson<Array<ContractCodeIde>>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [],
},
hasContractAuditReports: getEnvValue('NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS') === 'true' ? true : false,
});

export default UI;
3 changes: 2 additions & 1 deletion configs/envs/.env.eth_goerli
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-c
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true

# app features
NEXT_PUBLIC_APP_ENV=development
Expand All @@ -54,4 +55,4 @@ NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]

#meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ const schema = yup
.transform(replaceQuotes)
.json()
.of(contractCodeIdeSchema),
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: yup.boolean(),
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(),
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(),
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(),
Expand Down
2 changes: 2 additions & 0 deletions deploy/values/review/values.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ frontend:
NEXT_PUBLIC_HAS_USER_OPS: true
NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]"
NEXT_PUBLIC_SWAP_BUTTON_URL: uniswap
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true
envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
Expand All @@ -88,3 +89,4 @@ frontend:
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN

1 change: 1 addition & 0 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ Settings for meta tags and OG tags
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
| NEXT_PUBLIC_CONTRACT_CODE_IDES | `Array<ContractCodeIde>` where `ContractCodeIde` can have following [properties](#contract-code-ide-configuration-properties) | Used to build up links to IDEs with contract source code. | - | - | `[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}]` |
| NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS | `boolean` | Set to `true` to enable Submit Audit form on the contract page | - | `false` | `true` |
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS | `boolean` | Set to `true` to hide indexing alert in the page header about indexing chain's blocks | - | `false` | `true` |
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` |
| NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` |
Expand Down
14 changes: 13 additions & 1 deletion lib/api/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { BackendVersionConfig } from 'types/api/configs';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig, SolidityscanReport } from 'types/api/contract';
import type {
SmartContract,
SmartContractReadMethod,
SmartContractWriteMethod,
SmartContractVerificationConfig,
SolidityscanReport,
SmartContractSecurityAudits,
} from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
import type {
EnsAddressLookupFilters,
Expand Down Expand Up @@ -434,6 +441,10 @@ export const RESOURCES = {
path: '/api/v2/smart-contracts/:hash/solidityscan-report',
pathParams: [ 'hash' as const ],
},
contract_security_audits: {
path: '/api/v2/smart-contracts/:hash/audit-reports',
pathParams: [ 'hash' as const ],
},

verified_contracts: {
path: '/api/v2/smart-contracts',
Expand Down Expand Up @@ -844,6 +855,7 @@ Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse :
Q extends 'shibarium_deposits' ? ShibariumDepositsResponse :
Q extends 'shibarium_withdrawals_count' ? number :
Q extends 'shibarium_deposits_count' ? number :
Q extends 'contract_security_audits' ? SmartContractSecurityAudits :
never;
/* eslint-enable @typescript-eslint/indent */

Expand Down
16 changes: 16 additions & 0 deletions mocks/contract/audits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { SmartContractSecurityAudits } from 'types/api/contract';

export const contractAudits: SmartContractSecurityAudits = {
items: [
{
audit_company_name: 'OpenZeppelin',
audit_publish_date: '2023-03-01',
audit_report_url: 'https://blog.openzeppelin.com/eip-4337-ethereum-account-abstraction-incremental-audit',
},
{
audit_company_name: 'OpenZeppelin',
audit_publish_date: '2023-03-01',
audit_report_url: 'https://blog.openzeppelin.com/eip-4337-ethereum-account-abstraction-incremental-audit',
},
],
};
6 changes: 6 additions & 0 deletions playwright/utils/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ export const viewsEnvs = {
},
};

export const UIEnvs = {
hasContractAuditReports: [
{ name: 'NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS', value: 'true' },
],
};

export const stabilityEnvs = [
{ name: 'NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS', value: '["top_accounts"]' },
{ name: 'NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS', value: '["value","fee_currency","gas_price","gas_fees","burnt_fees"]' },
Expand Down
4 changes: 2 additions & 2 deletions theme/components/FormLabel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { getColor, mode } from '@chakra-ui/theme-tools';
import { getColor } from '@chakra-ui/theme-tools';

import getDefaultFormColors from '../utils/getDefaultFormColors';

Expand All @@ -20,7 +20,7 @@ const baseStyle = defineStyle({
const variantFloating = defineStyle((props) => {
const { theme, backgroundColor } = props;
const { focusPlaceholderColor } = getDefaultFormColors(props);
const bc = backgroundColor || mode('white', 'black')(props);
const bc = backgroundColor || 'transparent';

return {
left: '2px',
Expand Down
7 changes: 7 additions & 0 deletions theme/utils/getOutlinedFieldStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
},
// not filled input
':placeholder-shown:not(:focus-visible):not(:hover):not([aria-invalid=true])': { borderColor: borderColor || mode('gray.100', 'gray.700')(props) },

// not filled input with type="date"
':not(:placeholder-shown)[value=""]:not(:focus-visible):not(:hover):not([aria-invalid=true])': {
borderColor: borderColor || mode('gray.100', 'gray.700')(props),
color: 'gray.500',
},

':-webkit-autofill': { transition: 'background-color 5000s ease-in-out 0s' },
':-webkit-autofill:hover': { transition: 'background-color 5000s ease-in-out 0s' },
':-webkit-autofill:focus': { transition: 'background-color 5000s ease-in-out 0s' },
Expand Down
23 changes: 23 additions & 0 deletions types/api/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,26 @@ export type SolidityscanReport = {
scanner_reference_url: string;
};
}

type SmartContractSecurityAudit = {
audit_company_name: string;
audit_publish_date: string;
audit_report_url: string;
}

export type SmartContractSecurityAudits = {
items: Array<SmartContractSecurityAudit>;
}

export type SmartContractSecurityAuditSubmission = {
'address_hash': string;
'submitter_name': string;
'submitter_email': string;
'is_project_owner': boolean;
'project_name': string;
'project_url': string;
'audit_company_name': string;
'audit_report_url': string;
'audit_publish_date': string;
'comment'?: string;
}
55 changes: 55 additions & 0 deletions ui/address/contract/ContractCode.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';

import * as addressMock from 'mocks/address/address';
import { contractAudits } from 'mocks/contract/audits';
import * as contractMock from 'mocks/contract/info';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import MockAddressPage from 'ui/address/testUtils/MockAddressPage';

import ContractCode from './ContractCode';

const addressHash = 'hash';
const CONTRACT_API_URL = buildApiUrl('contract', { hash: addressHash });
const CONTRACT_AUDITS_API_URL = buildApiUrl('contract_security_audits', { hash: addressHash });
const hooksConfig = {
router: {
query: { hash: addressHash },
Expand Down Expand Up @@ -229,3 +233,54 @@ test('non verified', async({ mount, page }) => {

await expect(component).toHaveScreenshot();
});

test.describe('with audits feature', () => {

const withAuditsTest = test.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.UIEnvs.hasContractAuditReports) as any,
});

withAuditsTest('no audits', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.verified),
}));
await page.route(CONTRACT_AUDITS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [] }),
}));
await page.route('https://cdn.jsdelivr.net/npm/[email protected]/**', (route) => route.abort());

const component = await mount(
<TestApp>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);

await expect(component).toHaveScreenshot();
});

withAuditsTest('has audits', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.verified),
}));
await page.route(CONTRACT_AUDITS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractAudits),
}));

await page.route('https://cdn.jsdelivr.net/npm/[email protected]/**', (route) => route.abort());

const component = await mount(
<TestApp>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);

await expect(component).toHaveScreenshot();
});
});
34 changes: 25 additions & 9 deletions ui/address/contract/ContractCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Address as AddressInfo } from 'types/api/address';

import { route } from 'nextjs-routes';

import config from 'configs/app';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import useSocketChannel from 'lib/socket/useSocketChannel';
Expand All @@ -18,6 +19,7 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import RawDataSnippet from 'ui/shared/RawDataSnippet';

import ContractSecurityAudits from './ContractSecurityAudits';
import ContractSourceCode from './ContractSourceCode';

type Props = {
Expand All @@ -26,10 +28,17 @@ type Props = {
noSocket?: boolean;
}

const InfoItem = chakra(({ label, value, className, isLoading }: { label: string; value: string; className?: string; isLoading: boolean }) => (
type InfoItemProps = {
label: string;
content: string | React.ReactNode;
className?: string;
isLoading: boolean;
}

const InfoItem = chakra(({ label, content, className, isLoading }: InfoItemProps) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className } alignItems="baseline">
<Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ value }</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ content }</Skeleton>
</GridItem>
));

Expand Down Expand Up @@ -221,15 +230,22 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
</Flex>
{ data?.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name } isLoading={ isPlaceholderData }/> }
{ data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version } isLoading={ isPlaceholderData }/> }
{ data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ data.name && <InfoItem label="Contract name" content={ data.name } 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 }/> }
{ typeof data.optimization_enabled === 'boolean' &&
<InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) } isLoading={ isPlaceholderData }/> }
<InfoItem label="Optimization enabled" content={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ data.optimization_runs && <InfoItem label="Optimization runs" content={ String(data.optimization_runs) } isLoading={ isPlaceholderData }/> }
{ data.verified_at &&
<InfoItem label="Verified at" value={ dayjs(data.verified_at).format('llll') } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
{ data.file_path && <InfoItem label="Contract file path" value={ data.file_path } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
<InfoItem label="Verified at" content={ dayjs(data.verified_at).format('llll') } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
{ data.file_path && <InfoItem label="Contract file path" content={ data.file_path } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
{ config.UI.hasContractAuditReports && (
<InfoItem
label="Security audit"
content={ <ContractSecurityAudits addressHash={ addressHash }/> }
isLoading={ isPlaceholderData }
/>
) }
</Grid>
) }
<Flex flexDir="column" rowGap={ 6 }>
Expand Down
81 changes: 81 additions & 0 deletions ui/address/contract/ContractSecurityAudits.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Box, Button, useDisclosure } from '@chakra-ui/react';
import React from 'react';

import type { SmartContractSecurityAuditSubmission } from 'types/api/contract';

import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
import FormModal from 'ui/shared/FormModal';
import LinkExternal from 'ui/shared/LinkExternal';

import ContractSubmitAuditForm from './contractSubmitAuditForm/ContractSubmitAuditForm';

const SCROLL_GRADIENT_HEIGHT = 24;

type Props = {
addressHash?: string;
}

const ContractSecurityAudits = ({ addressHash }: Props) => {
const { data, isPlaceholderData } = useApiQuery('contract_security_audits', {
pathParams: { hash: addressHash },
queryOptions: {
refetchOnMount: false,
placeholderData: { items: [] },
enabled: Boolean(addressHash),
},
});

const containerRef = React.useRef<HTMLDivElement>(null);
const [ hasScroll, setHasScroll ] = React.useState(false);

React.useEffect(() => {
if (!containerRef.current) {
return;
}

setHasScroll(containerRef.current.scrollHeight >= containerRef.current.clientHeight + SCROLL_GRADIENT_HEIGHT / 2);
}, []);

const formTitle = 'Submit audit';

const modalProps = useDisclosure();

const renderForm = React.useCallback(() => {
return <ContractSubmitAuditForm address={ addressHash } onSuccess={ modalProps.onClose }/>;
}, [ addressHash, modalProps.onClose ]);

return (
<>
<Button variant="outline" size="sm" onClick={ modalProps.onOpen }>Submit audit</Button>
{ data?.items && data.items.length > 0 && (
<Box position="relative">
<ContainerWithScrollY
gradientHeight={ SCROLL_GRADIENT_HEIGHT }
hasScroll={ hasScroll }
rowGap={ 1 }
w="100%"
maxH="80px"
ref={ containerRef }
mt={ 2 }
>
{ data.items.map(item => (
<LinkExternal href={ item.audit_report_url } key={ item.audit_company_name + item.audit_publish_date } isLoading={ isPlaceholderData }>
{ `${ item.audit_company_name }, ${ dayjs(item.audit_publish_date).format('MMM DD, YYYY') }` }
</LinkExternal>
)) }
</ContainerWithScrollY>
</Box>
) }
<FormModal<SmartContractSecurityAuditSubmission>
isOpen={ modalProps.isOpen }
onClose={ modalProps.onClose }
title={ formTitle }
renderForm={ renderForm }
/>
</>
);
};

export default React.memo(ContractSecurityAudits);
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

0 comments on commit 5c2d06f

Please sign in to comment.