Commit 532f7e7a authored by tom's avatar tom

contract verification: flattened code method

parent cdb1793c
......@@ -29,6 +29,7 @@ const RESTRICTED_MODULES = {
{ name: 'ui/shared/Tabs/RoutedTabs', message: 'Please use RoutedTabs component from toolkit/components/RoutedTabs instead' },
{ name: 'ui/shared/chakra/Tag', message: 'Please use Tag component from toolkit/chakra instead' },
{ name: 'ui/shared/select/Select', message: 'Please use Select component from toolkit/chakra instead' },
{ name: 'ui/shared/forms/fields/FormFieldFancySelect', message: 'Please use FormFieldSelect component' },
{
name: '@chakra-ui/react',
importNames: [
......
......@@ -4,12 +4,12 @@ import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
// import ContractVerificationForAddress from 'ui/pages/ContractVerificationForAddress';
import ContractVerificationForAddress from 'ui/pages/ContractVerificationForAddress';
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/address/[hash]/contract-verification" query={ props.query }>
{ /* <ContractVerificationForAddress/> */ }
<ContractVerificationForAddress/>
</PageNextJs>
);
};
......
......@@ -4,12 +4,12 @@ import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
// import ContractVerification from 'ui/pages/ContractVerification';
import ContractVerification from 'ui/pages/ContractVerification';
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/contract-verification" query={ props.query }>
{ /* <ContractVerification/> */ }
<ContractVerification/>
</PageNextJs>
);
};
......
......@@ -10,6 +10,7 @@ import { recipe as drawer } from './drawer.recipe';
import { recipe as field } from './field.recipe';
import { recipe as input } from './input.recipe';
import { recipe as link } from './link.recipe';
import { recipe as list } from './list.recipe';
import { recipe as menu } from './menu.recipe';
import { recipe as nativeSelect } from './native-select.recipe';
import { recipe as pinInput } from './pin-input.recipe';
......@@ -49,6 +50,7 @@ export const slotRecipes = {
dialog,
drawer,
field,
list,
menu,
nativeSelect,
pinInput,
......
......@@ -64,6 +64,9 @@ export const recipe = defineRecipe({
},
_placeholderShown: {
borderColor: 'input.border',
_invalid: {
borderColor: 'input.border.error',
},
},
_hover: {
borderColor: 'input.border.hover',
......
import { defineSlotRecipe } from '@chakra-ui/react';
export const recipe = defineSlotRecipe({
slots: [ 'root', 'item', 'indicator' ],
base: {
root: {
display: 'flex',
flexDirection: 'column',
gap: 'var(--list-gap)',
'& :where(ul, ol)': {
marginTop: 'var(--list-gap)',
},
},
item: {
whiteSpace: 'normal',
display: 'list-item',
'&::marker': {
color: 'inherit',
},
},
indicator: {
marginEnd: '2',
minHeight: '1lh',
flexShrink: 0,
display: 'inline-block',
verticalAlign: 'middle',
},
},
variants: {
variant: {
marker: {
root: {
listStyle: 'revert',
},
item: {
_marker: {
color: 'inherit',
},
},
},
plain: {
item: {
alignItems: 'flex-start',
display: 'inline-flex',
},
},
},
align: {
center: {
item: { alignItems: 'center' },
},
start: {
item: { alignItems: 'flex-start' },
},
end: {
item: { alignItems: 'flex-end' },
},
},
},
defaultVariants: {
variant: 'marker',
},
});
import { Button, Grid, Text, chakra, useUpdateEffect } from '@chakra-ui/react';
import { Grid, Text, chakra, useUpdateEffect } from '@chakra-ui/react';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
......@@ -14,24 +14,25 @@ import useApiFetch from 'lib/api/useApiFetch';
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
import delay from 'lib/delay';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel/index';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { Button } from 'toolkit/chakra/button';
import { toaster } from 'toolkit/chakra/toaster';
import ContractVerificationFieldAddress from './fields/ContractVerificationFieldAddress';
import ContractVerificationFieldLicenseType from './fields/ContractVerificationFieldLicenseType';
import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod';
import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode';
import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile';
import ContractVerificationSolidityFoundry from './methods/ContractVerificationSolidityFoundry';
import ContractVerificationSolidityHardhat from './methods/ContractVerificationSolidityHardhat';
import ContractVerificationSourcify from './methods/ContractVerificationSourcify';
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationStylusGitHubRepo from './methods/ContractVerificationStylusGitHubRepo';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import ContractVerificationVyperStandardInput from './methods/ContractVerificationVyperStandardInput';
// import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile';
// import ContractVerificationSolidityFoundry from './methods/ContractVerificationSolidityFoundry';
// import ContractVerificationSolidityHardhat from './methods/ContractVerificationSolidityHardhat';
// import ContractVerificationSourcify from './methods/ContractVerificationSourcify';
// import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
// import ContractVerificationStylusGitHubRepo from './methods/ContractVerificationStylusGitHubRepo';
// import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
// import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
// import ContractVerificationVyperStandardInput from './methods/ContractVerificationVyperStandardInput';
import { prepareRequestBody, formatSocketErrors, getDefaultValues, METHOD_LABELS } from './utils';
interface Props {
......@@ -43,14 +44,13 @@ interface Props {
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({
mode: 'onBlur',
defaultValues: getDefaultValues(methodFromQuery, config, hash, null),
defaultValues: getDefaultValues(methodFromQuery, config, hash, []),
});
const { handleSubmit, watch, formState, setError, reset, getFieldState, getValues, clearErrors } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const methodNameRef = React.useRef<string>();
const apiFetch = useApiFetch();
const toast = useToast();
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
const body = prepareRequestBody(data);
......@@ -76,7 +76,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
try {
await apiFetch('contract_verification_via', {
pathParams: { method: data.method.value, hash: data.address.toLowerCase() },
pathParams: { method: data.method[0], hash: data.address.toLowerCase() },
fetchParams: {
method: 'POST',
body,
......@@ -116,13 +116,9 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
return;
}
toast({
position: 'top-right',
toaster.success({
title: 'Success',
description: 'Contract is successfully verified.',
status: 'success',
variant: 'subtle',
isClosable: true,
});
mixpanel.logEvent(
......@@ -132,7 +128,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
);
window.location.assign(route({ pathname: '/address/[hash]', query: { hash: address, tab: 'contract' } }));
}, [ setError, toast, address, getValues ]);
}, [ setError, address, getValues ]);
const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) {
......@@ -141,20 +137,14 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
submitPromiseResolver.current?.(null);
const toastId = 'socket-error';
!toast.isActive(toastId) && toast({
id: toastId,
position: 'top-right',
toaster.error({
title: 'Error',
description: 'There was an error with socket connection. Try again later.',
status: 'error',
variant: 'subtle',
isClosable: true,
});
// callback should not change when form is submitted
// otherwise it will resubscribe to channel, but we don't want that since in that case we might miss verification result message
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ toast ]);
}, [ ]);
const channel = useSocketChannel({
topic: `addresses:${ address?.toLowerCase() }`,
......@@ -171,21 +161,21 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
const methods = React.useMemo(() => {
return {
'flattened-code': <ContractVerificationFlattenSourceCode config={ config }/>,
'standard-input': <ContractVerificationStandardInput config={ config }/>,
sourcify: <ContractVerificationSourcify/>,
'multi-part': <ContractVerificationMultiPartFile/>,
'vyper-code': <ContractVerificationVyperContract config={ config }/>,
'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
'vyper-standard-input': <ContractVerificationVyperStandardInput/>,
'solidity-hardhat': <ContractVerificationSolidityHardhat config={ config }/>,
'solidity-foundry': <ContractVerificationSolidityFoundry/>,
'stylus-github-repository': <ContractVerificationStylusGitHubRepo/>,
// 'standard-input': <ContractVerificationStandardInput config={ config }/>,
// sourcify: <ContractVerificationSourcify/>,
// 'multi-part': <ContractVerificationMultiPartFile/>,
// 'vyper-code': <ContractVerificationVyperContract config={ config }/>,
// 'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
// 'vyper-standard-input': <ContractVerificationVyperStandardInput/>,
// 'solidity-hardhat': <ContractVerificationSolidityHardhat config={ config }/>,
// 'solidity-foundry': <ContractVerificationSolidityFoundry/>,
// 'stylus-github-repository': <ContractVerificationStylusGitHubRepo/>,
};
}, [ config ]);
const method = watch('method');
const methodValue = method?.[0];
const licenseType = watch('license_type');
const content = methods[method?.value] || null;
const methodValue = method?.value;
const content = methods[methodValue] || null;
useUpdateEffect(() => {
if (methodValue) {
......@@ -212,13 +202,12 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
</Grid>
{ content }
{ formState.errors.root?.message && <Text color="error"mt={ 4 } fontSize="sm" whiteSpace="pre-wrap">{ formState.errors.root.message }</Text> }
{ Boolean(method) && method.value !== 'solidity-hardhat' && method.value !== 'solidity-foundry' && (
{ Boolean(method) && methodValue !== 'solidity-hardhat' && methodValue !== 'solidity-foundry' && (
<Button
variant="solid"
size="lg"
size="md"
type="submit"
mt={ 12 }
isLoading={ formState.isSubmitting }
loading={ formState.isSubmitting }
loadingText="Verify & publish"
>
Verify & publish
......
......@@ -8,10 +8,10 @@ import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props {
isReadOnly?: boolean;
readOnly?: boolean;
}
const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => {
const ContractVerificationFieldAddress = ({ readOnly }: Props) => {
return (
<>
<ContractVerificationFormRow>
......@@ -22,10 +22,9 @@ const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => {
<ContractVerificationFormRow>
<FormFieldAddress<FormFields>
name="address"
isRequired
required
placeholder="Smart contract / Address (0x...)"
isReadOnly={ isReadOnly }
size={{ base: 'md', lg: 'lg' }}
readOnly={ readOnly }
/>
</ContractVerificationFormRow>
</>
......
......@@ -15,9 +15,8 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => {
<ContractVerificationFormRow>
<FormFieldText<FormFields>
name="code"
isRequired
required
placeholder="Contract code"
size={{ base: 'md', lg: 'lg' }}
asComponent="Textarea"
/>
{ isVyper ? null : (
......
import { chakra, Checkbox, Code } from '@chakra-ui/react';
import { chakra, Code, createListCollection } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import { useFormContext } from 'react-hook-form';
......@@ -7,8 +7,8 @@ import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import IconSvg from 'ui/shared/IconSvg';
import { Checkbox } from 'toolkit/chakra/checkbox';
import FormFieldSelect from 'ui/shared/forms/fields/FormFieldSelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -26,12 +26,15 @@ const ContractVerificationFieldCompiler = ({ isVyper, isStylus }: Props) => {
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const handleCheckboxChange = React.useCallback(() => {
if (isNightly) {
const field = getValues('compiler');
field?.value.includes('nightly') && resetField('compiler', { defaultValue: null });
}
setIsNightly(prev => !prev);
}, [ getValues, isNightly, resetField ]);
setIsNightly(prev => {
if (prev) {
const field = getValues('compiler');
field?.[0]?.includes('nightly') && resetField('compiler', { defaultValue: [] });
}
return !prev;
});
}, [ getValues, resetField ]);
const options = React.useMemo(() => {
const versions = (() => {
......@@ -47,11 +50,21 @@ const ContractVerificationFieldCompiler = ({ isVyper, isStylus }: Props) => {
return versions?.map((option) => ({ label: option, value: option })) || [];
}, [ isStylus, isVyper, config?.solidity_compiler_versions, config?.stylus_compiler_versions, config?.vyper_compiler_versions ]);
const loadOptions = React.useCallback(async(inputValue: string) => {
return options
.filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase()))
// const loadOptions = React.useCallback(async(inputValue: string) => {
// return options
// .filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase()))
// .filter(({ label }) => isNightly ? true : !label.includes('nightly'))
// .slice(0, OPTIONS_LIMIT);
// }, [ isNightly, options ]);
// TODO @tom2drum implement filtering the options
const collection = React.useMemo(() => {
const items = options
// .filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase()))
.filter(({ label }) => isNightly ? true : !label.includes('nightly'))
.slice(0, OPTIONS_LIMIT);
return createListCollection({ items });
}, [ isNightly, options ]);
return (
......@@ -59,22 +72,19 @@ const ContractVerificationFieldCompiler = ({ isVyper, isStylus }: Props) => {
<>
{ !isVyper && !isStylus && (
<Checkbox
size="lg"
mb={ 2 }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
checked={ isNightly }
onCheckedChange={ handleCheckboxChange }
disabled={ formState.isSubmitting }
>
Include nightly builds
</Checkbox>
) }
<FormFieldFancySelect<FormFields, 'compiler'>
<FormFieldSelect<FormFields, 'compiler'>
name="compiler"
placeholder="Compiler (enter version or use the dropdown)"
loadOptions={ loadOptions }
defaultOptions
placeholderIcon={ <IconSvg name="search"/> }
isRequired
isAsync
collection={ collection }
required
/>
</>
{ isVyper || isStylus ? null : (
......
import { Link } from '@chakra-ui/react';
import React from 'react';
import type { FormFields } from '../types';
import { Link } from 'toolkit/chakra/link';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -12,10 +12,9 @@ const ContractVerificationFieldConstructorArgs = () => {
<ContractVerificationFormRow>
<FormFieldText<FormFields>
name="constructor_args"
isRequired
required
rules={{ maxLength: 255 }}
placeholder="ABI-encoded Constructor Arguments"
size={{ base: 'md', lg: 'lg' }}
asComponent="Textarea"
/>
<>
......
import { Link } from '@chakra-ui/react';
import { createListCollection } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
......@@ -6,7 +6,8 @@ import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import { Link } from 'toolkit/chakra/link';
import FormFieldSelect from 'ui/shared/forms/fields/FormFieldSelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -18,17 +19,19 @@ const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const options = React.useMemo(() => (
(isVyper ? config?.vyper_evm_versions : config?.solidity_evm_versions)?.map((option) => ({ label: option, value: option })) || []
), [ config?.solidity_evm_versions, config?.vyper_evm_versions, isVyper ]);
const collection = React.useMemo(() => {
const items = (isVyper ? config?.vyper_evm_versions : config?.solidity_evm_versions)?.map((option) => ({ label: option, value: option })) || [];
return createListCollection({ items });
}, [ config?.solidity_evm_versions, config?.vyper_evm_versions, isVyper ]);
return (
<ContractVerificationFormRow>
<FormFieldFancySelect<FormFields, 'evm_version'>
<FormFieldSelect<FormFields, 'evm_version'>
name="evm_version"
placeholder="EVM Version"
options={ options }
isRequired
collection={ collection }
required
/>
<>
<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>
......
import { Checkbox, useUpdateEffect } from '@chakra-ui/react';
import { useUpdateEffect } from '@chakra-ui/react';
import React from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import { Checkbox } from 'toolkit/chakra/checkbox';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationFieldLibraryItem from './ContractVerificationFieldLibraryItem';
......@@ -44,10 +46,9 @@ const ContractVerificationFieldLibraries = () => {
<>
<ContractVerificationFormRow>
<Checkbox
size="lg"
onChange={ handleCheckboxChange }
mt={ 9 }
isDisabled={ formState.isSubmitting }
disabled={ formState.isSubmitting }
>
Add contract libraries
</Checkbox>
......
import { Flex, IconButton, Text } from '@chakra-ui/react';
import { Flex, Text } from '@chakra-ui/react';
import React from 'react';
import type { FormFields } from '../types';
import { IconButton } from 'toolkit/chakra/icon-button';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import IconSvg from 'ui/shared/IconSvg';
......@@ -38,7 +39,7 @@ const ContractVerificationFieldLibraryItem = ({ index, fieldsLength, onAddFieldC
<>
<ContractVerificationFormRow>
<Flex alignItems="center" justifyContent="space-between" ref={ ref } mt={ index !== 0 ? 6 : 0 }>
<Text variant="secondary" fontSize="sm">Contract library { index + 1 }</Text>
<Text color="text.secondary" fontSize="sm">Contract library { index + 1 }</Text>
<Flex columnGap={ 5 }>
{ fieldsLength > 1 && (
<IconButton
......@@ -47,9 +48,10 @@ const ContractVerificationFieldLibraryItem = ({ index, fieldsLength, onAddFieldC
w="30px"
h="30px"
onClick={ handleRemoveButtonClick }
icon={ <IconSvg name="minus" w="20px" h="20px"/> }
isDisabled={ isDisabled }
/>
disabled={ isDisabled }
>
<IconSvg name="minus" w="20px" h="20px"/>
</IconButton>
) }
{ fieldsLength < LIMIT && (
<IconButton
......@@ -58,9 +60,10 @@ const ContractVerificationFieldLibraryItem = ({ index, fieldsLength, onAddFieldC
w="30px"
h="30px"
onClick={ handleAddButtonClick }
icon={ <IconSvg name="plus" w="20px" h="20px"/> }
isDisabled={ isDisabled }
/>
disabled={ isDisabled }
>
<IconSvg name="plus" w="20px" h="20px"/>
</IconButton>
) }
</Flex>
</Flex>
......@@ -68,10 +71,9 @@ const ContractVerificationFieldLibraryItem = ({ index, fieldsLength, onAddFieldC
<ContractVerificationFormRow>
<FormFieldText<FormFields, `libraries.${ number }.name`>
name={ `libraries.${ index }.name` }
isRequired
required
rules={{ maxLength: 255 }}
placeholder="Library name (.sol file)"
size={{ base: 'md', lg: 'lg' }}
/>
{ index === 0 ? (
<>
......@@ -80,11 +82,10 @@ const ContractVerificationFieldLibraryItem = ({ index, fieldsLength, onAddFieldC
) : null }
</ContractVerificationFormRow>
<ContractVerificationFormRow>
<FormFieldAddress<FormFields, `libraries.${ number }.address`>
<FormFieldAddress<FormFields>
name={ `libraries.${ index }.address` }
isRequired
required
placeholder="Library address (0x...)"
size={{ base: 'md', lg: 'lg' }}
/>
{ index === 0 ? (
<>
......
import { createListCollection } from '@chakra-ui/react';
import React from 'react';
import type { FormFields } from '../types';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FormFieldSelect from 'ui/shared/forms/fields/FormFieldSelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const options = CONTRACT_LICENSES.map(({ label, title, type }) => ({ label: `${ title } (${ label })`, value: type }));
const collection = createListCollection({
items: CONTRACT_LICENSES.map(({ label, title, type }) => ({ label: `${ title } (${ label })`, value: type })),
});
const ContractVerificationFieldLicenseType = () => {
return (
<ContractVerificationFormRow>
<FormFieldFancySelect<FormFields, 'license_type'>
<FormFieldSelect<FormFields, 'license_type'>
name="license_type"
placeholder="Contract license"
options={ options }
collection={ collection }
/>
<span>
For best practices, all contract source code holders, publishers and authors are encouraged to also
......
import {
Link,
chakra,
PopoverTrigger,
Portal,
PopoverContent,
PopoverArrow,
PopoverBody,
useColorModeValue,
DarkMode,
ListItem,
OrderedList,
List,
Box,
createListCollection,
} from '@chakra-ui/react';
import React from 'react';
import type { FormFields } from '../types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/client/contract';
import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import { Link } from 'toolkit/chakra/link';
import { Tooltip } from 'toolkit/chakra/tooltip';
import FormFieldSelect from 'ui/shared/forms/fields/FormFieldSelect';
import IconSvg from 'ui/shared/IconSvg';
import { METHOD_LABELS } from '../utils';
......@@ -29,95 +21,87 @@ interface Props {
}
const ContractVerificationFieldMethod = ({ methods }: Props) => {
const tooltipBg = useColorModeValue('gray.700', 'gray.900');
const isMobile = useIsMobile();
const options = React.useMemo(() => methods.map((method) => ({
value: method,
label: METHOD_LABELS[method],
})), [ methods ]);
const collection = React.useMemo(() => createListCollection({
items: methods.map((method) => ({
value: method,
label: METHOD_LABELS[method],
})),
}), [ methods ]);
const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => {
switch (method) {
case 'flattened-code':
return <ListItem key={ method }>Verification through a single file.</ListItem>;
return <List.Item key={ method }>Verification through a single file.</List.Item>;
case 'multi-part':
return <ListItem key={ method }>Verification of multi-part Solidity files.</ListItem>;
return <List.Item key={ method }>Verification of multi-part Solidity files.</List.Item>;
case 'sourcify':
return <ListItem key={ method }>Verification through <Link href="https://sourcify.dev/" target="_blank">Sourcify</Link>.</ListItem>;
return <List.Item key={ method }>Verification through <Link href="https://sourcify.dev/" target="_blank" className="dark">Sourcify</Link>.</List.Item>;
case 'standard-input':
return (
<ListItem key={ method }>
<List.Item key={ method }>
<span>Verification using </span>
<Link
href="https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description"
target="_blank"
className="dark"
>
Standard input JSON
</Link>
<span> file.</span>
</ListItem>
</List.Item>
);
case 'vyper-code':
return <ListItem key={ method }>Verification of Vyper contract.</ListItem>;
return <List.Item key={ method }>Verification of Vyper contract.</List.Item>;
case 'vyper-multi-part':
return <ListItem key={ method }>Verification of multi-part Vyper files.</ListItem>;
return <List.Item key={ method }>Verification of multi-part Vyper files.</List.Item>;
case 'vyper-standard-input':
return (
<ListItem key={ method }>
<List.Item 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"
className="dark"
>
Standard input JSON
</Link>
<span> file.</span>
</ListItem>
</List.Item>
);
case 'solidity-hardhat':
return <ListItem key={ method }>Verification through Hardhat plugin.</ListItem>;
return <List.Item key={ method }>Verification through Hardhat plugin.</List.Item>;
case 'solidity-foundry':
return <ListItem key={ method }>Verification through Foundry.</ListItem>;
return <List.Item key={ method }>Verification through Foundry.</List.Item>;
case 'stylus-github-repository':
return <ListItem key={ method }>Verification of Stylus contract via GitHub repository.</ListItem>;
return <List.Item key={ method }>Verification of Stylus contract via GitHub repository.</List.Item>;
}
}, []);
const tooltipContent = (
<Box>
<span>Currently, Blockscout supports { methods.length } methods:</span>
<List.Root as="ol" pl={ 5 }>
{ methods.map(renderPopoverListItem) }
</List.Root>
</Box>
);
return (
<>
<Box mt={{ base: 10, lg: 6 }} gridColumn={{ lg: '1 / 3' }}>
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
Currently, Blockscout supports { methods.length } contract verification methods
</chakra.span>
<Popover trigger="hover" isLazy placement={ isMobile ? 'bottom-end' : 'right-start' } offset={ [ -8, 8 ] }>
<PopoverTrigger>
<chakra.span display="inline-block" ml={ 1 } cursor="pointer" verticalAlign="middle" h="22px">
<IconSvg name="info" boxSize={ 5 } color="icon_info" _hover={{ color: 'link_hovered' }}/>
</chakra.span>
</PopoverTrigger>
<Portal>
<PopoverContent bgColor={ tooltipBg } w={{ base: '300px', lg: '380px' }}>
<PopoverArrow bgColor={ tooltipBg }/>
<PopoverBody color="white">
<DarkMode>
<span>Currently, Blockscout supports { methods.length } methods:</span>
<OrderedList>
{ methods.map(renderPopoverListItem) }
</OrderedList>
</DarkMode>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
<Tooltip content={ tooltipContent } interactive contentProps={{ textAlign: 'left', className: 'light' }}>
<IconSvg name="info" boxSize={ 5 } ml={ 1 } cursor="pointer" color="icon.info" _hover={{ color: 'link.primary.hover' }}/>
</Tooltip>
</Box>
<FormFieldFancySelect<FormFields, 'method'>
<FormFieldSelect<FormFields, 'method'>
name="method"
placeholder="Verification method (compiler type)"
options={ options }
isRequired
isAsync={ false }
isReadOnly={ options.length === 1 }
collection={ collection }
required
readOnly={ collection.items.length === 1 }
/>
</>
);
......
......@@ -16,9 +16,8 @@ const ContractVerificationFieldName = ({ hint }: Props) => {
<ContractVerificationFormRow>
<FormFieldText<FormFields>
name="name"
isRequired
required
placeholder="Contract name"
size={{ base: 'md', lg: 'lg' }}
rules={{ maxLength: 255 }}
/>
{ hint ? <span>{ hint }</span> : (
......
......@@ -27,10 +27,12 @@ const ContractVerificationFieldOptimization = () => {
{ isEnabled && (
<FormFieldText<FormFields, 'optimization_runs'>
name="optimization_runs"
isRequired
required
placeholder="Optimization runs"
type="number"
size="xs"
inputProps={{
type: 'number',
}}
size="sm"
minW="100px"
maxW="200px"
flexShrink={ 1 }
......
......@@ -7,11 +7,6 @@ export interface ContractLibrary {
address: string;
}
interface MethodOption {
label: string;
value: SmartContractVerificationMethod;
}
export interface LicenseOption {
label: string;
value: SmartContractLicenseType;
......@@ -19,15 +14,15 @@ export interface LicenseOption {
interface FormFieldsBase {
address: string;
method: MethodOption;
license_type: LicenseOption | null;
method: Array<SmartContractVerificationMethod>;
license_type: Array<SmartContractLicenseType>;
}
export interface FormFieldsFlattenSourceCode extends FormFieldsBase {
is_yul: boolean;
name: string | undefined;
compiler: Option | null;
evm_version: Option | null;
compiler: Array<string>;
evm_version: Array<string>;
is_optimization_enabled: boolean;
optimization_runs: string;
code: string;
......@@ -38,7 +33,7 @@ export interface FormFieldsFlattenSourceCode extends FormFieldsBase {
export interface FormFieldsStandardInput extends FormFieldsBase {
name: string;
compiler: Option | null;
compiler: Array<string>;
sources: Array<File>;
autodetect_constructor_args: boolean;
constructor_args: string;
......@@ -46,8 +41,8 @@ export interface FormFieldsStandardInput extends FormFieldsBase {
export interface FormFieldsStandardInputZk extends FormFieldsBase {
name: string;
compiler: Option | null;
zk_compiler: Option | null;
compiler: Array<string>;
zk_compiler: Array<string>;
sources: Array<File>;
autodetect_constructor_args: boolean;
constructor_args: string;
......@@ -59,8 +54,8 @@ export interface FormFieldsSourcify extends FormFieldsBase {
}
export interface FormFieldsMultiPartFile extends FormFieldsBase {
compiler: Option | null;
evm_version: Option | null;
compiler: Array<string>;
evm_version: Array<string>;
is_optimization_enabled: boolean;
optimization_runs: string;
sources: Array<File>;
......@@ -69,26 +64,26 @@ export interface FormFieldsMultiPartFile extends FormFieldsBase {
export interface FormFieldsVyperContract extends FormFieldsBase {
name: string;
evm_version: Option | null;
compiler: Option | null;
evm_version: Array<string>;
compiler: Array<string>;
code: string;
constructor_args: string | undefined;
}
export interface FormFieldsVyperMultiPartFile extends FormFieldsBase {
compiler: Option | null;
evm_version: Option | null;
compiler: Array<string>;
evm_version: Array<string>;
sources: Array<File>;
interfaces: Array<File>;
}
export interface FormFieldsVyperStandardInput extends FormFieldsBase {
compiler: Option | null;
compiler: Array<string>;
sources: Array<File>;
}
export interface FormFieldsStylusGitHubRepo extends FormFieldsBase {
compiler: Option | null;
compiler: Array<string>;
repository_url: string;
commit_hash: string;
path_prefix: string;
......
......@@ -51,123 +51,93 @@ export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = {
export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> = {
'flattened-code': {
address: '',
method: {
value: 'flattened-code' as const,
label: METHOD_LABELS['flattened-code'],
},
method: [ 'flattened-code' ],
is_yul: false,
name: '',
compiler: null,
evm_version: null,
compiler: [],
evm_version: [],
is_optimization_enabled: true,
optimization_runs: '200',
code: '',
autodetect_constructor_args: true,
constructor_args: '',
libraries: [],
license_type: null,
license_type: [],
},
'standard-input': {
address: '',
method: {
value: 'standard-input' as const,
label: METHOD_LABELS['standard-input'],
},
method: [ 'standard-input' ],
name: '',
compiler: null,
compiler: [],
sources: [],
autodetect_constructor_args: true,
constructor_args: '',
license_type: null,
license_type: [],
},
sourcify: {
address: '',
method: {
value: 'sourcify' as const,
label: METHOD_LABELS.sourcify,
},
method: [ 'sourcify' ],
sources: [],
license_type: null,
license_type: [],
},
'multi-part': {
address: '',
method: {
value: 'multi-part' as const,
label: METHOD_LABELS['multi-part'],
},
compiler: null,
evm_version: null,
method: [ 'multi-part' ],
compiler: [],
evm_version: [],
is_optimization_enabled: true,
optimization_runs: '200',
sources: [],
libraries: [],
license_type: null,
license_type: [],
},
'vyper-code': {
address: '',
method: {
value: 'vyper-code' as const,
label: METHOD_LABELS['vyper-code'],
},
method: [ 'vyper-code' ],
name: '',
compiler: null,
evm_version: null,
compiler: [],
evm_version: [],
code: '',
constructor_args: '',
license_type: null,
license_type: [],
},
'vyper-multi-part': {
address: '',
method: {
value: 'vyper-multi-part' as const,
label: METHOD_LABELS['vyper-multi-part'],
},
compiler: null,
evm_version: null,
method: [ 'vyper-multi-part' ],
compiler: [],
evm_version: [],
sources: [],
license_type: null,
license_type: [],
},
'vyper-standard-input': {
address: '',
method: {
value: 'vyper-standard-input' as const,
label: METHOD_LABELS['vyper-standard-input'],
},
compiler: null,
method: [ 'vyper-standard-input' ],
compiler: [],
sources: [],
license_type: null,
license_type: [],
},
'solidity-hardhat': {
address: '',
method: {
value: 'solidity-hardhat' as const,
label: METHOD_LABELS['solidity-hardhat'],
},
compiler: null,
method: [ 'solidity-hardhat' ],
compiler: [],
sources: [],
license_type: null,
license_type: [],
},
'solidity-foundry': {
address: '',
method: {
value: 'solidity-foundry' as const,
label: METHOD_LABELS['solidity-foundry'],
},
compiler: null,
method: [ 'solidity-foundry' ],
compiler: [],
sources: [],
license_type: null,
license_type: [],
},
'stylus-github-repository': {
address: '',
method: {
value: 'stylus-github-repository' as const,
label: METHOD_LABELS['stylus-github-repository'],
},
compiler: null,
method: [ 'stylus-github-repository' ],
compiler: [],
repository_url: '',
commit_hash: '',
path_prefix: '',
license_type: null,
license_type: [],
},
};
......@@ -188,11 +158,11 @@ export function getDefaultValues(
if ('evm_version' in defaultValues) {
if (method === 'flattened-code' || method === 'multi-part') {
defaultValues.evm_version = config.solidity_evm_versions.find((value) => value === 'default') ? { label: 'default', value: 'default' } : null;
defaultValues.evm_version = config.solidity_evm_versions.find((value) => value === 'default') ? [ 'default' ] : [];
}
if (method === 'vyper-multi-part') {
defaultValues.evm_version = config.vyper_evm_versions.find((value) => value === 'default') ? { label: 'default', value: 'default' } : null;
defaultValues.evm_version = config.vyper_evm_versions.find((value) => value === 'default') ? [ 'default' ] : [];
}
}
......@@ -204,10 +174,7 @@ export function getDefaultValues(
}
if (singleMethod) {
defaultValues.method = {
label: METHOD_LABELS[config.verification_options[0]],
value: config.verification_options[0],
};
defaultValues.method = config.verification_options;
}
return defaultValues;
......@@ -235,21 +202,21 @@ export function sortVerificationMethods(methodA: SmartContractVerificationMethod
export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const defaultLicenseType: SmartContractLicenseType = 'none';
switch (data.method.value) {
switch (data.method[0]) {
case 'flattened-code': {
const _data = data as FormFieldsFlattenSourceCode;
return {
compiler_version: _data.compiler?.value,
compiler_version: _data.compiler?.[0],
source_code: _data.code,
is_optimization_enabled: _data.is_optimization_enabled,
is_yul_contract: _data.is_yul,
optimization_runs: _data.optimization_runs,
contract_name: _data.name || undefined,
libraries: reduceLibrariesArray(_data.libraries),
evm_version: _data.evm_version?.value,
evm_version: _data.evm_version?.[0],
autodetect_constructor_args: _data.autodetect_constructor_args,
constructor_args: _data.constructor_args,
license_type: _data.license_type?.value ?? defaultLicenseType,
license_type: _data.license_type?.[0] ?? defaultLicenseType,
};
}
......@@ -257,15 +224,15 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as (FormFieldsStandardInput | FormFieldsStandardInputZk);
const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value);
body.set('license_type', _data.license_type?.value ?? defaultLicenseType);
_data.compiler && body.set('compiler_version', _data.compiler?.[0]);
body.set('license_type', _data.license_type?.[0] ?? defaultLicenseType);
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, 'files');
// zkSync fields
'zk_compiler' in _data && _data.zk_compiler && body.set('zk_compiler_version', _data.zk_compiler.value);
'zk_compiler' in _data && _data.zk_compiler && body.set('zk_compiler_version', _data.zk_compiler?.[0]);
return body;
}
......@@ -274,8 +241,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as FormFieldsSourcify;
const body = new FormData();
addFilesToFormData(body, _data.sources, 'files');
body.set('chosen_contract_index', _data.contract_index?.value ?? defaultLicenseType);
_data.license_type && body.set('license_type', _data.license_type.value);
body.set('chosen_contract_index', _data.contract_index?.value ?? '0');
_data.license_type && body.set('license_type', _data.license_type?.[0] ?? defaultLicenseType);
return body;
}
......@@ -284,9 +251,9 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as FormFieldsMultiPartFile;
const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value);
_data.evm_version && body.set('evm_version', _data.evm_version.value);
body.set('license_type', _data.license_type?.value ?? defaultLicenseType);
_data.compiler && body.set('compiler_version', _data.compiler?.[0]);
_data.evm_version && body.set('evm_version', _data.evm_version?.[0]);
body.set('license_type', _data.license_type?.[0] ?? defaultLicenseType);
body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled)));
_data.is_optimization_enabled && body.set('optimization_runs', _data.optimization_runs);
......@@ -301,12 +268,12 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as FormFieldsVyperContract;
return {
compiler_version: _data.compiler?.value,
evm_version: _data.evm_version?.value,
compiler_version: _data.compiler?.[0],
evm_version: _data.evm_version?.[0],
source_code: _data.code,
contract_name: _data.name,
constructor_args: _data.constructor_args,
license_type: _data.license_type?.value ?? defaultLicenseType,
license_type: _data.license_type?.[0] ?? defaultLicenseType,
};
}
......@@ -314,9 +281,9 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as FormFieldsVyperMultiPartFile;
const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value);
_data.evm_version && body.set('evm_version', _data.evm_version.value);
body.set('license_type', _data.license_type?.value ?? defaultLicenseType);
_data.compiler && body.set('compiler_version', _data.compiler?.[0]);
_data.evm_version && body.set('evm_version', _data.evm_version?.[0]);
body.set('license_type', _data.license_type?.[0] ?? defaultLicenseType);
addFilesToFormData(body, _data.sources, 'files');
addFilesToFormData(body, _data.interfaces, 'interfaces');
......@@ -327,8 +294,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as FormFieldsVyperStandardInput;
const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value);
body.set('license_type', _data.license_type?.value ?? defaultLicenseType);
_data.compiler && body.set('compiler_version', _data.compiler?.[0]);
body.set('license_type', _data.license_type?.[0] ?? defaultLicenseType);
addFilesToFormData(body, _data.sources, 'files');
return body;
......@@ -338,11 +305,11 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as FormFieldsStylusGitHubRepo;
return {
cargo_stylus_version: _data.compiler?.value,
cargo_stylus_version: _data.compiler?.[0],
repository_url: _data.repository_url,
commit: _data.commit_hash,
path_prefix: _data.path_prefix,
license_type: _data.license_type?.value ?? defaultLicenseType,
license_type: _data.license_type?.[0] ?? defaultLicenseType,
};
}
......
......@@ -93,6 +93,8 @@ const ContractVerificationForAddress = () => {
fontSize="lg"
fontWeight={ 500 }
mb={ 12 }
w="min-content"
maxW="100%"
/>
{ content }
</>
......
......@@ -207,8 +207,8 @@ const VerifiedAddresses = () => {
Before starting, make sure that:
</chakra.p>
<List.Root ml={ 6 } as="ol">
<List.Item _marker={{ color: 'inherit' }}>The source code for the smart contract is deployed on “{ config.chain.name }”.</List.Item>
<List.Item _marker={{ color: 'inherit' }}>
<List.Item>The source code for the smart contract is deployed on “{ config.chain.name }”.</List.Item>
<List.Item>
<span>The source code is verified (if not yet verified, you can use </span>
<Link href="https://docs.blockscout.com/for-users/verifying-a-smart-contract" target="_blank">this tool</Link>
<span>).</span>
......
......@@ -31,10 +31,15 @@ const FormFieldText = <
group,
inputProps,
asComponent,
size = asComponent === 'Textarea' ? '2xl' : 'xl',
size: sizeProp,
disabled,
floating: floatingProp,
...restProps
}: Props<FormFields, Name>) => {
const defaultSize = asComponent === 'Textarea' ? '2xl' : 'xl';
const size = sizeProp || defaultSize;
const floating = floatingProp !== undefined ? floatingProp : size === defaultSize;
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof name>({
control,
......@@ -59,6 +64,8 @@ const FormFieldText = <
<Input
{ ...field }
autoComplete="off"
// for non-floating field label, we pass placeholder to the input component
placeholder={ !floating ? placeholder : undefined }
{ ...inputProps as InputProps }
onBlur={ handleBlur }
/>
......@@ -75,12 +82,13 @@ const FormFieldText = <
return (
<Field
label={ placeholder }
// for floating field label, we pass placeholder value to the label
label={ floating ? placeholder : undefined }
errorText={ getFieldErrorText(fieldState.error) }
invalid={ Boolean(fieldState.error) }
disabled={ formState.isSubmitting || disabled }
size={ size }
floating
floating={ floating }
{ ...restProps }
>
{ content }
......
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