import { Grid, Text, chakra } 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 { SmartContract, SmartContractVerificationMethodApi } from 'types/api/contract'; import type { SmartContractVerificationConfig } from 'types/client/contract'; import { route } from 'nextjs-routes'; import useApiFetch from 'lib/api/useApiFetch'; import capitalizeFirstLetter from 'lib/capitalizeFirstLetter'; import delay from 'lib/delay'; import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; import useRewardsActivity from 'lib/hooks/useRewardsActivity'; import * as mixpanel from 'lib/mixpanel/index'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; import { Button } from 'toolkit/chakra/button'; import { toaster } from 'toolkit/chakra/toaster'; import { useUpdateEffect } from 'toolkit/hooks/useUpdateEffect'; import ContractVerificationFieldAddress from './fields/ContractVerificationFieldAddress'; import ContractVerificationFieldLicenseType from './fields/ContractVerificationFieldLicenseType'; import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod'; import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode'; import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile'; import ContractVerificationSolidityFoundry from './methods/ContractVerificationSolidityFoundry'; import ContractVerificationSolidityHardhat from './methods/ContractVerificationSolidityHardhat'; import ContractVerificationSourcify from './methods/ContractVerificationSourcify'; import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput'; import ContractVerificationStylusGitHubRepo from './methods/ContractVerificationStylusGitHubRepo'; import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract'; import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile'; import ContractVerificationVyperStandardInput from './methods/ContractVerificationVyperStandardInput'; import { prepareRequestBody, formatSocketErrors, getDefaultValues, METHOD_LABELS } from './utils'; interface Props { method?: SmartContractVerificationMethodApi; config: SmartContractVerificationConfig; hash?: string; } const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => { const formApi = useForm({ mode: 'onBlur', defaultValues: getDefaultValues(methodFromQuery, config, hash, []), }); const { handleSubmit, watch, formState, setError, reset, getFieldState, getValues, clearErrors } = formApi; const submitPromiseResolver = React.useRef<((value: unknown) => void)>(undefined); const methodNameRef = React.useRef(undefined); const apiFetch = useApiFetch(); const { trackContract } = useRewardsActivity(); const onFormSubmit: SubmitHandler = React.useCallback(async(data) => { const body = prepareRequestBody(data); if (!hash) { try { const response = await apiFetch<'general:contract', SmartContract>('general: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 trackContract(data.address); await apiFetch('general:contract_verification_via', { pathParams: { method: data.method[0], hash: data.address.toLowerCase() }, fetchParams: { method: 'POST', body, }, }); } catch (error) { return; } return new Promise((resolve) => { submitPromiseResolver.current = resolve; }); }, [ apiFetch, hash, setError, trackContract ]); const handleFormChange = React.useCallback(() => { clearErrors('root'); }, [ clearErrors ]); const address = watch('address'); const addressState = getFieldState('address'); const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback(async(payload) => { if (payload.status === 'error') { const errors = formatSocketErrors(payload.errors); const existingErrors = errors.filter(Boolean).filter(([ field ]) => getValues(field)); if (existingErrors.length) { existingErrors.forEach(([ field, error ]) => setError(field, error)); } else { const globalErrors = Object.entries(payload.errors).map(([ , value ]) => value.join(', ')); const rootError = capitalizeFirstLetter(globalErrors.join('\n\n')); setError('root', { message: rootError }); } await delay(100); // have to wait a little bit, otherwise isSubmitting status will not be updated submitPromiseResolver.current?.(null); return; } toaster.success({ title: 'Success', description: 'Contract is successfully verified.', }); mixpanel.logEvent( mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Finished', Method: methodNameRef.current || '' }, { send_immediately: true }, ); window.location.assign(route({ pathname: '/address/[hash]', query: { hash: address, tab: 'contract' } })); }, [ setError, address, getValues ]); const handleSocketError = React.useCallback(() => { if (!formState.isSubmitting) { return; } submitPromiseResolver.current?.(null); toaster.error({ title: 'Error', description: 'There was an error with socket connection. Try again later.', }); // callback should not change when form is submitted // otherwise it will resubscribe to channel, but we don't want that since in that case we might miss verification result message // eslint-disable-next-line react-hooks/exhaustive-deps }, [ ]); const channel = useSocketChannel({ topic: `addresses:${ address?.toLowerCase() }`, onSocketClose: handleSocketError, onSocketError: handleSocketError, isDisabled: !address || Boolean(address && addressState.error), }); useSocketMessage({ channel, event: 'verification_result', handler: handleNewSocketMessage, }); const methods = React.useMemo(() => { return { 'flattened-code': , 'standard-input': , sourcify: , 'multi-part': , 'vyper-code': , 'vyper-multi-part': , 'vyper-standard-input': , 'solidity-hardhat': , 'solidity-foundry': , 'stylus-github-repository': , }; }, [ config ]); const method = watch('method'); const methodValue = method?.[0]; const licenseType = watch('license_type'); const content = methods[methodValue] || null; useUpdateEffect(() => { if (methodValue) { reset(getDefaultValues(methodValue, config, hash || address, licenseType)); const methodName = METHOD_LABELS[methodValue]; mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Method selected', Method: methodName }); methodNameRef.current = methodName; } // !!! should run only when method is changed }, [ methodValue ]); return ( { !hash && } { content } { formState.errors.root?.message && { formState.errors.root.message } } { Boolean(method) && methodValue !== 'solidity-hardhat' && methodValue !== 'solidity-foundry' && methodValue !== 'sourcify' && ( ) } ); }; export default React.memo(ContractVerificationForm);