Commit 4652c13f authored by tom goriunov's avatar tom goriunov Committed by GitHub

"Verify contract" page (#1394)

* route and dummy page

* form layout

* validate address on form submit

* fix form reset

* update screenshot

* fix ts errors
parent 47425b88
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.392 5.45c.252-.288.592-.45.948-.45h8.038a.63.63 0 0 1 .474.225l4.689 5.385a.83.83 0 0 1 .196.544v8.574l-.985 1.055-.355-.38v-8.666h-4.485a.702.702 0 0 1-.702-.702V6.538H7.34v16.924h9.44L18.217 25H7.34c-.356 0-.696-.162-.948-.45A1.661 1.661 0 0 1 6 23.461V6.538c0-.408.141-.799.392-1.087Zm9.222 1.678 2.791 3.205h-2.791V7.128ZM8.85 15.5a.65.65 0 0 1 .65-.65h7.2a.65.65 0 1 1 0 1.3H9.5a.65.65 0 0 1-.65-.65Zm0 2.4a.65.65 0 0 1 .65-.65h7.2a.65.65 0 1 1 0 1.3H9.5a.65.65 0 0 1-.65-.65Z" fill="currentColor"/>
<path d="m17.552 21.357 2.2 2.357 4.4-4.714" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
...@@ -24,6 +24,7 @@ import topAccountsIcon from 'icons/top-accounts.svg'; ...@@ -24,6 +24,7 @@ import topAccountsIcon from 'icons/top-accounts.svg';
import transactionsIcon from 'icons/transactions.svg'; import transactionsIcon from 'icons/transactions.svg';
import txnBatchIcon from 'icons/txn_batches.svg'; import txnBatchIcon from 'icons/txn_batches.svg';
import verifiedIcon from 'icons/verified.svg'; import verifiedIcon from 'icons/verified.svg';
import verifyContractIcon from 'icons/verify-contract.svg';
import watchlistIcon from 'icons/watchlist.svg'; import watchlistIcon from 'icons/watchlist.svg';
import { rightLineArrow } from 'lib/html-entities'; import { rightLineArrow } from 'lib/html-entities';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
...@@ -176,11 +177,19 @@ export default function useNavItems(): ReturnType { ...@@ -176,11 +177,19 @@ export default function useNavItems(): ReturnType {
isActive: apiNavItems.some(item => isInternalItem(item) && item.isActive), isActive: apiNavItems.some(item => isInternalItem(item) && item.isActive),
subItems: apiNavItems, subItems: apiNavItems,
}, },
config.UI.sidebar.otherLinks.length > 0 ? { {
text: 'Other', text: 'Other',
icon: gearIcon, icon: gearIcon,
subItems: config.UI.sidebar.otherLinks, subItems: [
} : null, {
text: 'Verify contract',
nextRoute: { pathname: '/contract-verification' as const },
icon: verifyContractIcon,
isActive: pathname.startsWith('/contract-verification'),
},
...config.UI.sidebar.otherLinks,
],
},
].filter(Boolean); ].filter(Boolean);
const accountNavItems: ReturnType['accountNavItems'] = [ const accountNavItems: ReturnType['accountNavItems'] = [
......
...@@ -12,6 +12,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -12,6 +12,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/accounts': 'Root page', '/accounts': 'Root page',
'/address/[hash]': 'Regular page', '/address/[hash]': 'Regular page',
'/verified-contracts': 'Root page', '/verified-contracts': 'Root page',
'/contract-verification': 'Root page',
'/address/[hash]/contract-verification': 'Regular page', '/address/[hash]/contract-verification': 'Regular page',
'/tokens': 'Root page', '/tokens': 'Root page',
'/token/[hash]': 'Regular page', '/token/[hash]': 'Regular page',
......
...@@ -15,6 +15,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -15,6 +15,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/accounts': DEFAULT_TEMPLATE, '/accounts': DEFAULT_TEMPLATE,
'/address/[hash]': 'View the account balance, transactions, and other data for %hash% on the %network_title%', '/address/[hash]': 'View the account balance, transactions, and other data for %hash% on the %network_title%',
'/verified-contracts': DEFAULT_TEMPLATE, '/verified-contracts': DEFAULT_TEMPLATE,
'/contract-verification': DEFAULT_TEMPLATE,
'/address/[hash]/contract-verification': 'View the account balance, transactions, and other data for %hash% on the %network_title%', '/address/[hash]/contract-verification': 'View the account balance, transactions, and other data for %hash% on the %network_title%',
'/tokens': DEFAULT_TEMPLATE, '/tokens': DEFAULT_TEMPLATE,
'/token/[hash]': '%hash%, balances and analytics on the %network_title%', '/token/[hash]': '%hash%, balances and analytics on the %network_title%',
......
...@@ -10,6 +10,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -10,6 +10,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/accounts': 'top accounts', '/accounts': 'top accounts',
'/address/[hash]': 'address details for %hash%', '/address/[hash]': 'address details for %hash%',
'/verified-contracts': 'verified contracts', '/verified-contracts': 'verified contracts',
'/contract-verification': 'verify contract',
'/address/[hash]/contract-verification': 'contract verification for %hash%', '/address/[hash]/contract-verification': 'contract verification for %hash%',
'/tokens': 'tokens', '/tokens': 'tokens',
'/token/[hash]': '%symbol% token details', '/token/[hash]': '%symbol% token details',
......
...@@ -10,7 +10,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -10,7 +10,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/accounts': 'Top accounts', '/accounts': 'Top accounts',
'/address/[hash]': 'Address details', '/address/[hash]': 'Address details',
'/verified-contracts': 'Verified contracts', '/verified-contracts': 'Verified contracts',
'/address/[hash]/contract-verification': 'Contract verification', '/contract-verification': 'Contract verification',
'/address/[hash]/contract-verification': 'Contract verification for address',
'/tokens': 'Tokens', '/tokens': 'Tokens',
'/token/[hash]': 'Token details', '/token/[hash]': 'Token details',
'/token/[hash]/instance/[id]': 'Token Instance', '/token/[hash]/instance/[id]': 'Token Instance',
......
...@@ -28,6 +28,7 @@ declare module "nextjs-routes" { ...@@ -28,6 +28,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/auth/unverified-email"> | StaticRoute<"/auth/unverified-email">
| DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }> | DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }>
| StaticRoute<"/blocks"> | StaticRoute<"/blocks">
| StaticRoute<"/contract-verification">
| StaticRoute<"/csv-export"> | StaticRoute<"/csv-export">
| StaticRoute<"/graphiql"> | StaticRoute<"/graphiql">
| StaticRoute<"/"> | StaticRoute<"/">
......
...@@ -4,12 +4,12 @@ import React from 'react'; ...@@ -4,12 +4,12 @@ import React from 'react';
import type { Props } from 'nextjs/getServerSideProps'; import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
import ContractVerification from 'ui/pages/ContractVerification'; import ContractVerificationForAddress from 'ui/pages/ContractVerificationForAddress';
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/address/[hash]/contract-verification" query={ props }> <PageNextJs pathname="/address/[hash]/contract-verification" query={ props }>
<ContractVerification/> <ContractVerificationForAddress/>
</PageNextJs> </PageNextJs>
); );
}; };
......
import type { NextPage } from 'next';
import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
import ContractVerification from 'ui/pages/ContractVerification';
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/contract-verification" query={ props }>
<ContractVerification/>
</PageNextJs>
);
};
export default Page;
export { base as getServerSideProps } from 'nextjs/getServerSideProps';
import { Button, chakra, useUpdateEffect } from '@chakra-ui/react'; import { Button, Grid, chakra, useUpdateEffect } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { FormFields } from './types'; import type { FormFields } from './types';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract'; import type { SmartContractVerificationMethod, SmartContractVerificationConfig, SmartContract } from 'types/api/contract';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import delay from 'lib/delay'; import delay from 'lib/delay';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import ContractVerificationFieldAddress from './fields/ContractVerificationFieldAddress';
import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod'; import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod';
import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode'; import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode';
import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile'; import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile';
...@@ -29,15 +31,15 @@ import { prepareRequestBody, formatSocketErrors, getDefaultValues, METHOD_LABELS ...@@ -29,15 +31,15 @@ import { prepareRequestBody, formatSocketErrors, getDefaultValues, METHOD_LABELS
interface Props { interface Props {
method?: SmartContractVerificationMethod; method?: SmartContractVerificationMethod;
config: SmartContractVerificationConfig; config: SmartContractVerificationConfig;
hash: string; hash?: string;
} }
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => { const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config) : undefined, defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config, hash) : undefined,
}); });
const { control, handleSubmit, watch, formState, setError, reset } = formApi; const { control, handleSubmit, watch, formState, setError, reset, getFieldState } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>(); const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const methodNameRef = React.useRef<string>(); const methodNameRef = React.useRef<string>();
...@@ -47,9 +49,28 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -47,9 +49,28 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
const body = prepareRequestBody(data); const body = prepareRequestBody(data);
if (!hash) {
try {
const response = await apiFetch<'contract', SmartContract>('contract', {
pathParams: { hash: data.address.toLowerCase() },
});
const isVerifiedContract = 'is_verified' in response && response?.is_verified && !response.is_partially_verified;
if (isVerifiedContract) {
setError('address', { message: 'Contract has already been verified' });
return Promise.resolve();
}
} catch (error) {
const statusCode = getErrorObjStatusCode(error);
const message = statusCode === 404 ? 'Address is not a smart contract' : 'Something went wrong';
setError('address', { message });
return Promise.resolve();
}
}
try { try {
await apiFetch('contract_verification_via', { await apiFetch('contract_verification_via', {
pathParams: { method: data.method.value, hash: hash.toLowerCase() }, pathParams: { method: data.method.value, hash: data.address.toLowerCase() },
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body, body,
...@@ -62,7 +83,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -62,7 +83,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
return new Promise((resolve) => { return new Promise((resolve) => {
submitPromiseResolver.current = resolve; submitPromiseResolver.current = resolve;
}); });
}, [ apiFetch, hash ]); }, [ apiFetch, hash, setError ]);
const address = watch('address');
const addressState = getFieldState('address');
const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback(async(payload) => { const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback(async(payload) => {
if (payload.status === 'error') { if (payload.status === 'error') {
...@@ -88,8 +112,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -88,8 +112,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
{ send_immediately: true }, { send_immediately: true },
); );
window.location.assign(route({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } })); window.location.assign(route({ pathname: '/address/[hash]', query: { hash: address, tab: 'contract' } }));
}, [ hash, setError, toast ]); }, [ setError, toast, address ]);
const handleSocketError = React.useCallback(() => { const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) { if (!formState.isSubmitting) {
...@@ -114,10 +138,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -114,10 +138,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
}, [ toast ]); }, [ toast ]);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`, topic: `addresses:${ address?.toLowerCase() }`,
onSocketClose: handleSocketError, onSocketClose: handleSocketError,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: false, isDisabled: Boolean(address && addressState.error),
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -142,7 +166,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -142,7 +166,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
useUpdateEffect(() => { useUpdateEffect(() => {
if (methodValue) { if (methodValue) {
reset(getDefaultValues(methodValue, config)); reset(getDefaultValues(methodValue, config, address || hash));
const methodName = METHOD_LABELS[methodValue]; const methodName = METHOD_LABELS[methodValue];
mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Method selected', Method: methodName }); mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Method selected', Method: methodName });
...@@ -157,11 +181,14 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -157,11 +181,14 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
noValidate noValidate
onSubmit={ handleSubmit(onFormSubmit) } onSubmit={ handleSubmit(onFormSubmit) }
> >
<ContractVerificationFieldMethod <Grid as="section" columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
control={ control } { !hash && <ContractVerificationFieldAddress/> }
methods={ config.verification_options } <ContractVerificationFieldMethod
isDisabled={ formState.isSubmitting } control={ control }
/> methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/>
</Grid>
{ content } { content }
{ Boolean(method) && ( { Boolean(method) && (
<Button <Button
......
import { FormControl, Input, chakra } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import { ADDRESS_REGEXP, ADDRESS_LENGTH } from 'lib/validations/address';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props {
isReadOnly?: boolean;
}
const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Smart contract / Address (0x...)" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return (
<>
<ContractVerificationFormRow>
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
Contract address to verify
</chakra.span>
</ContractVerificationFormRow>
<ContractVerificationFormRow mb={ 3 }>
<Controller
name="address"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: ADDRESS_REGEXP }}
/>
</ContractVerificationFormRow>
</>
);
};
export default React.memo(ContractVerificationFieldAddress);
...@@ -12,8 +12,6 @@ import { ...@@ -12,8 +12,6 @@ import {
DarkMode, DarkMode,
ListItem, ListItem,
OrderedList, OrderedList,
Grid,
Box,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form'; import type { ControllerRenderProps, Control } from 'react-hook-form';
...@@ -99,43 +97,40 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props ...@@ -99,43 +97,40 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
}, []); }, []);
return ( return (
<section> <>
<Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}> <div>
<div> <chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
<Box mb={ 5 }> Currently, Blockscout supports { methods.length } contract verification methods
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading"> </chakra.span>
Currently, Blockscout supports { methods.length } contract verification methods <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">
<Icon as={ infoIcon } boxSize={ 5 } color="link" _hover={{ color: 'link_hovered' }}/>
</chakra.span> </chakra.span>
<Popover trigger="hover" isLazy placement={ isMobile ? 'bottom-end' : 'right-start' } offset={ [ -8, 8 ] }> </PopoverTrigger>
<PopoverTrigger> <Portal>
<chakra.span display="inline-block" ml={ 1 } cursor="pointer" verticalAlign="middle" h="22px"> <PopoverContent bgColor={ tooltipBg } w={{ base: '300px', lg: '380px' }}>
<Icon as={ infoIcon } boxSize={ 5 } color="link" _hover={{ color: 'link_hovered' }}/> <PopoverArrow bgColor={ tooltipBg }/>
</chakra.span> <PopoverBody color="white">
</PopoverTrigger> <DarkMode>
<Portal> <span>Currently, Blockscout supports { methods.length } methods:</span>
<PopoverContent bgColor={ tooltipBg } w={{ base: '300px', lg: '380px' }}> <OrderedList>
<PopoverArrow bgColor={ tooltipBg }/> { methods.map(renderPopoverListItem) }
<PopoverBody color="white"> </OrderedList>
<DarkMode> </DarkMode>
<span>Currently, Blockscout supports { methods.length } methods:</span> </PopoverBody>
<OrderedList> </PopoverContent>
{ methods.map(renderPopoverListItem) } </Portal>
</OrderedList> </Popover>
</DarkMode> </div>
</PopoverBody> <div/>
</PopoverContent> <Controller
</Portal> name="method"
</Popover> control={ control }
</Box> render={ renderControl }
<Controller rules={{ required: true }}
name="method" />
control={ control } </>
render={ renderControl }
rules={{ required: true }}
/>
</div>
</Grid>
</section>
); );
}; };
......
...@@ -12,6 +12,7 @@ interface MethodOption { ...@@ -12,6 +12,7 @@ interface MethodOption {
} }
export interface FormFieldsFlattenSourceCode { export interface FormFieldsFlattenSourceCode {
address: string;
method: MethodOption; method: MethodOption;
is_yul: boolean; is_yul: boolean;
name: string | undefined; name: string | undefined;
...@@ -26,6 +27,7 @@ export interface FormFieldsFlattenSourceCode { ...@@ -26,6 +27,7 @@ export interface FormFieldsFlattenSourceCode {
} }
export interface FormFieldsStandardInput { export interface FormFieldsStandardInput {
address: string;
method: MethodOption; method: MethodOption;
name: string; name: string;
compiler: Option | null; compiler: Option | null;
...@@ -35,12 +37,14 @@ export interface FormFieldsStandardInput { ...@@ -35,12 +37,14 @@ export interface FormFieldsStandardInput {
} }
export interface FormFieldsSourcify { export interface FormFieldsSourcify {
address: string;
method: MethodOption; method: MethodOption;
sources: Array<File>; sources: Array<File>;
contract_index?: Option; contract_index?: Option;
} }
export interface FormFieldsMultiPartFile { export interface FormFieldsMultiPartFile {
address: string;
method: MethodOption; method: MethodOption;
compiler: Option | null; compiler: Option | null;
evm_version: Option | null; evm_version: Option | null;
...@@ -51,6 +55,7 @@ export interface FormFieldsMultiPartFile { ...@@ -51,6 +55,7 @@ export interface FormFieldsMultiPartFile {
} }
export interface FormFieldsVyperContract { export interface FormFieldsVyperContract {
address: string;
method: MethodOption; method: MethodOption;
name: string; name: string;
evm_version: Option | null; evm_version: Option | null;
...@@ -60,6 +65,7 @@ export interface FormFieldsVyperContract { ...@@ -60,6 +65,7 @@ export interface FormFieldsVyperContract {
} }
export interface FormFieldsVyperMultiPartFile { export interface FormFieldsVyperMultiPartFile {
address: string;
method: MethodOption; method: MethodOption;
compiler: Option | null; compiler: Option | null;
evm_version: Option | null; evm_version: Option | null;
...@@ -68,6 +74,7 @@ export interface FormFieldsVyperMultiPartFile { ...@@ -68,6 +74,7 @@ export interface FormFieldsVyperMultiPartFile {
} }
export interface FormFieldsVyperStandardInput { export interface FormFieldsVyperStandardInput {
address: string;
method: MethodOption; method: MethodOption;
compiler: Option | null; compiler: Option | null;
sources: Array<File>; sources: Array<File>;
......
import useApiQuery from 'lib/api/useApiQuery';
import { isValidVerificationMethod, sortVerificationMethods } from './utils';
export default function useFormConfigQuery(enabled: boolean) {
return useApiQuery('contract_verification_config', {
queryOptions: {
select: (data) => {
return {
...data,
verification_options: data.verification_options.filter(isValidVerificationMethod).sort(sortVerificationMethods),
};
},
enabled,
},
});
}
...@@ -37,6 +37,7 @@ export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = { ...@@ -37,6 +37,7 @@ export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = {
export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> = { export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> = {
'flattened-code': { 'flattened-code': {
address: '',
method: { method: {
value: 'flattened-code' as const, value: 'flattened-code' as const,
label: METHOD_LABELS['flattened-code'], label: METHOD_LABELS['flattened-code'],
...@@ -53,6 +54,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -53,6 +54,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
libraries: [], libraries: [],
}, },
'standard-input': { 'standard-input': {
address: '',
method: { method: {
value: 'standard-input' as const, value: 'standard-input' as const,
label: METHOD_LABELS['standard-input'], label: METHOD_LABELS['standard-input'],
...@@ -64,6 +66,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -64,6 +66,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
constructor_args: '', constructor_args: '',
}, },
sourcify: { sourcify: {
address: '',
method: { method: {
value: 'sourcify' as const, value: 'sourcify' as const,
label: METHOD_LABELS.sourcify, label: METHOD_LABELS.sourcify,
...@@ -71,6 +74,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -71,6 +74,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
sources: [], sources: [],
}, },
'multi-part': { 'multi-part': {
address: '',
method: { method: {
value: 'multi-part' as const, value: 'multi-part' as const,
label: METHOD_LABELS['multi-part'], label: METHOD_LABELS['multi-part'],
...@@ -83,6 +87,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -83,6 +87,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
libraries: [], libraries: [],
}, },
'vyper-code': { 'vyper-code': {
address: '',
method: { method: {
value: 'vyper-code' as const, value: 'vyper-code' as const,
label: METHOD_LABELS['vyper-code'], label: METHOD_LABELS['vyper-code'],
...@@ -94,6 +99,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -94,6 +99,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
constructor_args: '', constructor_args: '',
}, },
'vyper-multi-part': { 'vyper-multi-part': {
address: '',
method: { method: {
value: 'vyper-multi-part' as const, value: 'vyper-multi-part' as const,
label: METHOD_LABELS['vyper-multi-part'], label: METHOD_LABELS['vyper-multi-part'],
...@@ -103,6 +109,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -103,6 +109,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
sources: [], sources: [],
}, },
'vyper-standard-input': { 'vyper-standard-input': {
address: '',
method: { method: {
value: 'vyper-standard-input' as const, value: 'vyper-standard-input' as const,
label: METHOD_LABELS['vyper-standard-input'], label: METHOD_LABELS['vyper-standard-input'],
...@@ -112,8 +119,8 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -112,8 +119,8 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
}, },
}; };
export function getDefaultValues(method: SmartContractVerificationMethod, config: SmartContractVerificationConfig) { export function getDefaultValues(method: SmartContractVerificationMethod, config: SmartContractVerificationConfig, hash?: string) {
const defaultValues = DEFAULT_VALUES[method]; const defaultValues = { ...DEFAULT_VALUES[method], address: hash };
if ('evm_version' in defaultValues) { if ('evm_version' in defaultValues) {
if (method === 'flattened-code' || method === 'multi-part') { if (method === 'flattened-code' || method === 'multi-part') {
......
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SmartContractVerificationMethod } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm'; import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm';
import { isValidVerificationMethod, sortVerificationMethods } from 'ui/contractVerification/utils'; import useFormConfigQuery from 'ui/contractVerification/useFormConfigQuery';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
const ContractVerification = () => { const ContractVerification = () => {
const appProps = useAppContext(); const configQuery = useFormConfigQuery(true);
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const method = getQueryParamString(router.query.method) as SmartContractVerificationMethod;
const contractQuery = useApiQuery('contract', {
pathParams: { 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) => {
return {
...data,
verification_options: data.verification_options.filter(isValidVerificationMethod).sort(sortVerificationMethods),
};
},
enabled: Boolean(hash),
},
});
React.useEffect(() => {
if (method && hash) {
router.replace({ pathname: '/address/[hash]/contract-verification', query: { hash } }, undefined, { scroll: false, shallow: true });
}
// onMount only
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
const isVerifiedContract = contractQuery.data?.is_verified && !contractQuery.data.is_partially_verified;
React.useEffect(() => {
if (isVerifiedContract) {
router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { scroll: false, shallow: true });
}
}, [ hash, isVerifiedContract, router ]);
const content = (() => { const content = (() => {
if (configQuery.isError || !hash || contractQuery.isError) { if (configQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (configQuery.isPending || contractQuery.isPending || isVerifiedContract) { if (configQuery.isPending) {
return <ContentLoader/>; return <ContentLoader/>;
} }
return ( return (
<ContractVerificationForm <ContractVerificationForm config={ configQuery.data }/>
method={ method && configQuery.data.verification_options.includes(method) ? method : undefined }
config={ configQuery.data }
hash={ hash }
/>
); );
})(); })();
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to contract',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return ( return (
<> <>
<PageTitle <PageTitle title="Verify & publish contract"/>
title="New smart contract verification"
backLink={ backLink }
/>
<AddressEntity
address={{ hash, is_contract: true, implementation_name: null }}
noLink
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
mb={ 12 }
/>
{ content } { content }
</> </>
); );
......
import { useRouter } from 'next/router';
import React from 'react';
import type { SmartContractVerificationMethod } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm';
import useFormConfigQuery from 'ui/contractVerification/useFormConfigQuery';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import PageTitle from 'ui/shared/Page/PageTitle';
const ContractVerificationForAddress = () => {
const appProps = useAppContext();
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const method = getQueryParamString(router.query.method) as SmartContractVerificationMethod;
const contractQuery = useApiQuery('contract', {
pathParams: { 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 = useFormConfigQuery(Boolean(hash));
React.useEffect(() => {
if (method && hash) {
router.replace({ pathname: '/address/[hash]/contract-verification', query: { hash } }, undefined, { scroll: false, shallow: true });
}
// onMount only
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
const isVerifiedContract = contractQuery.data?.is_verified && !contractQuery.data.is_partially_verified;
React.useEffect(() => {
if (isVerifiedContract) {
router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { scroll: false, shallow: true });
}
}, [ hash, isVerifiedContract, router ]);
const content = (() => {
if (configQuery.isError || !hash || contractQuery.isError) {
return <DataFetchAlert/>;
}
if (configQuery.isPending || contractQuery.isPending || isVerifiedContract) {
return <ContentLoader/>;
}
return (
<ContractVerificationForm
method={ method && configQuery.data.verification_options.includes(method) ? method : undefined }
config={ configQuery.data }
hash={ hash }
/>
);
})();
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to contract',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return (
<>
<PageTitle
title="New smart contract verification"
backLink={ backLink }
/>
<AddressEntity
address={{ hash, is_contract: true, implementation_name: null }}
noLink
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
mb={ 12 }
/>
{ content }
</>
);
};
export default ContractVerificationForAddress;
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