"Verify contract" page (#1394)
* route and dummy page * form layout * validate address on form submit * fix form reset * update screenshot * fix ts errors
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 776 B |
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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';
|
||||
@@ -1,21 +1,23 @@
|
||||
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
|
||||
|
||||
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,57 @@
|
||||
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>;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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') {
|
||||
|
||||
@@ -1,109 +1,31 @@
|
||||
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 }
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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;
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |