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';
import transactionsIcon from 'icons/transactions.svg';
import txnBatchIcon from 'icons/txn_batches.svg';
import verifiedIcon from 'icons/verified.svg';
import verifyContractIcon from 'icons/verify-contract.svg';
import watchlistIcon from 'icons/watchlist.svg';
import { rightLineArrow } from 'lib/html-entities';
import UserAvatar from 'ui/shared/UserAvatar';
......@@ -176,11 +177,19 @@ export default function useNavItems(): ReturnType {
isActive: apiNavItems.some(item => isInternalItem(item) && item.isActive),
subItems: apiNavItems,
},
config.UI.sidebar.otherLinks.length > 0 ? {
{
text: 'Other',
icon: gearIcon,
subItems: config.UI.sidebar.otherLinks,
} : null,
subItems: [
{
text: 'Verify contract',
nextRoute: { pathname: '/contract-verification' as const },
icon: verifyContractIcon,
isActive: pathname.startsWith('/contract-verification'),
},
...config.UI.sidebar.otherLinks,
],
},
].filter(Boolean);
const accountNavItems: ReturnType['accountNavItems'] = [
......
......@@ -12,6 +12,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/accounts': 'Root page',
'/address/[hash]': 'Regular page',
'/verified-contracts': 'Root page',
'/contract-verification': 'Root page',
'/address/[hash]/contract-verification': 'Regular page',
'/tokens': 'Root page',
'/token/[hash]': 'Regular page',
......
......@@ -15,6 +15,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/accounts': DEFAULT_TEMPLATE,
'/address/[hash]': 'View the account balance, transactions, and other data for %hash% on the %network_title%',
'/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%',
'/tokens': DEFAULT_TEMPLATE,
'/token/[hash]': '%hash%, balances and analytics on the %network_title%',
......
......@@ -10,6 +10,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/accounts': 'top accounts',
'/address/[hash]': 'address details for %hash%',
'/verified-contracts': 'verified contracts',
'/contract-verification': 'verify contract',
'/address/[hash]/contract-verification': 'contract verification for %hash%',
'/tokens': 'tokens',
'/token/[hash]': '%symbol% token details',
......
......@@ -10,7 +10,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/accounts': 'Top accounts',
'/address/[hash]': 'Address details',
'/verified-contracts': 'Verified contracts',
'/address/[hash]/contract-verification': 'Contract verification',
'/contract-verification': 'Contract verification',
'/address/[hash]/contract-verification': 'Contract verification for address',
'/tokens': 'Tokens',
'/token/[hash]': 'Token details',
'/token/[hash]/instance/[id]': 'Token Instance',
......
......@@ -28,6 +28,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/auth/unverified-email">
| DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }>
| StaticRoute<"/blocks">
| StaticRoute<"/contract-verification">
| StaticRoute<"/csv-export">
| StaticRoute<"/graphiql">
| StaticRoute<"/">
......
......@@ -4,12 +4,12 @@ import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
import ContractVerification from 'ui/pages/ContractVerification';
import ContractVerificationForAddress from 'ui/pages/ContractVerificationForAddress';
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/address/[hash]/contract-verification" query={ props }>
<ContractVerification/>
<ContractVerificationForAddress/>
</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 type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
import type { FormFields } from './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 useApiFetch from 'lib/api/useApiFetch';
import delay from 'lib/delay';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel/index';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ContractVerificationFieldAddress from './fields/ContractVerificationFieldAddress';
import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod';
import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode';
import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile';
......@@ -29,15 +31,15 @@ import { prepareRequestBody, formatSocketErrors, getDefaultValues, METHOD_LABELS
interface Props {
method?: SmartContractVerificationMethod;
config: SmartContractVerificationConfig;
hash: string;
hash?: string;
}
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({
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 methodNameRef = React.useRef<string>();
......@@ -47,9 +49,28 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(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 {
await apiFetch('contract_verification_via', {
pathParams: { method: data.method.value, hash: hash.toLowerCase() },
pathParams: { method: data.method.value, hash: data.address.toLowerCase() },
fetchParams: {
method: 'POST',
body,
......@@ -62,7 +83,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
return new Promise((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) => {
if (payload.status === 'error') {
......@@ -88,8 +112,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
{ send_immediately: true },
);
window.location.assign(route({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }));
}, [ hash, setError, toast ]);
window.location.assign(route({ pathname: '/address/[hash]', query: { hash: address, tab: 'contract' } }));
}, [ setError, toast, address ]);
const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) {
......@@ -114,10 +138,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
}, [ toast ]);
const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`,
topic: `addresses:${ address?.toLowerCase() }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: false,
isDisabled: Boolean(address && addressState.error),
});
useSocketMessage({
channel,
......@@ -142,7 +166,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
useUpdateEffect(() => {
if (methodValue) {
reset(getDefaultValues(methodValue, config));
reset(getDefaultValues(methodValue, config, address || hash));
const methodName = METHOD_LABELS[methodValue];
mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Method selected', Method: methodName });
......@@ -157,11 +181,14 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
noValidate
onSubmit={ handleSubmit(onFormSubmit) }
>
<ContractVerificationFieldMethod
control={ control }
methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/>
<Grid as="section" columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
{ !hash && <ContractVerificationFieldAddress/> }
<ContractVerificationFieldMethod
control={ control }
methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/>
</Grid>
{ content }
{ Boolean(method) && (
<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 {
DarkMode,
ListItem,
OrderedList,
Grid,
Box,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form';
......@@ -99,43 +97,40 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
}, []);
return (
<section>
<Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
<div>
<Box mb={ 5 }>
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
Currently, Blockscout supports { methods.length } contract verification methods
<>
<div>
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
Currently, Blockscout supports { methods.length } contract verification methods
</chakra.span>
<Popover trigger="hover" isLazy placement={ isMobile ? 'bottom-end' : 'right-start' } offset={ [ -8, 8 ] }>
<PopoverTrigger>
<chakra.span display="inline-block" ml={ 1 } cursor="pointer" verticalAlign="middle" h="22px">
<Icon as={ infoIcon } boxSize={ 5 } color="link" _hover={{ color: 'link_hovered' }}/>
</chakra.span>
<Popover trigger="hover" isLazy placement={ isMobile ? 'bottom-end' : 'right-start' } offset={ [ -8, 8 ] }>
<PopoverTrigger>
<chakra.span display="inline-block" ml={ 1 } cursor="pointer" verticalAlign="middle" h="22px">
<Icon as={ infoIcon } boxSize={ 5 } color="link" _hover={{ color: 'link_hovered' }}/>
</chakra.span>
</PopoverTrigger>
<Portal>
<PopoverContent bgColor={ tooltipBg } w={{ base: '300px', lg: '380px' }}>
<PopoverArrow bgColor={ tooltipBg }/>
<PopoverBody color="white">
<DarkMode>
<span>Currently, Blockscout supports { methods.length } methods:</span>
<OrderedList>
{ methods.map(renderPopoverListItem) }
</OrderedList>
</DarkMode>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
</Box>
<Controller
name="method"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
</div>
</Grid>
</section>
</PopoverTrigger>
<Portal>
<PopoverContent bgColor={ tooltipBg } w={{ base: '300px', lg: '380px' }}>
<PopoverArrow bgColor={ tooltipBg }/>
<PopoverBody color="white">
<DarkMode>
<span>Currently, Blockscout supports { methods.length } methods:</span>
<OrderedList>
{ methods.map(renderPopoverListItem) }
</OrderedList>
</DarkMode>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
</div>
<div/>
<Controller
name="method"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
</>
);
};
......
......@@ -12,6 +12,7 @@ interface MethodOption {
}
export interface FormFieldsFlattenSourceCode {
address: string;
method: MethodOption;
is_yul: boolean;
name: string | undefined;
......@@ -26,6 +27,7 @@ export interface FormFieldsFlattenSourceCode {
}
export interface FormFieldsStandardInput {
address: string;
method: MethodOption;
name: string;
compiler: Option | null;
......@@ -35,12 +37,14 @@ export interface FormFieldsStandardInput {
}
export interface FormFieldsSourcify {
address: string;
method: MethodOption;
sources: Array<File>;
contract_index?: Option;
}
export interface FormFieldsMultiPartFile {
address: string;
method: MethodOption;
compiler: Option | null;
evm_version: Option | null;
......@@ -51,6 +55,7 @@ export interface FormFieldsMultiPartFile {
}
export interface FormFieldsVyperContract {
address: string;
method: MethodOption;
name: string;
evm_version: Option | null;
......@@ -60,6 +65,7 @@ export interface FormFieldsVyperContract {
}
export interface FormFieldsVyperMultiPartFile {
address: string;
method: MethodOption;
compiler: Option | null;
evm_version: Option | null;
......@@ -68,6 +74,7 @@ export interface FormFieldsVyperMultiPartFile {
}
export interface FormFieldsVyperStandardInput {
address: string;
method: MethodOption;
compiler: Option | null;
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> = {
export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> = {
'flattened-code': {
address: '',
method: {
value: 'flattened-code' as const,
label: METHOD_LABELS['flattened-code'],
......@@ -53,6 +54,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
libraries: [],
},
'standard-input': {
address: '',
method: {
value: 'standard-input' as const,
label: METHOD_LABELS['standard-input'],
......@@ -64,6 +66,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
constructor_args: '',
},
sourcify: {
address: '',
method: {
value: 'sourcify' as const,
label: METHOD_LABELS.sourcify,
......@@ -71,6 +74,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
sources: [],
},
'multi-part': {
address: '',
method: {
value: 'multi-part' as const,
label: METHOD_LABELS['multi-part'],
......@@ -83,6 +87,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
libraries: [],
},
'vyper-code': {
address: '',
method: {
value: 'vyper-code' as const,
label: METHOD_LABELS['vyper-code'],
......@@ -94,6 +99,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
constructor_args: '',
},
'vyper-multi-part': {
address: '',
method: {
value: 'vyper-multi-part' as const,
label: METHOD_LABELS['vyper-multi-part'],
......@@ -103,6 +109,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
sources: [],
},
'vyper-standard-input': {
address: '',
method: {
value: 'vyper-standard-input' as const,
label: METHOD_LABELS['vyper-standard-input'],
......@@ -112,8 +119,8 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
},
};
export function getDefaultValues(method: SmartContractVerificationMethod, config: SmartContractVerificationConfig) {
const defaultValues = DEFAULT_VALUES[method];
export function getDefaultValues(method: SmartContractVerificationMethod, config: SmartContractVerificationConfig, hash?: string) {
const defaultValues = { ...DEFAULT_VALUES[method], address: hash };
if ('evm_version' in defaultValues) {
if (method === 'flattened-code' || method === 'multi-part') {
......
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 { isValidVerificationMethod, sortVerificationMethods } from 'ui/contractVerification/utils';
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 ContractVerification = () => {
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 = 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 configQuery = useFormConfigQuery(true);
const content = (() => {
if (configQuery.isError || !hash || contractQuery.isError) {
if (configQuery.isError) {
return <DataFetchAlert/>;
}
if (configQuery.isPending || contractQuery.isPending || isVerifiedContract) {
if (configQuery.isPending) {
return <ContentLoader/>;
}
return (
<ContractVerificationForm
method={ method && configQuery.data.verification_options.includes(method) ? method : undefined }
config={ configQuery.data }
hash={ hash }
/>
<ContractVerificationForm config={ configQuery.data }/>
);
})();
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 }
/>
<PageTitle title="Verify & publish contract"/>
{ 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