Commit 05e381ca authored by tom goriunov's avatar tom goriunov Committed by GitHub

vyper contract verification: bugfix and improvements (#896)

* change hint and enable contract name field

* fix form re-submiting

* update screenshot

* change vyper multi part method

* vyper standard input json method

* fix required error message for sources field

* pin api host

* fix locked form

* clean up
parent e07f48d0
......@@ -5,6 +5,7 @@ import React from 'react';
import getSeo from 'lib/next/address/getSeo';
import ContractVerification from 'ui/pages/ContractVerification';
import Page from 'ui/shared/Page/Page';
const ContractVerificationPage: NextPage<RoutedQuery<'/address/[hash]/contract_verification'>> =
({ hash }: RoutedQuery<'/address/[hash]/contract_verification'>) => {
......@@ -16,7 +17,9 @@ const ContractVerificationPage: NextPage<RoutedQuery<'/address/[hash]/contract_v
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<ContractVerification/>
<Page>
<ContractVerification/>
</Page>
</>
);
};
......
......@@ -120,7 +120,8 @@ export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess |
// VERIFICATION
export type SmartContractVerificationMethod = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part' | 'vyper-code' | 'vyper-multi-part';
export type SmartContractVerificationMethod = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part'
| 'vyper-code' | 'vyper-multi-part' | 'vyper-standard-input';
export interface SmartContractVerificationConfigRaw {
solidity_compiler_versions: Array<string>;
......@@ -145,6 +146,7 @@ export type SmartContractVerificationResponse = {
export interface SmartContractVerificationError {
contract_source_code?: Array<string>;
files?: Array<string>;
interfaces?: Array<string>;
compiler_version?: Array<string>;
constructor_arguments?: Array<string>;
name?: Array<string>;
......
......@@ -37,6 +37,7 @@ const formConfig: SmartContractVerificationConfig = {
'multi-part',
'vyper-code',
'vyper-multi-part',
'vyper-standard-input',
],
vyper_compiler_versions: [
'v0.3.7+commit.6020b8bb',
......@@ -181,3 +182,18 @@ test('vyper multi-part method', async({ mount, page }) => {
await expect(component).toHaveScreenshot();
});
test('vyper vyper-standard-input method', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>,
{ hooksConfig },
);
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('vyper');
await page.getByRole('button', { name: /standard json input/i }).click();
await expect(component).toHaveScreenshot();
});
......@@ -9,6 +9,7 @@ import type { SocketMessage } from 'lib/socket/types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import useApiFetch from 'lib/api/useApiFetch';
import delay from 'lib/delay';
import useToast from 'lib/hooks/useToast';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -20,6 +21,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import ContractVerificationVyperStandardInput from './methods/ContractVerificationVyperStandardInput';
import { prepareRequestBody, formatSocketErrors, getDefaultValues } from './utils';
interface Props {
......@@ -59,10 +61,11 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
});
}, [ apiFetch, hash ]);
const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback((payload) => {
const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback(async(payload) => {
if (payload.status === 'error') {
const errors = formatSocketErrors(payload.errors);
errors.forEach(([ field, error ]) => setError(field, error));
errors.filter(Boolean).forEach(([ field, error ]) => setError(field, error));
await delay(100); // have to wait a little bit, otherwise isSubmitting status will not be updated
submitPromiseResolver.current?.(null);
return;
}
......@@ -121,6 +124,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
'multi-part': <ContractVerificationMultiPartFile/>,
'vyper-code': <ContractVerificationVyperContract config={ config }/>,
'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
'vyper-standard-input': <ContractVerificationVyperStandardInput/>,
};
}, [ config ]);
const method = watch('method');
......
......@@ -53,7 +53,15 @@ const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
/>
<>
<span>The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version. </span>
<Link href="https://forum.poa.network/t/smart-contract-verification-evm-version-details/2318" target="_blank">EVM version details</Link>
<Link
href={ isVyper ?
'https://docs.vyperlang.org/en/stable/compiling-a-contract.html#target-options' :
'https://docs.soliditylang.org/en/latest/using-the-compiler.html#target-options'
}
target="_blank"
>
EVM version details
</Link>
</>
</ContractVerificationFormRow>
);
......
......@@ -82,6 +82,19 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
return <ListItem key={ method }>Verification of Vyper contract.</ListItem>;
case 'vyper-multi-part':
return <ListItem key={ method }>Verification of multi-part Vyper files.</ListItem>;
case 'vyper-standard-input':
return (
<ListItem key={ method }>
<span>Verification of Vyper contract using </span>
<Link
href="https://docs.vyperlang.org/en/stable/compiling-a-contract.html#compiler-input-and-output-json-description"
target="_blank"
>
Standard input JSON
</Link>
<span> file.</span>
</ListItem>
);
}
}, []);
......
......@@ -16,16 +16,26 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
type FileTypes = '.sol' | '.yul' | '.json' | '.vy'
interface Props {
name?: 'sources' | 'interfaces';
fileTypes: Array<FileTypes>;
multiple?: boolean;
required?: boolean;
title: string;
hint: string;
hint: string | React.ReactNode;
}
const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }: Props) => {
const ContractVerificationFieldSources = ({ fileTypes, multiple, required, title, hint, name = 'sources' }: Props) => {
const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>();
const error = 'sources' in formState.errors ? formState.errors.sources : undefined;
const error = (() => {
if (name === 'sources' && 'sources' in formState.errors) {
return formState.errors.sources;
}
if (name === 'interfaces' && 'interfaces' in formState.errors) {
return formState.errors.interfaces;
}
})();
const commonError = !error?.type?.startsWith('file_') ? error : undefined;
const fileError = error?.type?.startsWith('file_') ? error : undefined;
......@@ -34,12 +44,12 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }:
return;
}
const value = getValues('sources').slice();
const value = getValues(name).slice();
value.splice(index, 1);
setValue('sources', value);
clearErrors('sources');
setValue(name, value);
clearErrors(name);
}, [ getValues, clearErrors, setValue ]);
}, [ getValues, name, setValue, clearErrors ]);
const renderUploadButton = React.useCallback(() => {
return (
......@@ -79,11 +89,24 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }:
);
}, [ formState.isSubmitting, handleFileRemove, fileError ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, typeof name>}) => {
const hasValue = field.value && field.value.length > 0;
const errorElement = (() => {
if (commonError?.type === 'required') {
return <FieldError message="Field is required"/>;
}
if (commonError?.message) {
return <FieldError message={ commonError.message }/>;
}
return null;
})();
return (
<>
<FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
<FileInput<FormFields, typeof name> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
{ ({ onChange }) => (
<Flex
flexDir="column"
......@@ -97,12 +120,12 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }:
</Flex>
) }
</FileInput>
{ commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
{ errorElement }
</>
);
}, [ fileTypes, multiple, commonError, formState.isSubmitting, renderFiles, renderUploadButton ]);
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, typeof name>): Promise<ValidateResult> => {
if (Array.isArray(value)) {
const errorText = `Wrong file type. Allowed files types are ${ fileTypes.join(',') }.`;
const errors = value.map(({ name }) => fileTypes.some((ext) => name.endsWith(ext)) ? '' : errorText);
......@@ -113,7 +136,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }:
return true;
}, [ fileTypes ]);
const validateFileSize = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
const validateFileSize = React.useCallback(async(value: FieldPathValue<FormFields, typeof name>): Promise<ValidateResult> => {
if (Array.isArray(value)) {
const FILE_SIZE_LIMIT = 20 * Mb;
const errors = value.map(({ size }) => size > FILE_SIZE_LIMIT ? 'File is too big. Maximum size is 20 Mb.' : '');
......@@ -124,7 +147,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }:
return true;
}, []);
const validateQuantity = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
const validateQuantity = React.useCallback(async(value: FieldPathValue<FormFields, typeof name>): Promise<ValidateResult> => {
if (!multiple && Array.isArray(value) && value.length > 1) {
return 'You can upload only one file';
}
......@@ -133,18 +156,18 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }:
}, [ multiple ]);
const rules = React.useMemo(() => ({
required: true,
required,
validate: {
file_type: validateFileType,
file_size: validateFileSize,
quantity: validateQuantity,
},
}), [ validateFileSize, validateFileType, validateQuantity ]);
}), [ validateFileSize, validateFileType, validateQuantity, required ]);
return (
<ContractVerificationFormRow>
<Controller
name="sources"
name={ name }
control={ control }
render={ renderControl }
rules={ rules }
......
......@@ -18,6 +18,7 @@ const ContractVerificationMultiPartFile = () => {
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
multiple
required
title="Sources *.sol or *.yul files"
hint="Upload all Solidity or Yul contract source files."
/>
......
......@@ -12,6 +12,7 @@ const ContractVerificationSourcify = () => {
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
multiple
required
title="Sources and Metadata JSON"
hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation."
/>
......
......@@ -19,6 +19,7 @@ const ContractVerificationStandardInput = ({ config }: { config: SmartContractVe
fileTypes={ FILE_TYPES }
title="Standard Input JSON"
hint="Upload the standard input JSON file created during contract compilation."
required
/>
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldAutodetectArgs/> }
</ContractVerificationMethod>
......
......@@ -12,7 +12,7 @@ import ContractVerificationFieldName from '../fields/ContractVerificationFieldNa
const ContractVerificationVyperContract = ({ config }: { config: SmartContractVerificationConfig }) => {
return (
<ContractVerificationMethod title="Contract verification via Vyper (contract)">
<ContractVerificationFieldName hint="Must match the name specified in the code." isReadOnly/>
<ContractVerificationFieldName hint="The contract name is the name assigned to the verified contract in Blockscout."/>
<ContractVerificationFieldCompiler isVyper/>
{ config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldEvmVersion isVyper/> }
<ContractVerificationFieldCode isVyper/>
......
import { Link } from '@chakra-ui/react';
import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod';
......@@ -5,18 +6,39 @@ import ContractVerificationFieldCompiler from '../fields/ContractVerificationFie
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.vy' as const ];
const MAIN_SOURCES_TYPES = [ '.vy' as const ];
const INTERFACE_TYPES = [ '.vy' as const, '.json' as const ];
const ContractVerificationVyperMultiPartFile = () => {
const interfacesHint = (
<>
<span>Add any </span>
<Link href="https://docs.vyperlang.org/en/stable/interfaces.html" target="_blank">required interfaces</Link>
<span> for the main compiled contract.</span>
</>
);
return (
<ContractVerificationMethod title="Contract verification via Vyper (multi-part files)">
<ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldEvmVersion isVyper/>
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
name="sources"
fileTypes={ MAIN_SOURCES_TYPES }
title="Upload main *.vy source"
hint={ `
Primary compiled Vyper contract.
Only add the main contract here whose bytecode has been deployed, all other files can be uploaded to the interfaces box below.
` }
required
/>
<ContractVerificationFieldSources
name="interfaces"
fileTypes={ INTERFACE_TYPES }
multiple
title="Sources *.vy files"
hint="Upload all Vyper contract source files."
title="Interfaces (.vy or .json)"
hint={ interfacesHint }
/>
</ContractVerificationMethod>
);
......
import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const ];
const ContractVerificationVyperStandardInput = () => {
return (
<ContractVerificationMethod title="Contract verification via Vyper (standard JSON input) ">
<ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
title="Standard Input JSON"
hint="Upload the standard input JSON file created during contract compilation."
required
/>
</ContractVerificationMethod>
);
};
export default React.memo(ContractVerificationVyperStandardInput);
......@@ -64,7 +64,14 @@ export interface FormFieldsVyperMultiPartFile {
compiler: Option | null;
evm_version: Option | null;
sources: Array<File>;
interfaces: Array<File>;
}
export interface FormFieldsVyperStandardInput {
method: MethodOption;
compiler: Option | null;
sources: Array<File>;
}
export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify |
FormFieldsMultiPartFile | FormFieldsVyperContract | FormFieldsVyperMultiPartFile;
FormFieldsMultiPartFile | FormFieldsVyperContract | FormFieldsVyperMultiPartFile | FormFieldsVyperStandardInput;
......@@ -9,6 +9,7 @@ import type {
FormFieldsStandardInput,
FormFieldsVyperContract,
FormFieldsVyperMultiPartFile,
FormFieldsVyperStandardInput,
} from './types';
import type { SmartContractVerificationMethod, SmartContractVerificationError, SmartContractVerificationConfig } from 'types/api/contract';
......@@ -21,6 +22,7 @@ export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMeth
'multi-part',
'vyper-code',
'vyper-multi-part',
'vyper-standard-input',
];
export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = {
......@@ -30,6 +32,7 @@ export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = {
'multi-part': 'Solidity (Multi-part files)',
'vyper-code': 'Vyper (Contract)',
'vyper-multi-part': 'Vyper (Multi-part files)',
'vyper-standard-input': 'Vyper (Standard JSON input)',
};
export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> = {
......@@ -84,7 +87,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
value: 'vyper-code' as const,
label: METHOD_LABELS['vyper-code'],
},
name: 'Vyper_contract',
name: '',
compiler: null,
evm_version: null,
code: '',
......@@ -99,6 +102,14 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
evm_version: null,
sources: [],
},
'vyper-standard-input': {
method: {
value: 'vyper-standard-input' as const,
label: METHOD_LABELS['vyper-standard-input'],
},
compiler: null,
sources: [],
},
};
export function getDefaultValues(method: SmartContractVerificationMethod, config: SmartContractVerificationConfig) {
......@@ -169,7 +180,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
body.set('contract_name', _data.name);
body.set('autodetect_constructor_args', String(Boolean(_data.autodetect_constructor_args)));
body.set('constructor_args', _data.constructor_args);
addFilesToFormData(body, _data.sources);
addFilesToFormData(body, _data.sources, 'files');
return body;
}
......@@ -177,7 +188,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
case 'sourcify': {
const _data = data as FormFieldsSourcify;
const body = new FormData();
addFilesToFormData(body, _data.sources);
addFilesToFormData(body, _data.sources, 'files');
_data.contract_index && body.set('chosen_contract_index', _data.contract_index.value);
return body;
......@@ -194,7 +205,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const libraries = reduceLibrariesArray(_data.libraries);
libraries && body.set('libraries', JSON.stringify(libraries));
addFilesToFormData(body, _data.sources);
addFilesToFormData(body, _data.sources, 'files');
return body;
}
......@@ -217,7 +228,18 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value);
_data.evm_version && body.set('evm_version', _data.evm_version.value);
addFilesToFormData(body, _data.sources);
addFilesToFormData(body, _data.sources, 'files');
addFilesToFormData(body, _data.interfaces, 'interfaces');
return body;
}
case 'vyper-standard-input': {
const _data = data as FormFieldsVyperStandardInput;
const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value);
addFilesToFormData(body, _data.sources, 'files');
return body;
}
......@@ -239,27 +261,33 @@ function reduceLibrariesArray(libraries: Array<ContractLibrary> | undefined) {
}, {});
}
function addFilesToFormData(body: FormData, files: Array<File> | undefined) {
function addFilesToFormData(body: FormData, files: Array<File> | undefined, fieldName: 'files' | 'interfaces') {
if (!files) {
return;
}
for (let index = 0; index < files.length; index++) {
const file = files[index];
body.set(`files[${ index }]`, file, file.name);
body.set(`${ fieldName }[${ index }]`, file, file.name);
}
}
const API_ERROR_TO_FORM_FIELD: Record<keyof SmartContractVerificationError, FieldPath<FormFields>> = {
contract_source_code: 'code',
files: 'sources',
interfaces: 'interfaces',
compiler_version: 'compiler',
constructor_arguments: 'constructor_args',
name: 'name',
};
export function formatSocketErrors(errors: SmartContractVerificationError): Array<[FieldPath<FormFields>, ErrorOption]> {
export function formatSocketErrors(errors: SmartContractVerificationError): Array<[FieldPath<FormFields>, ErrorOption] | undefined> {
return Object.entries(errors).map(([ key, value ]) => {
return [ API_ERROR_TO_FORM_FIELD[key as keyof SmartContractVerificationError], { message: value.join(',') } ];
const _key = key as keyof SmartContractVerificationError;
if (!API_ERROR_TO_FORM_FIELD[_key]) {
return;
}
return [ API_ERROR_TO_FORM_FIELD[_key], { message: value.join(',') } ];
});
}
......@@ -15,7 +15,6 @@ import ContentLoader from 'ui/shared/ContentLoader';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const ContractVerification = () => {
......@@ -96,7 +95,7 @@ const ContractVerification = () => {
}, [ appProps.referrer ]);
return (
<Page>
<>
<PageTitle
title="New smart contract verification"
backLink={ backLink }
......@@ -111,7 +110,7 @@ const ContractVerification = () => {
</Address>
) }
{ content }
</Page>
</>
);
};
......
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