Commit f58a3e9c authored by tom goriunov's avatar tom goriunov Committed by GitHub

Add support for zkSync smart-contracts (#2173)

* additional form fields

* pass optimization modes from API and handle single verification method case

* display zk contract info
parent 5df91795
......@@ -10,6 +10,8 @@ NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
# Instance ENVs
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=zksync.blockscout.com
......
......@@ -99,6 +99,13 @@ export const withChangedByteCode: SmartContract = {
is_blueprint: true,
};
export const zkSync: SmartContract = {
...verified,
zk_compiler_version: 'v1.2.5',
optimization_enabled: true,
optimization_runs: 's',
};
export const nonVerified: SmartContract = {
is_verified: false,
is_blueprint: false,
......
......@@ -37,6 +37,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
zkSyncRollup: [
[ 'NEXT_PUBLIC_ROLLUP_TYPE', 'zkSync' ],
[ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ],
[ 'NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS', 'none' ],
],
bridgedTokens: [
[ 'NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', '[{"id":"1","title":"Ethereum","short_title":"ETH","base_url":"https://eth.blockscout.com/token/"},{"id":"56","title":"Binance Smart Chain","short_title":"BSC","base_url":"https://bscscan.com/token/"},{"id":"99","title":"POA","short_title":"POA","base_url":"https://blockscout.com/poa/core/token/"}]' ],
......
......@@ -27,7 +27,7 @@ export interface SmartContract {
compiler_version: string | null;
evm_version: string | null;
optimization_enabled: boolean | null;
optimization_runs: number | null;
optimization_runs: number | string | null;
name: string | null;
verified_at: string | null;
is_blueprint: boolean | null;
......@@ -57,6 +57,7 @@ export interface SmartContract {
language: string | null;
license_type: SmartContractLicenseType | null;
certified?: boolean;
zk_compiler_version?: string;
}
export type SmartContractDecodedConstructorArg = [
......@@ -86,6 +87,8 @@ export interface SmartContractVerificationConfigRaw {
vyper_evm_versions: Array<string>;
is_rust_verifier_microservice_enabled: boolean;
license_types: Record<SmartContractLicenseType, number>;
zk_compiler_versions?: Array<string>;
zk_optimization_modes?: Array<string>;
}
export type SmartContractVerificationResponse = {
......
......@@ -13,6 +13,7 @@ export interface VerifiedContract {
verified_at: string;
market_cap: string | null;
license_type: SmartContractLicenseType | null;
zk_compiler_version?: string;
}
export interface VerifiedContractsResponse {
......
......@@ -126,6 +126,14 @@ test('non verified', async({ render, mockApiResponse }) => {
await expect(component).toHaveScreenshot();
});
test('zkSync contract', async({ render, mockApiResponse, page, mockEnvs }) => {
await mockEnvs(ENVS_MAP.zkSyncRollup);
await mockApiResponse('contract', contractMock.zkSync, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } });
});
test.describe('with audits feature', () => {
test.beforeEach(async({ mockEnvs }) => {
......
......@@ -60,6 +60,8 @@ const InfoItem = chakra(({ label, content, hint, className, isLoading }: InfoIte
</GridItem>
));
const rollupFeature = config.features.rollup;
const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
......@@ -266,6 +268,7 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" content={ contractNameWithCertifiedIcon } isLoading={ isPlaceholderData }/> }
{ data.compiler_version && <InfoItem label="Compiler version" content={ data.compiler_version } isLoading={ isPlaceholderData }/> }
{ data.zk_compiler_version && <InfoItem label="ZK compiler version" content={ data.zk_compiler_version } isLoading={ isPlaceholderData }/> }
{ data.evm_version && <InfoItem label="EVM version" content={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ licenseLink && (
<InfoItem
......@@ -277,8 +280,13 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
) }
{ typeof data.optimization_enabled === 'boolean' &&
<InfoItem label="Optimization enabled" content={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ data.optimization_runs !== null &&
<InfoItem label="Optimization runs" content={ String(data.optimization_runs) } isLoading={ isPlaceholderData }/> }
{ data.optimization_runs !== null && (
<InfoItem
label={ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' ? 'Optimization mode' : 'Optimization runs' }
content={ String(data.optimization_runs) }
isLoading={ isPlaceholderData }
/>
) }
{ data.verified_at &&
<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 }/> }
......
......@@ -2,6 +2,7 @@ import React from 'react';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import * as socketServer from 'playwright/fixtures/socketServer';
import { test, expect } from 'playwright/lib';
......@@ -215,3 +216,17 @@ test('solidity-foundry method', async({ render, page }) => {
await expect(component).toHaveScreenshot();
});
test('verification of zkSync contract', async({ render, mockEnvs }) => {
const zkSyncFormConfig: SmartContractVerificationConfig = {
...formConfig,
verification_options: [ 'standard-input' ],
zk_compiler_versions: [ 'v1.4.1', 'v1.4.0', 'v1.3.23', 'v1.3.22' ],
zk_optimization_modes: [ '0', '1', '2', '3', 's', 'z' ],
};
await mockEnvs(ENVS_MAP.zkSyncRollup);
const component = await render(<ContractVerificationForm config={ zkSyncFormConfig } hash={ hash }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
......@@ -41,7 +41,7 @@ interface Props {
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({
mode: 'onBlur',
defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config, hash, null) : undefined,
defaultValues: getDefaultValues(methodFromQuery, config, hash, null),
});
const { control, handleSubmit, watch, formState, setError, reset, getFieldState } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
......
......@@ -4,14 +4,15 @@ import React from 'react';
interface Props {
title: string;
children: React.ReactNode;
disableScroll?: boolean;
}
const ContractVerificationMethod = ({ title, children }: Props) => {
const ContractVerificationMethod = ({ title, children, disableScroll }: Props) => {
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
ref.current?.scrollIntoView({ behavior: 'smooth' });
}, []);
!disableScroll && ref.current?.scrollIntoView({ behavior: 'smooth' });
}, [ disableScroll ]);
return (
<section ref={ ref }>
......
......@@ -51,6 +51,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
isDisabled={ isDisabled }
isRequired
isAsync={ false }
isReadOnly={ options.length === 1 }
/>
);
}, [ isDisabled, isMobile, options ]);
......
import { Box, Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import IconSvg from 'ui/shared/IconSvg';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const OPTIONS_LIMIT = 50;
const ContractVerificationFieldZkCompiler = () => {
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const options = React.useMemo(() => (
config?.zk_compiler_versions?.map((option) => ({ label: option, value: option })) || []
), [ config?.zk_compiler_versions ]);
const loadOptions = React.useCallback(async(inputValue: string) => {
return options
.filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase()))
.slice(0, OPTIONS_LIMIT);
}, [ options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'zk_compiler'>}) => {
const error = 'zk_compiler' in formState.errors ? formState.errors.zk_compiler : undefined;
return (
<FancySelect
{ ...field }
loadOptions={ loadOptions }
defaultOptions
size={ isMobile ? 'md' : 'lg' }
placeholder="ZK compiler (enter version or use the dropdown)"
placeholderIcon={ <IconSvg name="search"/> }
isDisabled={ formState.isSubmitting }
error={ error }
isRequired
isAsync
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, loadOptions ]);
return (
<ContractVerificationFormRow>
<Controller
name="zk_compiler"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
<Box>
<Link isExternal href="https://docs.zksync.io/zk-stack/components/compiler/specification#glossary">zksolc</Link>
<span> compiler version.</span>
</Box>
</ContractVerificationFormRow>
);
};
export default React.memo(ContractVerificationFieldZkCompiler);
import { Flex, Select } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import CheckboxInput from 'ui/shared/CheckboxInput';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props {
config: SmartContractVerificationConfig;
}
const ContractVerificationFieldZkOptimization = ({ config }: Props) => {
const [ isEnabled, setIsEnabled ] = React.useState(false);
const { formState, control } = useFormContext<FormFields>();
const error = 'optimization_mode' in formState.errors ? formState.errors.optimization_mode : undefined;
const handleCheckboxChange = React.useCallback(() => {
setIsEnabled(prev => !prev);
}, []);
const renderCheckboxControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_optimization_enabled'>}) => (
<Flex flexShrink={ 0 }>
<CheckboxInput<FormFields, 'is_optimization_enabled'>
text="Optimization enabled"
field={ field }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
/>
</Flex>
), [ formState.isSubmitting, handleCheckboxChange ]);
const renderInputControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'optimization_mode'>}) => {
return (
<Select
size="xs"
{ ...field }
w="auto"
borderRadius="base"
isDisabled={ formState.isSubmitting }
placeholder="Optimization mode"
isInvalid={ Boolean(error) }
>
{ config.zk_optimization_modes?.map((value) => (
<option key={ value } value={ value }>
{ value }
</option>
)) }
</Select>
);
}, [ config.zk_optimization_modes, error, formState.isSubmitting ]);
return (
<ContractVerificationFormRow>
<Flex columnGap={ 5 } rowGap={ 2 } h={{ base: 'auto', lg: '32px' }} flexDir={{ base: 'column', lg: 'row' }}>
<Controller
name="is_optimization_enabled"
control={ control }
render={ renderCheckboxControl }
/>
{ isEnabled && (
<Controller
name="optimization_mode"
control={ control }
render={ renderInputControl }
/>
) }
</Flex>
</ContractVerificationFormRow>
);
};
export default React.memo(ContractVerificationFieldZkOptimization);
......@@ -2,25 +2,32 @@ import React from 'react';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import config from 'configs/app';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
import ContractVerificationFieldZkCompiler from '../fields/ContractVerificationFieldZkCompiler';
import ContractVerificationFieldZkOptimization from '../fields/ContractVerificationFieldZkOptimization';
const FILE_TYPES = [ '.json' as const ];
const rollupFeature = config.features.rollup;
const ContractVerificationStandardInput = ({ config }: { config: SmartContractVerificationConfig }) => {
return (
<ContractVerificationMethod title="Contract verification via Solidity (standard JSON input) ">
<ContractVerificationMethod title="Contract verification via Solidity (standard JSON input) " disableScroll={ config.verification_options.length === 1 }>
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldName/> }
<ContractVerificationFieldCompiler/>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && <ContractVerificationFieldZkCompiler/> }
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
title="Standard Input JSON"
hint="Upload the standard input JSON file created during contract compilation."
required
/>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && <ContractVerificationFieldZkOptimization config={ config }/> }
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldAutodetectArgs/> }
</ContractVerificationMethod>
);
......
......@@ -44,6 +44,20 @@ export interface FormFieldsStandardInput {
license_type: LicenseOption | null;
}
export interface FormFieldsStandardInputZk {
address: string;
method: MethodOption;
name: string;
compiler: Option | null;
zk_compiler: Option | null;
sources: Array<File>;
autodetect_constructor_args: boolean;
constructor_args: string;
license_type: LicenseOption | null;
is_optimization_enabled: boolean;
optimization_mode: string | undefined;
}
export interface FormFieldsSourcify {
address: string;
method: MethodOption;
......@@ -93,5 +107,5 @@ export interface FormFieldsVyperStandardInput {
license_type: LicenseOption | null;
}
export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify |
export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsStandardInputZk | FormFieldsSourcify |
FormFieldsMultiPartFile | FormFieldsVyperContract | FormFieldsVyperMultiPartFile | FormFieldsVyperStandardInput;
......@@ -7,6 +7,7 @@ import type {
FormFieldsMultiPartFile,
FormFieldsSourcify,
FormFieldsStandardInput,
FormFieldsStandardInputZk,
FormFieldsVyperContract,
FormFieldsVyperMultiPartFile,
FormFieldsVyperStandardInput,
......@@ -155,11 +156,18 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
};
export function getDefaultValues(
method: SmartContractVerificationMethod,
methodParam: SmartContractVerificationMethod | undefined,
config: SmartContractVerificationConfig,
hash: string | undefined,
licenseType: FormFields['license_type'],
) {
const singleMethod = config.verification_options.length === 1 ? config.verification_options[0] : undefined;
const method = singleMethod || methodParam;
if (!method) {
return;
}
const defaultValues: FormFields = { ...DEFAULT_VALUES[method], address: hash || '', license_type: licenseType };
if ('evm_version' in defaultValues) {
......@@ -179,6 +187,13 @@ export function getDefaultValues(
}
}
if (singleMethod) {
defaultValues.method = {
label: METHOD_LABELS[config.verification_options[0]],
value: config.verification_options[0],
};
}
return defaultValues;
}
......@@ -223,7 +238,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
}
case 'standard-input': {
const _data = data as FormFieldsStandardInput;
const _data = data as (FormFieldsStandardInput | FormFieldsStandardInputZk);
const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value);
......@@ -233,6 +248,15 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
body.set('constructor_args', _data.constructor_args);
addFilesToFormData(body, _data.sources, 'files');
// zkSync fields
'zk_compiler' in _data && _data.zk_compiler && body.set('zk_compiler_version', _data.zk_compiler.value);
if ('is_optimization_enabled' in _data) {
body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled)));
if (_data.is_optimization_enabled && 'optimization_mode' in _data && _data.optimization_mode) {
body.set('optimization_runs', _data.optimization_mode);
}
}
return body;
}
......
......@@ -70,6 +70,14 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => {
<Box color="text_secondary" wordBreak="break-all" whiteSpace="pre-wrap"> ({ data.compiler_version })</Box>
</Skeleton>
</Flex>
{ data.zk_compiler_version && (
<Flex columnGap={ 3 }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 } flexShrink="0">ZK compiler</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary" wordBreak="break-all" whiteSpace="pre-wrap">
{ data.zk_compiler_version }
</Skeleton>
</Flex>
) }
<Flex columnGap={ 3 }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Optimization</Skeleton>
{ data.optimization_enabled ?
......
......@@ -70,6 +70,14 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => {
</Tooltip>
</Skeleton>
</Flex>
{ data.zk_compiler_version && (
<Flex flexWrap="wrap" columnGap={ 2 } my={ 1 }>
<Skeleton isLoaded={ !isLoading } >ZK compiler</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary" wordBreak="break-all">
<span>{ data.zk_compiler_version }</span>
</Skeleton>
</Flex>
) }
</Td>
<Td>
<Tooltip label={ isLoading ? undefined : 'Optimization' }>
......
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