Commit 25bd3615 authored by tom's avatar tom

error handling

parent 77621de2
...@@ -2,6 +2,7 @@ import type { Channel } from 'phoenix'; ...@@ -2,6 +2,7 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -45,6 +46,6 @@ export namespace SocketMessage { ...@@ -45,6 +46,6 @@ export namespace SocketMessage {
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; 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', unknown>; export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -129,3 +129,18 @@ export interface SmartContractVerificationConfigRaw { ...@@ -129,3 +129,18 @@ export interface SmartContractVerificationConfigRaw {
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw { export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
verification_options: Array<SmartContractVerificationMethod>; 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>;
}
...@@ -8,6 +8,7 @@ import type { SocketMessage } from 'lib/socket/types'; ...@@ -8,6 +8,7 @@ import type { SocketMessage } from 'lib/socket/types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract'; import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -18,7 +19,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify ...@@ -18,7 +19,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput'; import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract'; import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile'; import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, METHOD_TO_ENDPOINT_MAP } from './utils'; import { prepareRequestBody, METHOD_TO_ENDPOINT_MAP, formatSocketErrors } from './utils';
const METHOD_COMPONENTS = { const METHOD_COMPONENTS = {
flattened_code: <ContractVerificationFlattenSourceCode/>, flattened_code: <ContractVerificationFlattenSourceCode/>,
...@@ -42,10 +43,11 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -42,10 +43,11 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
method: methodFromQuery, method: methodFromQuery,
}, },
}); });
const { control, handleSubmit, watch, formState } = formApi; const { control, handleSubmit, watch, formState, setError } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>(); const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast();
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
...@@ -72,22 +74,40 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -72,22 +74,40 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback((payload) => { const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback((payload) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('__>__', payload); console.log('__>__ handleNewSocketMessage', payload);
}, []);
if (payload.status === 'error') {
const errors = formatSocketErrors(payload.errors);
errors.forEach(([ field, error ]) => setError(field, error));
}
submitPromiseResolver.current?.(null);
}, [ setError ]);
const handleSocketError = React.useCallback(() => { const handleSocketError = React.useCallback(() => {
submitPromiseResolver.current?.(null); 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,
});
}, [ toast ]);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `address:${ hash }`, topic: `addresses:${ hash.toLowerCase() }`,
onSocketClose: handleSocketError, onSocketClose: handleSocketError,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: !formState.isSubmitting, isDisabled: !formState.isSubmitting,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
event: 'verification', event: 'verification_result',
handler: handleNewSocketMessage, handler: handleNewSocketMessage,
}); });
......
...@@ -5,6 +5,7 @@ import { useFormContext, Controller } from 'react-hook-form'; ...@@ -5,6 +5,7 @@ import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -27,7 +28,8 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => { ...@@ -27,7 +28,8 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => {
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
required required
/> />
<InputPlaceholder text="Contract code" error={ error }/> <InputPlaceholder text="Contract code"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl> </FormControl>
); );
}, [ formState.errors, formState.isSubmitting ]); }, [ formState.errors, formState.isSubmitting ]);
......
...@@ -5,6 +5,7 @@ import { Controller, useFormContext } from 'react-hook-form'; ...@@ -5,6 +5,7 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -13,17 +14,22 @@ const ContractVerificationFieldConstructorArgs = () => { ...@@ -13,17 +14,22 @@ const ContractVerificationFieldConstructorArgs = () => {
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'constructor_args'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'constructor_args'>}) => {
const error = 'constructor_args' in formState.errors ? formState.errors.constructor_args : undefined;
return ( 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 <Textarea
{ ...field } { ...field }
maxLength={ 255 } maxLength={ 255 }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
isInvalid={ Boolean(error) }
required
/> />
<InputPlaceholder text="ABI-encoded Constructor Arguments"/> <InputPlaceholder text="ABI-encoded Constructor Arguments"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl> </FormControl>
); );
}, [ formState.isSubmitting ]); }, [ formState.errors, formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
...@@ -31,6 +37,7 @@ const ContractVerificationFieldConstructorArgs = () => { ...@@ -31,6 +37,7 @@ const ContractVerificationFieldConstructorArgs = () => {
name="constructor_args" name="constructor_args"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }}
/> />
<> <>
<span>Add arguments in </span> <span>Add arguments in </span>
......
import type { FieldPath, ErrorOption } from 'react-hook-form';
import type { ContractLibrary, FormFields } from './types'; import type { ContractLibrary, FormFields } from './types';
import type { SmartContractVerificationMethod } from 'types/api/contract'; import type { SmartContractVerificationMethod, SmartContractVerificationError } from 'types/api/contract';
import type { Params as FetchParams } from 'lib/hooks/useFetch'; import type { Params as FetchParams } from 'lib/hooks/useFetch';
...@@ -44,13 +46,13 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -44,13 +46,13 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
switch (data.method) { switch (data.method) {
case 'flattened_code': { case 'flattened_code': {
return { return {
compiler_version: data.compiler.value, compiler_version: data.compiler?.value,
source_code: data.code, source_code: data.code,
is_optimization_enabled: data.is_optimization_enabled, is_optimization_enabled: data.is_optimization_enabled,
optimization_runs: data.optimization_runs, optimization_runs: data.optimization_runs,
contract_name: data.name, contract_name: data.name,
libraries: reduceLibrariesArray(data.libraries), libraries: reduceLibrariesArray(data.libraries),
evm_version: data.evm_version.value, evm_version: data.evm_version?.value,
autodetect_constructor_args: data.autodetect_constructor_args, autodetect_constructor_args: data.autodetect_constructor_args,
constructor_args: data.constructor_args, constructor_args: data.constructor_args,
}; };
...@@ -58,7 +60,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -58,7 +60,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
case 'standard_input': { case 'standard_input': {
const body = new FormData(); const body = new FormData();
body.set('compiler_version', data.compiler.value); body.set('compiler_version', data.compiler?.value);
body.set('contract_name', data.name); body.set('contract_name', data.name);
body.set('autodetect_constructor_args', String(Boolean(data.autodetect_constructor_args))); body.set('autodetect_constructor_args', String(Boolean(data.autodetect_constructor_args)));
body.set('constructor_args', data.constructor_args); body.set('constructor_args', data.constructor_args);
...@@ -76,8 +78,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -76,8 +78,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
case 'multi_part': { case 'multi_part': {
const body = new FormData(); const body = new FormData();
body.set('compiler_version', data.compiler.value); body.set('compiler_version', data.compiler?.value);
body.set('evm_version', data.evm_version.value); body.set('evm_version', data.evm_version?.value);
body.set('is_optimization_enabled', String(Boolean(data.is_optimization_enabled))); body.set('is_optimization_enabled', String(Boolean(data.is_optimization_enabled)));
data.is_optimization_enabled && body.set('optimization_runs', data.optimization_runs); data.is_optimization_enabled && body.set('optimization_runs', data.optimization_runs);
...@@ -90,7 +92,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -90,7 +92,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
case 'vyper_code': { case 'vyper_code': {
return { return {
compiler_version: data.compiler.value, compiler_version: data.compiler?.value,
source_code: data.code, source_code: data.code,
contract_name: data.name, contract_name: data.name,
constructor_args: data.constructor_args, constructor_args: data.constructor_args,
...@@ -99,8 +101,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -99,8 +101,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
case 'vyper_multi_part': { case 'vyper_multi_part': {
const body = new FormData(); const body = new FormData();
body.set('compiler_version', data.compiler.value); body.set('compiler_version', data.compiler?.value);
body.set('evm_version', data.evm_version.value); body.set('evm_version', data.evm_version?.value);
addFilesToFormData(body, data.sources); addFilesToFormData(body, data.sources);
return body; return body;
...@@ -123,9 +125,27 @@ function reduceLibrariesArray(libraries: Array<ContractLibrary> | undefined) { ...@@ -123,9 +125,27 @@ function reduceLibrariesArray(libraries: Array<ContractLibrary> | undefined) {
}, {}); }, {});
} }
function addFilesToFormData(body: FormData, files: Array<File>) { function addFilesToFormData(body: FormData, files: Array<File> | undefined) {
if (!files) {
return;
}
for (let index = 0; index < files.length; index++) { for (let index = 0; index < files.length; index++) {
const file = files[index]; const file = files[index];
body.set(`files[${ index }]`, file, file.name); 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(',') } ];
});
}
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);
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