Commit 27974d19 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #570 from blockscout/verification-api

contract verification: api
parents 6fd2316b 8ca3d5d3
......@@ -13,9 +13,10 @@ export default function fetchFactory(
// first arg can be only a string
// FIXME migrate to RequestInfo later if needed
return function fetch(url: string, init?: RequestInit): Promise<Response> {
const incomingContentType = _req.headers['content-type'];
const headers = {
accept: 'application/json',
'content-type': 'application/json',
'content-type': incomingContentType?.match(/^multipart\/form-data/) ? incomingContentType : 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
};
......@@ -25,10 +26,23 @@ export default function fetchFactory(
req: _req,
});
const body = (() => {
const _body = init?.body;
if (!_body) {
return;
}
if (typeof _body === 'string') {
return _body;
}
return JSON.stringify(_body);
})();
return nodeFetch(url, {
...init,
headers,
body: init?.body ? JSON.stringify(init.body) : undefined,
body,
});
};
}
......@@ -17,7 +17,7 @@ import type {
import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
......@@ -212,6 +212,12 @@ export const RESOURCES = {
contract_methods_write_proxy: {
path: '/api/v2/smart-contracts/:id/methods-write-proxy',
},
contract_verification_config: {
path: '/api/v2/smart-contracts/verification/config',
},
contract_verification_via: {
path: '/api/v2/smart-contracts/:id/verification/via/:method',
},
// TOKEN
token: {
......@@ -365,6 +371,7 @@ Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> :
Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> :
Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
never;
/* eslint-enable @typescript-eslint/indent */
......
......@@ -12,3 +12,6 @@ export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;
export const MONTH = 30 * DAY;
export const YEAR = 365 * DAY;
export const Kb = 1_000;
export const Mb = 1_000 * Kb;
......@@ -11,7 +11,7 @@ export interface Params {
method?: RequestInit['method'];
headers?: RequestInit['headers'];
signal?: RequestInit['signal'];
body?: Record<string, unknown>;
body?: Record<string, unknown> | FormData;
credentials?: RequestCredentials;
}
......@@ -20,13 +20,27 @@ export default function useFetch() {
const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => {
const hasBody = params?.method && ![ 'GET', 'HEAD' ].includes(params.method);
const _body = params?.body;
const isFormData = _body instanceof FormData;
const isBodyAllowed = params?.method && ![ 'GET', 'HEAD' ].includes(params.method);
const body: FormData | string | undefined = (() => {
if (!isBodyAllowed) {
return;
}
if (isFormData) {
return _body;
}
return JSON.stringify({ ..._body, _csrf_token: token });
})();
const reqParams = {
...params,
body: hasBody ? JSON.stringify({ ...params.body, _csrf_token: token }) : undefined,
body,
headers: {
...(hasBody ? { 'Content-type': 'application/json' } : undefined),
...(isBodyAllowed && !isFormData ? { 'Content-type': 'application/json' } : undefined),
...params?.headers,
},
};
......
// https://hexdocs.pm/phoenix/js/
import type { SocketConnectOption } from 'phoenix';
import { Socket } from 'phoenix';
import React, { useEffect, useState } from 'react';
......
......@@ -2,6 +2,7 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction';
......@@ -19,6 +20,7 @@ SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer |
SocketMessage.TokenTransfers |
SocketMessage.ContractVerification |
SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
......@@ -43,6 +45,7 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
}
......@@ -60,7 +60,11 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
} else {
ch = socket.channel(topic);
CHANNEL_REGISTRY[topic] = ch;
ch.join().receive('ok', (message) => onJoinRef.current?.(ch, message));
ch.join()
.receive('ok', (message) => onJoinRef.current?.(ch, message))
.receive('error', () => {
onSocketError?.();
});
}
setChannel(ch);
......@@ -70,7 +74,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
delete CHANNEL_REGISTRY[topic];
setChannel(undefined);
};
}, [ socket, topic, params, isDisabled ]);
}, [ socket, topic, params, isDisabled, onSocketError ]);
return channel;
}
......@@ -35,10 +35,13 @@ function MyApp({ Component, pageProps }: AppProps) {
},
}));
const renderErrorScreen = React.useCallback(() => {
const renderErrorScreen = React.useCallback((error?: Error) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusCode = (error?.cause as any)?.status || 500;
return (
<AppError
statusCode={ 500 }
statusCode={ statusCode }
height="100vh"
display="flex"
flexDirection="column"
......
import { formAnatomy as parts } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { getColor } from '@chakra-ui/theme-tools';
import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
import FancySelect from './FancySelect';
......@@ -81,11 +81,18 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles,
[`
input[disabled] + label,
textarea[disabled] + label,
&[aria-disabled=true] label
`]: {
backgroundColor: 'transparent',
},
// in textarea bg of label could not be transparent; it should match the background color of input but without alpha
// so we have to use non-standard colors here
'textarea[disabled] + label': {
backgroundColor: mode('#ececec', '#232425')(props),
},
'textarea[disabled] + label[data-in-modal=true]': {
backgroundColor: mode('#ececec', '#292b34')(props),
},
// indicator styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': {
......
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
......
......@@ -113,3 +113,34 @@ export interface SmartContractQueryMethodReadError {
}
export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError;
// VERIFICATION
export type SmartContractVerificationMethod = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part' | 'vyper-code' | 'vyper-multi-part';
export interface SmartContractVerificationConfigRaw {
solidity_compiler_versions: Array<string>;
solidity_evm_versions: Array<string>;
verification_options: Array<string>;
vyper_compiler_versions: Array<string>;
vyper_evm_versions: Array<string>;
}
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
verification_options: Array<SmartContractVerificationMethod>;
}
export type SmartContractVerificationResponse = {
status: 'error';
errors: SmartContractVerificationError;
} | {
status: 'success';
}
export interface SmartContractVerificationError {
contract_source_code?: Array<string>;
files?: Array<string>;
compiler_version?: Array<string>;
constructor_arguments?: Array<string>;
name?: Array<string>;
}
......@@ -129,7 +129,7 @@ const ContractCode = () => {
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<LinkInternal href={ link('address_contract_verification', { id: data.verified_twin_address_hash }) }>Verify & Publish</LinkInternal>
<LinkInternal href={ link('address_contract_verification', { id: addressHash }) }>Verify & Publish</LinkInternal>
<span> page</span>
</Alert>
) }
......
......@@ -103,7 +103,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
{ ...field }
disabled={ true }
/>
<FormLabel>Auto-generated API key token</FormLabel>
<FormLabel data-in-modal="true">Auto-generated API key token</FormLabel>
</FormControl>
);
}, []);
......
......@@ -12,7 +12,7 @@ type Props = {
data?: ApiKey;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const ApiKeyModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const title = data ? 'Edit API key' : 'New API key';
const text = !data ? 'Add an application name to identify your API key. Click the button below to auto-generate the associated key.' : '';
......@@ -34,4 +34,4 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
);
};
export default AddressModal;
export default ApiKeyModal;
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import TestApp from 'playwright/TestApp';
import ContractVerificationForm from './ContractVerificationForm';
......@@ -11,10 +13,49 @@ const hooksConfig = {
},
};
const hash = '0x2F99338637F027CFB7494E46B49987457beCC6E3';
const formConfig: SmartContractVerificationConfig = {
solidity_compiler_versions: [
'v0.8.17+commit.8df45f5f',
'v0.8.16+commit.07a7930e',
'v0.8.15+commit.e14f2714',
'v0.8.18-nightly.2022.11.23+commit.eb2f874e',
'v0.8.17-nightly.2022.8.24+commit.22a0c46e',
'v0.8.16-nightly.2022.7.6+commit.b6f11b33',
],
solidity_evm_versions: [
'default',
'london',
'berlin',
],
verification_options: [
'flattened-code',
'standard-input',
'sourcify',
'multi-part',
'vyper-code',
],
vyper_compiler_versions: [
'v0.3.7+commit.6020b8bb',
'v0.3.1+commit.0463ea4c',
'v0.3.0+commit.8a23feb',
'v0.2.16+commit.59e1bdd',
'v0.2.3+commit.006968f',
'v0.2.2+commit.337c2ef',
'v0.1.0-beta.17+commit.0671b7b',
],
vyper_evm_versions: [
'byzantium',
'constantinople',
'petersburg',
'istanbul',
],
};
test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractVerificationForm/>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -30,7 +71,7 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) =
test('standard input json method', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractVerificationForm/>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -43,7 +84,7 @@ test('standard input json method', async({ mount, page }) => {
test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractVerificationForm/>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -62,7 +103,7 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
test('multi-part files method', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractVerificationForm/>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -75,7 +116,7 @@ test('multi-part files method', async({ mount, page }) => {
test('vyper contract method', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractVerificationForm/>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>,
{ hooksConfig },
);
......
......@@ -4,54 +4,140 @@ import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
import type { FormFields, VerificationMethod } from './types';
import type { FormFields } from './types';
import type { SocketMessage } from 'lib/socket/types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import delay from 'lib/delay';
import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast';
import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ContractVerificationFieldMethod, { VERIFICATION_METHODS } from './fields/ContractVerificationFieldMethod';
import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod';
import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode';
import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile';
import ContractVerificationSourcify from './methods/ContractVerificationSourcify';
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors } from './utils';
const METHODS = {
flatten_source_code: <ContractVerificationFlattenSourceCode/>,
standard_input: <ContractVerificationStandardInput/>,
const METHOD_COMPONENTS = {
'flattened-code': <ContractVerificationFlattenSourceCode/>,
'standard-input': <ContractVerificationStandardInput/>,
sourcify: <ContractVerificationSourcify/>,
multi_part_file: <ContractVerificationMultiPartFile/>,
vyper_contract: <ContractVerificationVyperContract/>,
'multi-part': <ContractVerificationMultiPartFile/>,
'vyper-code': <ContractVerificationVyperContract/>,
'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
};
const ContractVerificationForm = () => {
const router = useRouter();
const methodFromQuery = router.query.method?.toString() as VerificationMethod;
interface Props {
method?: SmartContractVerificationMethod;
config: SmartContractVerificationConfig;
hash: string;
}
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({
mode: 'onBlur',
defaultValues: {
method: VERIFICATION_METHODS.includes(methodFromQuery) ? methodFromQuery : undefined,
method: methodFromQuery,
},
});
const { control, handleSubmit, watch, formState } = formApi;
const { control, handleSubmit, watch, formState, setError } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const apiFetch = useApiFetch();
const toast = useToast();
const router = useRouter();
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
// eslint-disable-next-line no-console
console.log('__>__', data);
await delay(5_000);
}, []);
const body = prepareRequestBody(data);
const method = watch('method');
try {
await apiFetch('contract_verification_via', {
pathParams: { method: data.method, id: hash },
fetchParams: {
method: 'POST',
body,
},
});
} catch (error) {
return;
}
return new Promise((resolve) => {
submitPromiseResolver.current = resolve;
});
}, [ apiFetch, hash ]);
const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback((payload) => {
if (payload.status === 'error') {
const errors = formatSocketErrors(payload.errors);
errors.forEach(([ field, error ]) => setError(field, error));
submitPromiseResolver.current?.(null);
return;
}
toast({
position: 'top-right',
title: 'Success',
description: 'Contract is successfully verified.',
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
router.push(link('address_index', { id: hash }, { tab: 'contract' }), undefined, { shallow: true });
},
});
}, [ hash, router, setError, toast ]);
const content = METHODS[method] || null;
const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) {
return;
}
submitPromiseResolver.current?.(null);
const toastId = 'socket-error';
!toast.isActive(toastId) && toast({
id: toastId,
position: 'top-right',
title: 'Error',
description: 'There was an error with socket connection. Try again later.',
status: 'error',
variant: 'subtle',
isClosable: true,
});
}, [ formState.isSubmitting, toast ]);
const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: false,
});
useSocketMessage({
channel,
event: 'verification_result',
handler: handleNewSocketMessage,
});
const method = watch('method');
const content = METHOD_COMPONENTS[method] || null;
return (
<FormProvider { ...formApi }>
<chakra.form
noValidate
onSubmit={ handleSubmit(onFormSubmit) }
mt={ 12 }
>
<ContractVerificationFieldMethod control={ control } isDisabled={ Boolean(method) }/>
<ContractVerificationFieldMethod
control={ control }
isDisabled={ Boolean(method) }
methods={ config.verification_options }
/>
{ content }
{ Boolean(method) && (
<Button
......
......@@ -7,27 +7,39 @@ import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationFieldConstructorArgs from './ContractVerificationFieldConstructorArgs';
const ContractVerificationFieldConstArgs = () => {
const { formState, control } = useFormContext<FormFields>();
const ContractVerificationFieldAutodetectArgs = () => {
const [ isOn, setIsOn ] = React.useState(true);
const { formState, control, resetField } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'constructor_args'>}) => (
<CheckboxInput<FormFields, 'constructor_args'>
const handleCheckboxChange = React.useCallback(() => {
!isOn && resetField('constructor_args');
setIsOn(prev => !prev);
}, [ isOn, resetField ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'autodetect_constructor_args'>}) => (
<CheckboxInput<FormFields, 'autodetect_constructor_args'>
text="Try to fetch constructor arguments automatically"
field={ field }
isDisabled={ formState.isSubmitting }
onChange={ handleCheckboxChange }
/>
), [ formState.isSubmitting ]);
), [ formState.isSubmitting, handleCheckboxChange ]);
return (
<ContractVerificationFormRow>
<Controller
name="constructor_args"
control={ control }
render={ renderControl }
/>
</ContractVerificationFormRow>
<>
<ContractVerificationFormRow>
<Controller
name="autodetect_constructor_args"
control={ control }
render={ renderControl }
defaultValue={ true }
/>
</ContractVerificationFormRow>
{ !isOn && <ContractVerificationFieldConstructorArgs/> }
</>
);
};
export default React.memo(ContractVerificationFieldConstArgs);
export default React.memo(ContractVerificationFieldAutodetectArgs);
......@@ -5,6 +5,7 @@ import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -27,7 +28,8 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => {
isDisabled={ formState.isSubmitting }
required
/>
<InputPlaceholder text="Contract code" error={ error }/>
<InputPlaceholder text="Contract code"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
......
import { Code, Checkbox } from '@chakra-ui/react';
import { Code } 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/api/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const COMPILERS = [
'v0.8.17+commit.8df45f5f',
'v0.8.16+commit.07a7930e',
'v0.8.15+commit.e14f2714',
];
const COMPILERS_NIGHTLY = [
'v0.8.18-nightly.2022.11.23+commit.eb2f874e',
'v0.8.17-nightly.2022.8.24+commit.22a0c46e',
'v0.8.16-nightly.2022.7.6+commit.b6f11b33',
];
const OPTIONS_LIMIT = 50;
interface Props {
isVyper?: boolean;
}
const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
const [ isNightly, setIsNightly ] = React.useState(false);
const { formState, control, getValues, resetField } = useFormContext<FormFields>();
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const options = React.useMemo(() => (
[
...COMPILERS, ...(isNightly ? COMPILERS_NIGHTLY : []),
].map((option) => ({ label: option, value: option }))
), [ isNightly ]);
(isVyper ? config?.vyper_compiler_versions : config?.solidity_compiler_versions)?.map((option) => ({ label: option, value: option })) || []
), [ config?.solidity_compiler_versions, config?.vyper_compiler_versions, isVyper ]);
const loadOptions = React.useCallback(async(inputValue: string) => {
return options
.filter(({ label }) => {
if (!inputValue) {
return !label.toLowerCase().includes('nightly');
}
const handleCheckboxChange = React.useCallback(() => {
if (isNightly) {
const field = getValues('compiler');
field.value.includes('nightly') && resetField('compiler', { defaultValue: null });
}
setIsNightly(prev => !prev);
}, [ getValues, isNightly, resetField ]);
return label.toLowerCase().includes(inputValue.toLowerCase());
})
.slice(0, OPTIONS_LIMIT);
}, [ options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => {
const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined;
......@@ -51,36 +47,26 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
return (
<FancySelect
{ ...field }
options={ options }
loadOptions={ loadOptions }
defaultOptions
size={ isMobile ? 'md' : 'lg' }
placeholder="Compiler"
isDisabled={ formState.isSubmitting }
error={ error }
isRequired
isAsync
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, options ]);
}, [ formState.errors, formState.isSubmitting, isMobile, loadOptions ]);
return (
<ContractVerificationFormRow>
<>
<Controller
name="compiler"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
{ !isVyper && (
<Checkbox
size="lg"
mt={ 3 }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
>
Include nightly builds
</Checkbox>
) }
</>
<Controller
name="compiler"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
{ isVyper ? null : (
<>
<span>The compiler version is specified in </span>
......
......@@ -5,32 +5,39 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldAbiEncodedArgs = () => {
const ContractVerificationFieldConstructorArgs = () => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'abi_encoded_args'>}) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'constructor_args'>}) => {
const error = 'constructor_args' in formState.errors ? formState.errors.constructor_args : undefined;
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }}>
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Textarea
{ ...field }
maxLength={ 255 }
isDisabled={ formState.isSubmitting }
isInvalid={ Boolean(error) }
required
/>
<InputPlaceholder text="ABI-encoded Constructor Arguments"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl>
);
}, [ formState.isSubmitting ]);
}, [ formState.errors, formState.isSubmitting ]);
return (
<ContractVerificationFormRow>
<Controller
name="abi_encoded_args"
name="constructor_args"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
<>
<span>Add arguments in </span>
......@@ -44,4 +51,4 @@ const ContractVerificationFieldAbiEncodedArgs = () => {
);
};
export default React.memo(ContractVerificationFieldAbiEncodedArgs);
export default React.memo(ContractVerificationFieldConstructorArgs);
import { 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/api/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const VERSIONS = [
'default',
'london',
'berlin',
];
interface Props {
isVyper?: boolean;
}
const ContractVerificationFieldEvmVersion = () => {
const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const options = React.useMemo(() => (
VERSIONS.map((option) => ({ label: option, value: option }))
), [ ]);
(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 renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'evm_version'>}) => {
const error = 'evm_version' in formState.errors ? formState.errors.evm_version : undefined;
......
......@@ -20,32 +20,26 @@ import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { FormFields, VerificationMethod } from '../types';
import type { FormFields } from '../types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import infoIcon from 'icons/info.svg';
export const VERIFICATION_METHODS: Array<VerificationMethod> = [
'flatten_source_code',
'standard_input',
'sourcify',
'multi_part_file',
'vyper_contract',
];
interface Props {
control: Control<FormFields>;
isDisabled?: boolean;
methods: SmartContractVerificationConfig['verification_options'];
}
const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => {
const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => {
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean();
const tooltipBg = useColorModeValue('gray.700', 'gray.900');
const renderItem = React.useCallback((method: VerificationMethod) => {
const renderItem = React.useCallback((method: SmartContractVerificationMethod) => {
switch (method) {
case 'flatten_source_code':
case 'flattened-code':
return 'Via flattened source code';
case 'standard_input':
case 'standard-input':
return (
<>
<span>Via standard </span>
......@@ -91,10 +85,12 @@ const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => {
</Popover>
</>
);
case 'multi_part_file':
case 'multi-part':
return 'Via multi-part files';
case 'vyper_contract':
case 'vyper-code':
return 'Vyper contract';
case 'vyper-multi-part':
return 'Via multi-part Vyper files';
default:
break;
......@@ -103,15 +99,15 @@ const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => {
const renderRadioGroup = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
return (
<RadioGroup defaultValue="add" colorScheme="blue" isDisabled={ isDisabled } { ...field }>
<RadioGroup defaultValue="add" colorScheme="blue" isDisabled={ isDisabled } isFocusable={ !isDisabled } { ...field } >
<Stack spacing={ 4 }>
{ VERIFICATION_METHODS.map((method) => {
{ methods.map((method) => {
return <Radio key={ method } value={ method } size="lg">{ renderItem(method) }</Radio>;
}) }
</Stack>
</RadioGroup>
);
}, [ isDisabled, renderItem ]);
}, [ isDisabled, methods, renderItem ]);
return (
<section>
......
......@@ -11,7 +11,7 @@ import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldOptimization = () => {
const [ isEnabled, setIsEnabled ] = React.useState(false);
const [ isEnabled, setIsEnabled ] = React.useState(true);
const { formState, control } = useFormContext<FormFields>();
const handleCheckboxChange = React.useCallback(() => {
......@@ -49,6 +49,7 @@ const ContractVerificationFieldOptimization = () => {
name="is_optimization_enabled"
control={ control }
render={ renderCheckboxControl }
defaultValue={ true }
/>
</ContractVerificationFormRow>
{ isEnabled && (
......@@ -58,7 +59,7 @@ const ContractVerificationFieldOptimization = () => {
control={ control }
render={ renderInputControl }
rules={{ required: true }}
defaultValue=""
defaultValue="200"
/>
</ContractVerificationFormRow>
) }
......
import { Text, Button, Box, chakra } from '@chakra-ui/react';
import { Text, Button, Box, chakra, Flex } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import { Mb } from 'lib/consts';
// import DragAndDropArea from 'ui/shared/forms/DragAndDropArea';
import FieldError from 'ui/shared/forms/FieldError';
import FileInput from 'ui/shared/forms/FileInput';
import FileSnippet from 'ui/shared/forms/FileSnippet';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
type FileTypes = '.sol' | '.yul' | '.json' | '.vy'
interface Props {
accept?: string;
fileTypes: Array<FileTypes>;
multiple?: boolean;
title: string;
className?: string;
hint: string;
}
const ContractVerificationFieldSources = ({ accept, multiple, title, className, hint }: Props) => {
const { setValue, getValues, control, formState } = useFormContext<FormFields>();
const ContractVerificationFieldSources = ({ fileTypes, multiple, title, className, hint }: Props) => {
const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>();
const error = 'sources' in formState.errors ? formState.errors.sources : undefined;
const commonError = !error?.type?.startsWith('file_') ? error : undefined;
const fileError = error?.type?.startsWith('file_') ? error : undefined;
const handleFileRemove = React.useCallback((index?: number) => {
if (index === undefined) {
......@@ -31,41 +38,89 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className,
const value = getValues('sources').slice();
value.splice(index, 1);
setValue('sources', value);
clearErrors('sources');
}, [ getValues, setValue ]);
}, [ getValues, clearErrors, setValue ]);
const renderFiles = React.useCallback((files: Array<File>) => {
const errorList = fileError?.message?.split(';');
return (
<Box display="grid" gridTemplateColumns={{ base: '1fr', lg: '1fr 1fr' }} columnGap={ 3 } rowGap={ 3 }>
<Box display="grid" gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }} columnGap={ 3 } rowGap={ 3 }>
{ files.map((file, index) => (
<FileSnippet
key={ file.name + file.lastModified }
file={ file }
maxW="initial"
onRemove={ handleFileRemove }
index={ index }
isDisabled={ formState.isSubmitting }
/>
<Box key={ file.name + file.lastModified + index }>
<FileSnippet
file={ file }
maxW="initial"
onRemove={ handleFileRemove }
index={ index }
isDisabled={ formState.isSubmitting }
/>
{ errorList?.[index] && <FieldError message={ errorList?.[index] } mt={ 1 } px={ 3 }/> }
</Box>
)) }
</Box>
);
}, [ formState.isSubmitting, handleFileRemove ]);
}, [ formState.isSubmitting, handleFileRemove, fileError ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => (
<>
<FileInput<FormFields, 'sources'> accept={ accept } multiple={ multiple } field={ field }>
<Button variant="outline" size="sm" display={ field.value && field.value.length > 0 ? 'none' : 'block' }>
Upload file{ multiple ? 's' : '' }
</Button>
<FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
{ () => (
<Flex
flexDir="column"
alignItems="flex-start"
rowGap={ 2 }
w="100%"
display={ field.value && field.value.length > 0 && !multiple ? 'none' : 'block' }
mb={ field.value && field.value.length > 0 ? 2 : 0 }
>
<Button
variant="outline"
size="sm"
// mb={ 2 }
>
Upload file{ multiple ? 's' : '' }
</Button>
{ /* design is not ready */ }
{ /* <DragAndDropArea onDrop={ onChange }/> */ }
</Flex>
) }
</FileInput>
{ field.value && field.value.length > 0 && renderFiles(field.value) }
{ error && (
<Box fontSize="sm" mt={ 2 } color="error">
{ error.type === 'required' ? 'Field is required' : error.message }
</Box>
) }
{ commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
</>
), [ accept, error, multiple, renderFiles ]);
), [ fileTypes, commonError, multiple, renderFiles ]);
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): 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);
if (errors.some((item) => item !== '')) {
return errors.join(';');
}
}
return true;
}, [ fileTypes ]);
const validateFileSize = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): 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.' : '');
if (errors.some((item) => item !== '')) {
return errors.join(';');
}
}
return true;
}, []);
const rules = React.useMemo(() => ({
required: true,
validate: {
file_type: validateFileType,
file_size: validateFileSize,
},
}), [ validateFileSize, validateFileType ]);
return (
<>
......@@ -77,7 +132,7 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className,
name="sources"
control={ control }
render={ renderControl }
rules={{ required: true }}
rules={ rules }
/>
{ hint ? <span>{ hint }</span> : null }
</ContractVerificationFormRow>
......
import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstArgs from '../fields/ContractVerificationFieldConstArgs';
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldIsYul from '../fields/ContractVerificationFieldIsYul';
import ContractVerificationFieldLibraries from '../fields/ContractVerificationFieldLibraries';
......@@ -19,7 +19,7 @@ const ContractVerificationFlattenSourceCode = () => {
<ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/>
<ContractVerificationFieldCode/>
<ContractVerificationFieldConstArgs/>
<ContractVerificationFieldAutodetectArgs/>
<ContractVerificationFieldLibraries/>
</ContractVerificationMethod>
);
......
......@@ -7,6 +7,8 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi
import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.sol' as const, '.yul' as const ];
const ContractVerificationMultiPartFile = () => {
return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification">
......@@ -14,7 +16,7 @@ const ContractVerificationMultiPartFile = () => {
<ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/>
<ContractVerificationFieldSources
accept=".sol,.yul"
fileTypes={ FILE_TYPES }
multiple
title="Sources *.sol or *.yul files"
hint="Upload all Solidity or Yul contract source files."
......
......@@ -3,11 +3,13 @@ import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const, '.sol' as const ];
const ContractVerificationSourcify = () => {
return (
<ContractVerificationMethod title="New Smart Contract Verification">
<ContractVerificationFieldSources
accept=".json"
fileTypes={ FILE_TYPES }
multiple
title="Sources and Metadata JSON" mt={ 0 }
hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation."
......
import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstArgs from '../fields/ContractVerificationFieldConstArgs';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const ];
const ContractVerificationStandardInput = () => {
return (
<ContractVerificationMethod title="New Smart Contract Verification">
<ContractVerificationFieldName/>
<ContractVerificationFieldCompiler/>
<ContractVerificationFieldSources
accept=".json"
fileTypes={ FILE_TYPES }
title="Standard Input JSON"
hint="Upload the standard input JSON file created during contract compilation."
/>
<ContractVerificationFieldConstArgs/>
<ContractVerificationFieldAutodetectArgs/>
</ContractVerificationMethod>
);
};
......
import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAbiEncodedArgs from '../fields/ContractVerificationFieldAbiEncodedArgs';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstructorArgs from '../fields/ContractVerificationFieldConstructorArgs';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
const ContractVerificationVyperContract = () => {
......@@ -12,7 +12,7 @@ const ContractVerificationVyperContract = () => {
<ContractVerificationFieldName hint="Must match the name specified in the code."/>
<ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldCode isVyper/>
<ContractVerificationFieldAbiEncodedArgs/>
<ContractVerificationFieldConstructorArgs/>
</ContractVerificationMethod>
);
};
......
import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.vy' as const ];
const ContractVerificationVyperMultiPartFile = () => {
return (
<ContractVerificationMethod title="New Vyper Smart Contract Verification">
<ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldEvmVersion isVyper/>
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
multiple
title="Sources *.vy files"
hint="Upload all Vyper contract source files."
/>
</ContractVerificationMethod>
);
};
export default React.memo(ContractVerificationVyperMultiPartFile);
import type { Option } from 'ui/shared/FancySelect/types';
export type VerificationMethod = 'flatten_source_code' | 'standard_input' | 'sourcify' | 'multi_part_file' | 'vyper_contract'
export interface ContractLibrary {
name: string;
address: string;
}
export interface FormFieldsFlattenSourceCode {
method: 'flatten_source_code';
method: 'flattened-code';
is_yul: boolean;
name: string;
compiler: Option;
......@@ -15,15 +13,18 @@ export interface FormFieldsFlattenSourceCode {
is_optimization_enabled: boolean;
optimization_runs: string;
code: string;
constructor_args: boolean;
autodetect_constructor_args: boolean;
constructor_args: string;
libraries: Array<ContractLibrary>;
}
export interface FormFieldsStandardInput {
method: 'standard_input';
method: 'standard-input';
name: string;
compiler: Option;
sources: Array<File>;
autodetect_constructor_args: boolean;
constructor_args: string;
}
export interface FormFieldsSourcify {
......@@ -32,21 +33,29 @@ export interface FormFieldsSourcify {
}
export interface FormFieldsMultiPartFile {
method: 'multi_part_file';
method: 'multi-part';
compiler: Option;
evm_version: Option;
is_optimization_enabled: boolean;
optimization_runs: string;
sources: Array<File>;
libraries: Array<ContractLibrary>;
}
export interface FormFieldsVyperContract {
method: 'vyper_contract';
method: 'vyper-code';
name: string;
compiler: Option;
code: string;
abi_encoded_args: string;
constructor_args: string;
}
export interface FormFieldsVyperMultiPartFile {
method: 'vyper-multi-part';
compiler: Option;
evm_version: Option;
sources: Array<File>;
}
export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify |
FormFieldsMultiPartFile | FormFieldsVyperContract;
FormFieldsMultiPartFile | FormFieldsVyperContract | FormFieldsVyperMultiPartFile;
import type { FieldPath, ErrorOption } from 'react-hook-form';
import type { ContractLibrary, FormFields } from './types';
import type { SmartContractVerificationMethod, SmartContractVerificationError } from 'types/api/contract';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMethod> = [
'flattened-code',
'standard-input',
'sourcify',
'multi-part',
'vyper-code',
'vyper-multi-part',
];
export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod {
return method && SUPPORTED_VERIFICATION_METHODS.includes(method as SmartContractVerificationMethod) ? true : false;
}
export function sortVerificationMethods(methodA: SmartContractVerificationMethod, methodB: SmartContractVerificationMethod) {
const indexA = SUPPORTED_VERIFICATION_METHODS.indexOf(methodA);
const indexB = SUPPORTED_VERIFICATION_METHODS.indexOf(methodB);
if (indexA > indexB) {
return 1;
}
if (indexA < indexB) {
return -1;
}
return 0;
}
export function prepareRequestBody(data: FormFields): FetchParams['body'] {
switch (data.method) {
case 'flattened-code': {
return {
compiler_version: data.compiler?.value,
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,
libraries: reduceLibrariesArray(data.libraries),
evm_version: data.evm_version?.value,
autodetect_constructor_args: data.autodetect_constructor_args,
constructor_args: data.constructor_args,
};
}
case 'standard-input': {
const body = new FormData();
body.set('compiler_version', data.compiler?.value);
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);
return body;
}
case 'sourcify': {
const body = new FormData();
addFilesToFormData(body, data.sources);
return body;
}
case 'multi-part': {
const body = new FormData();
body.set('compiler_version', data.compiler?.value);
body.set('evm_version', data.evm_version?.value);
body.set('is_optimization_enabled', String(Boolean(data.is_optimization_enabled)));
data.is_optimization_enabled && body.set('optimization_runs', data.optimization_runs);
const libraries = reduceLibrariesArray(data.libraries);
libraries && body.set('libraries', JSON.stringify(libraries));
addFilesToFormData(body, data.sources);
return body;
}
case 'vyper-code': {
return {
compiler_version: data.compiler?.value,
source_code: data.code,
contract_name: data.name,
constructor_args: data.constructor_args,
};
}
case 'vyper-multi-part': {
const body = new FormData();
body.set('compiler_version', data.compiler?.value);
body.set('evm_version', data.evm_version?.value);
addFilesToFormData(body, data.sources);
return body;
}
default: {
return {};
}
}
}
function reduceLibrariesArray(libraries: Array<ContractLibrary> | undefined) {
if (!libraries || libraries.length === 0) {
return;
}
return libraries.reduce<Record<string, string>>((result, item) => {
result[item.name] = item.address;
return result;
}, {});
}
function addFilesToFormData(body: FormData, files: Array<File> | undefined) {
if (!files) {
return;
}
for (let index = 0; index < files.length; index++) {
const file = files[index];
body.set(`files[${ index }]`, file, file.name);
}
}
const API_ERROR_TO_FORM_FIELD: Record<keyof SmartContractVerificationError, FieldPath<FormFields>> = {
contract_source_code: 'code',
files: 'sources',
compiler_version: 'compiler',
constructor_arguments: 'constructor_args',
name: 'name',
};
export function formatSocketErrors(errors: SmartContractVerificationError): Array<[FieldPath<FormFields>, ErrorOption]> {
return Object.entries(errors).map(([ key, value ]) => {
return [ API_ERROR_TO_FORM_FIELD[key as keyof SmartContractVerificationError], { message: value.join(',') } ];
});
}
......@@ -2,13 +2,19 @@ import { Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { SmartContractVerificationConfigRaw, SmartContractVerificationMethod } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import isBrowser from 'lib/isBrowser';
import link from 'lib/link/link';
import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm';
import { isValidVerificationMethod, sortVerificationMethods } from 'ui/contractVerification/utils';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
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';
......@@ -21,7 +27,31 @@ const ContractVerification = () => {
const router = useRouter();
const hash = router.query.id?.toString();
const method = router.query.id?.toString();
const method = router.query.id?.toString() as SmartContractVerificationMethod | undefined;
const contractQuery = useApiQuery('contract', {
pathParams: { id: hash },
queryOptions: {
enabled: Boolean(hash),
},
});
if (contractQuery.isError && contractQuery.error.status === 404) {
throw Error('Not found', { cause: contractQuery.error as unknown as Error });
}
const configQuery = useApiQuery('contract_verification_config', {
queryOptions: {
select: (data: unknown) => {
const _data = data as SmartContractVerificationConfigRaw;
return {
..._data,
verification_options: _data.verification_options.filter(isValidVerificationMethod).sort(sortVerificationMethods),
};
},
enabled: Boolean(hash),
},
});
React.useEffect(() => {
if (method && hash) {
......@@ -31,6 +61,32 @@ const ContractVerification = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
const isVerifiedContract = contractQuery.data?.is_verified;
React.useEffect(() => {
if (isVerifiedContract) {
router.push(link('address_index', { id: hash }, { tab: 'contract' }), undefined, { scroll: false, shallow: true });
}
}, [ hash, isVerifiedContract, router ]);
const content = (() => {
if (configQuery.isError || !hash || contractQuery.isError) {
return <DataFetchAlert/>;
}
if (configQuery.isLoading || contractQuery.isLoading || isVerifiedContract) {
return <ContentLoader/>;
}
return (
<ContractVerificationForm
method={ method && configQuery.data.verification_options.includes(method) ? method : undefined }
config={ configQuery.data }
hash={ hash }
/>
);
})();
return (
<Page>
<PageTitle
......@@ -39,7 +95,7 @@ const ContractVerification = () => {
backLinkLabel="Back to contract"
/>
{ hash && (
<Address>
<Address mb={ 12 }>
<AddressIcon address={{ hash, is_contract: true, implementation_name: null }} flexShrink={ 0 }/>
<Text fontFamily="heading" ml={ 2 } fontWeight={ 500 } fontSize="lg" w={{ base: '100%', lg: 'auto' }} whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ hash }/>
......@@ -47,7 +103,7 @@ const ContractVerification = () => {
<CopyToClipboard text={ hash }/>
</Address>
) }
<ContractVerificationForm/>
{ content }
</Page>
);
};
......
import { FormControl, useToken, useColorMode } from '@chakra-ui/react';
import type { Size, CSSObjectWithLabel, OptionsOrGroups, GroupBase, SingleValue, MultiValue } from 'chakra-react-select';
import { Select } from 'chakra-react-select';
import type { CSSObjectWithLabel, GroupBase, SingleValue, MultiValue, AsyncProps, Props as SelectProps } from 'chakra-react-select';
import { Select, AsyncSelect } from 'chakra-react-select';
import React from 'react';
import type { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form';
......@@ -9,20 +9,23 @@ import type { Option } from './types';
import { getChakraStyles } from 'ui/shared/FancySelect/utils';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
size?: Size; // we don't have styles for sm select/input with floating label yet
options: OptionsOrGroups<Option, GroupBase<Option>>;
placeholder: string;
name: string;
onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void;
onBlur?: () => void;
isDisabled?: boolean;
isRequired?: boolean;
interface CommonProps {
error?: Merge<FieldError, FieldErrorsImpl<Option>> | undefined;
value?: SingleValue<Option> | MultiValue<Option>;
}
const FancySelect = ({ size = 'md', options, placeholder, name, onChange, onBlur, isDisabled, isRequired, error, value }: Props) => {
interface RegularSelectProps extends SelectProps<Option, boolean, GroupBase<Option>>, CommonProps {
isAsync?: false;
onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void;
}
interface AsyncSelectProps extends AsyncProps<Option, boolean, GroupBase<Option>>, CommonProps {
isAsync: true;
onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void;
}
type Props = RegularSelectProps | AsyncSelectProps;
const FancySelect = (props: Props) => {
const menuZIndex = useToken('zIndices', 'dropdown');
const { colorMode } = useColorMode();
......@@ -32,34 +35,30 @@ const FancySelect = ({ size = 'md', options, placeholder, name, onChange, onBlur
const chakraStyles = React.useMemo(() => getChakraStyles(colorMode), [ colorMode ]);
const SelectComponent = props.isAsync ? AsyncSelect : Select;
return (
<FormControl
variant="floating"
size={ size }
isRequired={ isRequired }
{ ...(error ? { 'aria-invalid': true } : {}) }
{ ...(isDisabled ? { 'aria-disabled': true } : {}) }
{ ...(value ? { 'data-active': true } : {}) }
size={ props.size || 'md' }
isRequired={ props.isRequired }
{ ...(props.error ? { 'aria-invalid': true } : {}) }
{ ...(props.isDisabled ? { 'aria-disabled': true } : {}) }
{ ...(props.value ? { 'data-active': true } : {}) }
>
<Select
<SelectComponent
{ ...props }
size={ props.size || 'md' }
menuPortalTarget={ window.document.body }
placeholder=""
name={ name }
options={ options }
size={ size }
styles={ styles }
chakraStyles={ chakraStyles }
isInvalid={ Boolean(props.error) }
useBasicStyles
onChange={ onChange }
onBlur={ onBlur }
isDisabled={ isDisabled }
isRequired={ isRequired }
isInvalid={ Boolean(error) }
value={ value }
/>
<InputPlaceholder
text={ placeholder }
error={ error }
text={ typeof props.placeholder === 'string' ? props.placeholder : '' }
error={ props.error }
isFancy
/>
</FormControl>
......
......@@ -7,9 +7,10 @@ interface Props {
error?: Partial<FieldError>;
className?: string;
isFancy?: boolean;
isInModal?: boolean;
}
const InputPlaceholder = ({ text, error, className, isFancy }: Props) => {
const InputPlaceholder = ({ text, error, className, isFancy, isInModal }: Props) => {
let errorMessage = error?.message;
if (!errorMessage && error?.type === 'pattern') {
......@@ -20,6 +21,7 @@ const InputPlaceholder = ({ text, error, className, isFancy }: Props) => {
<FormLabel
className={ className }
{ ...(isFancy ? { 'data-fancy': true } : {}) }
{ ...(isInModal ? { 'data-in-modal': true } : {}) }
>
<chakra.span>{ text }</chakra.span>
{ errorMessage && <chakra.span order={ 3 } whiteSpace="pre"> - { errorMessage }</chakra.span> }
......
......@@ -35,7 +35,7 @@ const Page = ({
return <PageContent isHomePage={ isHomePage }>{ content }</PageContent>;
}
return isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ 500 }/>;
return isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode }/>;
}, [ isHomePage, wrapChildren ]);
const renderedChildren = wrapChildren ? (
......
import { Box } from '@chakra-ui/react';
import type { DragEvent } from 'react';
import React from 'react';
import { getAllFileEntries, convertFileEntryToFile } from './utils/files';
interface Props {
onDrop: (files: Array<File>) => void;
}
const DragAndDropArea = ({ onDrop }: Props) => {
const [ isDragOver, setIsDragOver ] = React.useState(false);
const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
const fileEntries = await getAllFileEntries(event.dataTransfer.items);
const files = await Promise.all(fileEntries.map(convertFileEntryToFile));
onDrop(files);
setIsDragOver(false);
}, [ onDrop ]);
const handleDragOver = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
}, []);
const handleDragEnter = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(true);
}, []);
const handleDragLeave = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(false);
}, []);
return (
<Box
w="100%"
h="200px"
bgColor="lightpink"
opacity={ isDragOver ? 0.8 : 1 }
onDrop={ handleDrop }
onDragOver={ handleDragOver }
onDragEnter={ handleDragEnter }
onDragLeave={ handleDragLeave }
/>
);
};
export default React.memo(DragAndDropArea);
import { Box, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
message: string;
className?: string;
}
const FieldError = ({ message, className }: Props) => {
return <Box className={ className } color="error" fontSize="sm" mt={ 2 } wordBreak="break-all">{ message }</Box>;
};
export default chakra(FieldError);
......@@ -3,8 +3,12 @@ import type { ChangeEvent } from 'react';
import React from 'react';
import type { ControllerRenderProps, FieldValues, Path } from 'react-hook-form';
interface InjectedProps {
onChange: (files: Array<File>) => void;
}
interface Props<V extends FieldValues, N extends Path<V>> {
children: React.ReactNode;
children: React.ReactNode | ((props: InjectedProps) => React.ReactNode);
field: ControllerRenderProps<V, N>;
accept?: string;
multiple?: boolean;
......@@ -13,6 +17,10 @@ interface Props<V extends FieldValues, N extends Path<V>> {
const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ children, accept, multiple, field }: Props<Values, Names>) => {
const ref = React.useRef<HTMLInputElement>(null);
const onChange = React.useCallback((files: Array<File>) => {
field.onChange([ ...(field.value || []), ...files ]);
}, [ field ]);
const handleInputChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
const fileList = event.target.files;
if (!fileList) {
......@@ -20,9 +28,9 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi
}
const files = Array.from(fileList);
field.onChange(files);
onChange(files);
field.onBlur();
}, [ field ]);
}, [ onChange, field ]);
const handleClick = React.useCallback(() => {
ref.current?.click();
......@@ -32,6 +40,12 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi
field.onBlur();
}, [ field ]);
const injectedProps = React.useMemo(() => ({
onChange,
}), [ onChange ]);
const content = typeof children === 'function' ? children(injectedProps) : children;
return (
<InputGroup onClick={ handleClick } onBlur={ handleInputBlur }>
<VisuallyHiddenInput
......@@ -42,7 +56,7 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi
multiple={ multiple }
name={ field.name }
/>
{ children }
{ content }
</InputGroup>
);
};
......
// Function to get all files in drop directory
export async function getAllFileEntries(dataTransferItemList: DataTransferItemList): Promise<Array<FileSystemFileEntry>> {
const fileEntries: Array<FileSystemFileEntry> = [];
// Use BFS to traverse entire directory/file structure
const queue: Array<FileSystemFileEntry | FileSystemDirectoryEntry> = [];
// Unfortunately dataTransferItemList is not iterable i.e. no forEach
for (let i = 0; i < dataTransferItemList.length; i++) {
// Note webkitGetAsEntry a non-standard feature and may change
// Usage is necessary for handling directories
// + typescript types are kinda wrong - https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
const item = dataTransferItemList[i].webkitGetAsEntry() as FileSystemFileEntry | FileSystemDirectoryEntry | null;
item && queue.push(item);
}
while (queue.length > 0) {
const entry = queue.shift();
if (entry?.isFile) {
fileEntries.push(entry as FileSystemFileEntry);
} else if (entry?.isDirectory && 'createReader' in entry) {
queue.push(...await readAllDirectoryEntries(entry.createReader()));
}
}
return fileEntries;
}
// Get all the entries (files or sub-directories) in a directory
// by calling readEntries until it returns empty array
async function readAllDirectoryEntries(directoryReader: DirectoryReader) {
const entries: Array<FileSystemFileEntry> = [];
let readEntries = await readEntriesPromise(directoryReader);
while (readEntries && readEntries.length > 0) {
entries.push(...readEntries);
readEntries = await readEntriesPromise(directoryReader);
}
return entries;
}
// Wrap readEntries in a promise to make working with readEntries easier
// readEntries will return only some of the entries in a directory
// e.g. Chrome returns at most 100 entries at a time
async function readEntriesPromise(directoryReader: DirectoryReader): Promise<Array<FileSystemFileEntry> | undefined> {
try {
return await new Promise((resolve, reject) => {
directoryReader.readEntries(
(fileEntry) => {
resolve(fileEntry as Array<FileSystemFileEntry>);
},
reject,
);
});
} catch (err) {}
}
export function convertFileEntryToFile(entry: FileSystemFileEntry): Promise<File> {
return new Promise((resolve) => {
entry.file(async(file: File) => {
// const newFile = new File([ file ], entry.fullPath, { lastModified: file.lastModified, type: file.type });
resolve(file);
});
});
}
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