diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e79dd5aa..3cf81252d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- [#1187](https://github.com/alleslabs/celatone-frontend/pull/1187) Add onboarding section to EVM contract details page and add EVM contract verify page - [#1184](https://github.com/alleslabs/celatone-frontend/pull/1184) Add custom layer to Initia Widget - [#1182](https://github.com/alleslabs/celatone-frontend/pull/1182) Add Initia Widget diff --git a/src/lib/amplitude/types.ts b/src/lib/amplitude/types.ts index d88441143..fb44e746b 100644 --- a/src/lib/amplitude/types.ts +++ b/src/lib/amplitude/types.ts @@ -55,6 +55,7 @@ export enum AmpEvent { TO_CONTRACT_DETAILS = "To Contract Detail", TO_CODE_DETAILS = "To Code Detail", TO_EVM_CONTRACT_DETAILS = "To EVM Contract Detail", + TO_EVM_CONTRACT_VERIFY = "To EVM Contract Verify", TO_PROJECT_DETAILS = "To Public Project Detail", TO_EVM_TRANSACTION_DETAILS = "To EVM Transaction Detail", TO_TRANSACTION_DETAILS = "To Transaction Detail", diff --git a/src/lib/components/evm-verify-section/NotVerifiedDetails.tsx b/src/lib/components/evm-verify-section/NotVerifiedDetails.tsx new file mode 100644 index 000000000..8c3c20d5b --- /dev/null +++ b/src/lib/components/evm-verify-section/NotVerifiedDetails.tsx @@ -0,0 +1,53 @@ +import { Button, Flex, Text } from "@chakra-ui/react"; + +import { useInternalNavigate } from "lib/app-provider"; +import type { HexAddr20 } from "lib/types"; + +interface NotVerifiedDetailsProps { + contractAddress: HexAddr20; +} + +export const NotVerifiedDetails = ({ + contractAddress, +}: NotVerifiedDetailsProps) => { + const navigate = useInternalNavigate(); + + const handleNavigate = () => + navigate({ + pathname: "/evm-contracts/verify", + query: { contractAddress }, + }); + + return ( + + + This contract has not been verified. If you are the owner, you can{" "} + + verify it + {" "} + to allow other users to view the source code + + + + ); +}; diff --git a/src/lib/components/evm-verify-section/index.ts b/src/lib/components/evm-verify-section/index.ts new file mode 100644 index 000000000..c97512379 --- /dev/null +++ b/src/lib/components/evm-verify-section/index.ts @@ -0,0 +1 @@ +export * from "./NotVerifiedDetails"; diff --git a/src/lib/components/forms/SelectInput.tsx b/src/lib/components/forms/SelectInput.tsx index 960ba26f5..0273ab6cd 100644 --- a/src/lib/components/forms/SelectInput.tsx +++ b/src/lib/components/forms/SelectInput.tsx @@ -1,3 +1,4 @@ +import { Flex, Stack, Text } from "@chakra-ui/react"; import type { SystemStyleObject } from "@chakra-ui/styled-system"; import type { Props } from "chakra-react-select"; import { Select } from "chakra-react-select"; @@ -13,7 +14,10 @@ export interface SelectInputOption { interface SelectInputProps< OptionValue extends SelectInputOptionValue, IsMulti extends boolean = false, -> extends Props, IsMulti> {} +> extends Props, IsMulti> { + label?: string; + isRequired?: boolean; +} const handleFilterOption = ( candidate: { label: string; value: string }, @@ -48,60 +52,90 @@ export const SelectInput = < onFocus, autoFocus, classNamePrefix, + label, + isRequired, + isDisabled, }: SelectInputProps) => ( - , IsMulti> - menuPosition="fixed" - menuPortalTarget={menuPortalTarget} - placeholder={placeholder} - options={options} - value={value} - onChange={onChange} - size={size} - filterOption={handleFilterOption} - formatOptionLabel={formatOptionLabel} - components={components} - isSearchable={isSearchable} - chakraStyles={{ - container: (provided: SystemStyleObject) => ({ - ...provided, - width: "100%", - }), - valueContainer: (provided: SystemStyleObject) => ({ - ...provided, - pl: 3, - pr: 0, - }), - dropdownIndicator: (provided: SystemStyleObject) => ({ - ...provided, - px: 2, - color: "gray.600", - }), - placeholder: (provided: SystemStyleObject) => ({ - ...provided, - color: "gray.500", - fontSize: "14px", - whiteSpace: "nowrap", - }), - option: (provided) => ({ - ...provided, - color: "text.main", - fontSize: "16px", - _hover: { - bg: "gray.700", - }, - _selected: { - bg: "gray.800", - }, - }), - ...chakraStyles, - }} - inputId={inputId} - name={name} - isMulti={isMulti} - closeMenuOnSelect={closeMenuOnSelect} - onBlur={onBlur} - onFocus={onFocus} - autoFocus={autoFocus} - classNamePrefix={classNamePrefix} - /> + + {label && ( + + {label} + + )} + , IsMulti> + menuPosition="fixed" + menuPortalTarget={menuPortalTarget} + placeholder={placeholder} + options={options} + value={value} + isDisabled={isDisabled} + onChange={onChange} + size={size} + filterOption={handleFilterOption} + formatOptionLabel={formatOptionLabel} + components={components} + isSearchable={isSearchable} + chakraStyles={{ + container: (provided: SystemStyleObject) => ({ + ...provided, + width: "100%", + }), + valueContainer: (provided: SystemStyleObject) => ({ + ...provided, + pl: 3, + pr: 0, + }), + dropdownIndicator: (provided: SystemStyleObject) => ({ + ...provided, + px: 2, + color: "gray.600", + }), + placeholder: (provided: SystemStyleObject) => ({ + ...provided, + color: "gray.500", + fontSize: "14px", + whiteSpace: "nowrap", + }), + option: (provided) => ({ + ...provided, + color: "text.main", + fontSize: "16px", + _hover: { + bg: "gray.700", + }, + _selected: { + bg: "gray.800", + }, + }), + ...chakraStyles, + }} + inputId={inputId} + name={name} + isMulti={isMulti} + closeMenuOnSelect={closeMenuOnSelect} + onBlur={onBlur} + onFocus={onFocus} + autoFocus={autoFocus} + classNamePrefix={classNamePrefix} + /> + ); diff --git a/src/lib/hooks/index.ts b/src/lib/hooks/index.ts index 40d47cfb5..40356db72 100644 --- a/src/lib/hooks/index.ts +++ b/src/lib/hooks/index.ts @@ -13,3 +13,4 @@ export * from "./useGetMaxLengthError"; export * from "./useLocalStorage"; export * from "./useUploadCode"; export * from "./useTxBroadcast"; +export * from "./useStepper"; diff --git a/src/lib/hooks/useStepper.ts b/src/lib/hooks/useStepper.ts new file mode 100644 index 000000000..d46335c5e --- /dev/null +++ b/src/lib/hooks/useStepper.ts @@ -0,0 +1,48 @@ +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect } from "react"; + +export const useStepper = ( + total: number, + handleSubmit: () => void, + handleBack?: () => void +) => { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const currentStep = searchParams.get("step") ?? "1"; + + useEffect(() => { + if (Number(currentStep) === 1) return; + const params = new URLSearchParams(searchParams); + params.set("step", "1"); + router.replace(`${pathname}?${params.toString()}`); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleNext = useCallback(() => { + const params = new URLSearchParams(searchParams); + params.set("step", String(Number(currentStep) + 1)); + + return currentStep && Number(currentStep) < total + ? router.replace(`${pathname}?${params.toString()}`) + : handleSubmit(); + }, [currentStep, handleSubmit, total, pathname, router, searchParams]); + + const handlePrevious = useCallback(() => { + const params = new URLSearchParams(searchParams); + params.set("step", String(Number(currentStep) - 1)); + + return currentStep && Number(currentStep) > 1 + ? router.replace(`${pathname}?${params.toString()}`) + : (handleBack?.() ?? router.back()); + }, [currentStep, pathname, router, searchParams, handleBack]); + + return { + currentStepIndex: Number(currentStep) - 1, + handleNext, + handlePrevious, + hasNext: Number(currentStep) < total, + hasPrevious: Number(currentStep) > 1, + }; +}; diff --git a/src/lib/pages/custom-network/manual/components/AddNetworkStepper.tsx b/src/lib/pages/custom-network/manual/components/AddNetworkStepper.tsx index 592981a9a..4024b1767 100644 --- a/src/lib/pages/custom-network/manual/components/AddNetworkStepper.tsx +++ b/src/lib/pages/custom-network/manual/components/AddNetworkStepper.tsx @@ -1,6 +1,6 @@ import { Flex, Text } from "@chakra-ui/react"; -import { getStepStyles } from "../hooks/utils"; +import { getStepStyles } from "../helpers"; const steps = [ { label: "Network Details" }, diff --git a/src/lib/pages/custom-network/manual/hooks/utils.tsx b/src/lib/pages/custom-network/manual/helpers.tsx similarity index 100% rename from src/lib/pages/custom-network/manual/hooks/utils.tsx rename to src/lib/pages/custom-network/manual/helpers.tsx diff --git a/src/lib/pages/custom-network/manual/hooks/useNetworkStepper.ts b/src/lib/pages/custom-network/manual/hooks/useNetworkStepper.ts deleted file mode 100644 index 995f355e9..000000000 --- a/src/lib/pages/custom-network/manual/hooks/useNetworkStepper.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useRouter } from "next/router"; -import { useEffect } from "react"; - -import { useCelatoneApp, useInternalNavigate } from "lib/app-provider"; - -export const useNetworkStepper = (limit: number, handleSubmit: () => void) => { - const router = useRouter(); - const navigate = useInternalNavigate(); - const currentStep = router.query.step; - const { currentChainId } = useCelatoneApp(); - - useEffect(() => { - if (Number(currentStep) === 1) return; - - router.push( - { - query: { - network: currentChainId, - step: 1, - }, - }, - undefined, - { shallow: true } - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const handleNext = () => - currentStep && Number(currentStep) < limit - ? router.push( - { - query: { - network: currentChainId, - step: Number(currentStep) + 1, - }, - }, - undefined, - { shallow: true } - ) - : handleSubmit(); - - const handlePrevious = () => { - if (Number(currentStep) === 1) { - navigate({ pathname: "/custom-network/add" }); - return; - } - - router.back(); - }; - - return { - currentStepIndex: Number(currentStep) - 1, - handleNext, - handlePrevious, - hasNext: Number(currentStep) < limit, - hasPrevious: Number(currentStep) > 1, - }; -}; diff --git a/src/lib/pages/custom-network/manual/index.tsx b/src/lib/pages/custom-network/manual/index.tsx index 46a0796a3..8f14ab7e0 100644 --- a/src/lib/pages/custom-network/manual/index.tsx +++ b/src/lib/pages/custom-network/manual/index.tsx @@ -13,7 +13,11 @@ import { zWalletRegistryForm, } from "../types"; import type { AddNetworkManualForm } from "../types"; -import { useAllowCustomNetworks, useChainConfigs } from "lib/app-provider"; +import { + useAllowCustomNetworks, + useChainConfigs, + useInternalNavigate, +} from "lib/app-provider"; import ActionPageContainer from "lib/components/ActionPageContainer"; import { CustomIcon } from "lib/components/icon"; import { FooterCta } from "lib/components/layouts"; @@ -21,10 +25,11 @@ import { CelatoneSeo } from "lib/components/Seo"; import { useLocalChainConfigStore } from "lib/providers/store"; import { AddNetworkForm, AddNetworkStepper } from "./components"; -import { useNetworkStepper } from "./hooks/useNetworkStepper"; +import { useStepper } from "lib/hooks"; export const AddNetworkManual = () => { useAllowCustomNetworks({ shouldRedirect: true }); + const navigate = useInternalNavigate(); const { isOpen, onClose, onOpen } = useDisclosure(); const { addLocalChainConfig } = useLocalChainConfigStore(); const { isChainIdExist } = useChainConfigs(); @@ -95,7 +100,9 @@ export const AddNetworkManual = () => { }; const { currentStepIndex, handleNext, handlePrevious, hasNext, hasPrevious } = - useNetworkStepper(3, handleSubmit(handleSubmitForm)); + useStepper(3, handleSubmit(handleSubmitForm), () => + navigate({ pathname: "/custom-network/add" }) + ); const isFormDisabled = () => { if (currentStepIndex === 0) diff --git a/src/lib/pages/evm-contract-details/components/EvmContractDetailsOverview.tsx b/src/lib/pages/evm-contract-details/components/EvmContractDetailsOverview.tsx index 2efd6a792..6e484da65 100644 --- a/src/lib/pages/evm-contract-details/components/EvmContractDetailsOverview.tsx +++ b/src/lib/pages/evm-contract-details/components/EvmContractDetailsOverview.tsx @@ -1,18 +1,26 @@ -import { Flex, Grid, Heading, Spinner, Stack, Text } from "@chakra-ui/react"; +import { Flex, Grid, Spinner, Stack, Text } from "@chakra-ui/react"; import type { TxsTabIndex } from "../types"; import { useCelatoneApp, useMobile } from "lib/app-provider"; import { AssetsSection } from "lib/components/asset"; +import { NotVerifiedDetails } from "lib/components/evm-verify-section"; import { ExplorerLink } from "lib/components/ExplorerLink"; import { LabelText } from "lib/components/LabelText"; import { useFormatAddresses } from "lib/hooks/useFormatAddresses"; -import type { BechAddr, BechAddr20, Nullish, Option } from "lib/types"; +import type { + BechAddr, + BechAddr20, + HexAddr20, + Nullish, + Option, +} from "lib/types"; import { dateFromNow, formatEvmTxHash, formatUTC } from "lib/utils"; import { EvmContractDetailsTxs } from "./EvmContractDetailsTxs"; interface EvmContractDetailsOverviewProps { - contractAddress: BechAddr20; + contractAddressBech: BechAddr20; + contractAddressHex: HexAddr20; hash: Option; evmHash: Nullish; sender: Option; @@ -25,7 +33,8 @@ interface EvmContractDetailsOverviewProps { } export const EvmContractDetailsOverview = ({ - contractAddress, + contractAddressBech, + contractAddressHex, hash, evmHash, sender, @@ -42,18 +51,16 @@ export const EvmContractDetailsOverview = ({ return ( + {/* // TODO: Support all status */} + - - Contract Info - - + ( + + + + + What is contract license? + + + + + + A contract license specifies how a smart contract's code can be used, + modified, and distributed, ensuring legal clarity and protecting + creators' rights. + + + + + +); diff --git a/src/lib/pages/evm-contract-verify/components/EvmContractVerifyFooter.tsx b/src/lib/pages/evm-contract-verify/components/EvmContractVerifyFooter.tsx new file mode 100644 index 000000000..c9f2d0146 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/EvmContractVerifyFooter.tsx @@ -0,0 +1,47 @@ +import { CustomIcon } from "lib/components/icon"; +import { FooterCta } from "lib/components/layouts"; + +interface EvmContractFooterProps { + handleNext: () => void; + handlePrevious: () => void; + hasNext: boolean; + hasPrevious: boolean; + isDisabled: boolean; +} + +export const EvmContractFooter = ({ + handleNext, + handlePrevious, + hasNext, + hasPrevious, + isDisabled, +}: EvmContractFooterProps) => ( + + ) : undefined, + }} + cancelLabel={hasPrevious ? "Previous" : "Cancel"} + actionButton={{ + onClick: handleNext, + isDisabled, + rightIcon: hasNext ? ( + + ) : undefined, + }} + actionLabel="Verify & Publish Contract" + sx={{ + backgroundColor: "background.main", + borderColor: "gray.700", + display: "grid", + gridTemplateColumns: "6fr 4fr", + px: "96px", + "> div": { + width: "100%", + }, + }} + /> +); diff --git a/src/lib/pages/evm-contract-verify/components/EvmContractVerifyOptions.tsx b/src/lib/pages/evm-contract-verify/components/EvmContractVerifyOptions.tsx new file mode 100644 index 000000000..890675c4a --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/EvmContractVerifyOptions.tsx @@ -0,0 +1,108 @@ +import { Grid, Heading, Radio, RadioGroup, Stack } from "@chakra-ui/react"; +import { Control, useController, useWatch } from "react-hook-form"; +import { + EvmContractVerifyForm, + EvmProgrammingLanguage, + VerificationOptions, +} from "../types"; + +interface EvmContractVerifyVyperProps { + control: Control; +} + +export const EvmContractVerifyOptions = ({ + control, +}: EvmContractVerifyVyperProps) => { + const { field: verifyFormOptinField } = useController({ + control, + name: "verifyForm.option", + }); + + const [verifyFormOption, language] = useWatch({ + control, + name: ["verifyForm.option", "language"], + }); + + return ( + + + Select Verification Option + + verifyFormOptinField.onChange(nextVal)} + value={verifyFormOption} + > + + {language === EvmProgrammingLanguage.Solidity && ( + + Upload Files + + )} + {language === EvmProgrammingLanguage.Vyper && ( + + Upload File + + )} + + Contract Code + + + JSON Input + + {language === EvmProgrammingLanguage.Solidity && ( + <> + + Hardhat + + + Foundry + + + )} + + + + ); +}; diff --git a/src/lib/pages/evm-contract-verify/components/EvmContractVerifyTop.tsx b/src/lib/pages/evm-contract-verify/components/EvmContractVerifyTop.tsx new file mode 100644 index 000000000..385e48912 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/EvmContractVerifyTop.tsx @@ -0,0 +1,14 @@ +import { Heading, Stack, Text } from "@chakra-ui/react"; + +export const EvmContractVerifyTop = () => ( + + + Verify & Publish Contract + + + Verifying your contract offers enhanced credibility with a verified badge. + Once verified, users will able to access its source code in contract + details page. + + +); diff --git a/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidity.tsx b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidity.tsx new file mode 100644 index 000000000..7479c381c --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidity.tsx @@ -0,0 +1,44 @@ +import { Divider, Stack } from "@chakra-ui/react"; +import { EvmContractVerifyOptions } from "../EvmContractVerifyOptions"; +import { Control, useWatch } from "react-hook-form"; +import { EvmContractVerifyForm, VerificationOptions } from "../../types"; +import { EvmContractVerifySolidityUploadFiles } from "./EvmContractVerifySolidityUploadFiles"; +import { EvmContractVerifySolidityContractCode } from "./EvmContractVerifySolidityContractCode"; +import { EvmContractVerifySolidityJsonInput } from "./EvmContractVerifySolidityJsonInput"; +import { EvmContractVerifySolidityHardhat } from "./EvmContractVerifySolidityHardhat"; +import { EvmContractVerifySolidityFoundry } from "./EvmContractVerifySolidityFoundry"; + +interface EvmContractVerifySolidityProps { + control: Control; +} + +const EvmContractVerifySolidityOptions = ({ + control, +}: EvmContractVerifySolidityProps) => { + const verifyFormOption = useWatch({ control, name: "verifyForm.option" }); + + switch (verifyFormOption) { + case VerificationOptions.UploadFiles: + return ; + case VerificationOptions.ContractCode: + return ; + case VerificationOptions.JsonInput: + return ; + case VerificationOptions.Hardhat: + return ; + case VerificationOptions.Foundry: + return ; + default: + return null; + } +}; + +export const EvmContractVerifySolidity = ({ + control, +}: EvmContractVerifySolidityProps) => ( + + + + + +); diff --git a/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityContractCode.tsx b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityContractCode.tsx new file mode 100644 index 000000000..2efeb3c33 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityContractCode.tsx @@ -0,0 +1,3 @@ +export const EvmContractVerifySolidityContractCode = () => { + return
TODO: EvmContractVerifySolidityContractCode
; +}; diff --git a/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityFoundry.tsx b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityFoundry.tsx new file mode 100644 index 000000000..fd17c8ef1 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityFoundry.tsx @@ -0,0 +1,3 @@ +export const EvmContractVerifySolidityFoundry = () => { + return
TODO: EvmContractVerifySolidityFoundry
; +}; diff --git a/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityHardhat.tsx b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityHardhat.tsx new file mode 100644 index 000000000..b7cae9211 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityHardhat.tsx @@ -0,0 +1,3 @@ +export const EvmContractVerifySolidityHardhat = () => { + return
TODO: EvmContractVerifySolidityHardhat
; +}; diff --git a/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityJsonInput.tsx b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityJsonInput.tsx new file mode 100644 index 000000000..0e11118f3 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityJsonInput.tsx @@ -0,0 +1,3 @@ +export const EvmContractVerifySolidityJsonInput = () => { + return
TODO: EvmContractVerifySolidityJsonInput
; +}; diff --git a/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityUploadFiles.tsx b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityUploadFiles.tsx new file mode 100644 index 000000000..1beaaad92 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/solidity/EvmContractVerifySolidityUploadFiles.tsx @@ -0,0 +1,3 @@ +export const EvmContractVerifySolidityUploadFiles = () => { + return
TODO: EvmContractVerifySolidityUploadFiles
; +}; diff --git a/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyper.tsx b/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyper.tsx new file mode 100644 index 000000000..c7e7f12db --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyper.tsx @@ -0,0 +1,38 @@ +import { Divider, Stack } from "@chakra-ui/react"; +import { EvmContractVerifyForm, VerificationOptions } from "../../types"; +import { Control, useWatch } from "react-hook-form"; +import { EvmContractVerifyOptions } from "../EvmContractVerifyOptions"; +import { EvmContractVerifyVyperUploadFile } from "./EvmContractVerifyVyperUploadFile"; +import { EvmContractVerifyVyperContractCode } from "./EvmContractVerifyVyperContractCode"; +import { EvmContractVerifyVyperJsonInput } from "./EvmContractVerifyVyperJsonInput"; + +interface EvmContractVerifyVyperProps { + control: Control; +} + +const EvmContractVerifyVyperOptions = ({ + control, +}: EvmContractVerifyVyperProps) => { + const verifyFormOption = useWatch({ control, name: "verifyForm.option" }); + + switch (verifyFormOption) { + case VerificationOptions.UploadFile: + return ; + case VerificationOptions.ContractCode: + return ; + case VerificationOptions.JsonInput: + return ; + default: + return null; + } +}; + +export const EvmContractVerifyVyper = ({ + control, +}: EvmContractVerifyVyperProps) => ( + + + + + +); diff --git a/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyperContractCode.tsx b/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyperContractCode.tsx new file mode 100644 index 000000000..79859a9b7 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyperContractCode.tsx @@ -0,0 +1,3 @@ +export const EvmContractVerifyVyperContractCode = () => { + return
TODO: EvmContractVerifyVyperContractCode
; +}; diff --git a/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyperJsonInput.tsx b/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyperJsonInput.tsx new file mode 100644 index 000000000..a38b55254 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyperJsonInput.tsx @@ -0,0 +1,3 @@ +export const EvmContractVerifyVyperJsonInput = () => { + return
TODO: EvmContractVerifyVyperJsonInput
; +}; diff --git a/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyperUploadFile.tsx b/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyperUploadFile.tsx new file mode 100644 index 000000000..eb74f96e6 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/components/vyper/EvmContractVerifyVyperUploadFile.tsx @@ -0,0 +1,3 @@ +export const EvmContractVerifyVyperUploadFile = () => { + return
TODO: EvmContractVerifyVyperUploadFile
; +}; diff --git a/src/lib/pages/evm-contract-verify/index.tsx b/src/lib/pages/evm-contract-verify/index.tsx new file mode 100644 index 000000000..a1a4bd4b5 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/index.tsx @@ -0,0 +1,251 @@ +import { useEvmConfig, useExampleAddresses, useMobile } from "lib/app-provider"; +import { useRouter } from "next/router"; +import { useEffect, useMemo } from "react"; +import { track } from "@amplitude/analytics-browser"; +import { AmpEvent } from "lib/amplitude"; +import PageContainer from "lib/components/PageContainer"; +import { CelatoneSeo } from "lib/components/Seo"; +import { Grid, GridItem, Heading, Stack, Text } from "@chakra-ui/react"; +import { EvmContractVerifyTop } from "./components/EvmContractVerifyTop"; +import { ContractLicenseInfoAccordion } from "./components/ContractLicenseInfoAccordion"; +import { useStepper } from "lib/hooks"; +import { EvmContractFooter } from "./components/EvmContractVerifyFooter"; +import { ControllerInput, SelectInput } from "lib/components/forms"; +import { useForm } from "react-hook-form"; +import { + EvmContractVerifyForm, + EvmProgrammingLanguage, + VerificationOptions, + zEvmContractVerifyForm, +} from "./types"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { truncate } from "lib/utils"; +import { EvmContractVerifySolidity } from "./components/solidity/EvmContractVerifySolidity"; +import { EvmContractVerifyVyper } from "./components/vyper/EvmContractVerifyVyper"; +import { NoMobile } from "lib/components/modal"; + +export const EvmContractVerify = () => { + useEvmConfig({ shouldRedirect: true }); + const isMobile = useMobile(); + const router = useRouter(); + // TODO: add evm contract address + const { contract: exampleContractAddress } = useExampleAddresses(); + + useEffect(() => { + if (router.isReady) track(AmpEvent.TO_EVM_CONTRACT_VERIFY); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [router.isReady]); + + const { control, watch, handleSubmit, setValue } = + useForm({ + resolver: zodResolver(zEvmContractVerifyForm), + mode: "all", + reValidateMode: "onChange", + defaultValues: { + contractAddress: "", + compilerVersion: "", + }, + }); + const { licenseType, language, compilerVersion } = watch(); + + const { handleNext, handlePrevious, hasNext, hasPrevious } = useStepper( + 1, + () => alert("Submit!") + ); + + const licenseTypeOptions = useMemo( + () => [ + { + label: "1. No License (None)", + value: "no-license", + }, + { + label: "2. The Unlicense (Unlicense)", + value: "the-unlicense", + }, + { + label: "3. MIT License (MIT)", + value: "mit", + }, + ], + [] + ); + + const programmingLangaugeOptions = useMemo( + () => [ + { + label: "Solidity", + value: EvmProgrammingLanguage.Solidity, + }, + { + label: "Vyper", + value: EvmProgrammingLanguage.Vyper, + }, + ], + [] + ); + + // TODO: fetch from API + const compilerVersionOptions = useMemo( + () => [ + { + label: "0.8.0", + value: "0.8.0", + }, + { + label: "0.7.0", + value: "0.7.0", + }, + { + label: "0.6.0", + value: "0.6.0", + }, + ], + [] + ); + + const isFormDisabled = () => { + // TODO: Update the validation + return false; + }; + + return ( + <> + + {isMobile ? ( + + ) : ( + <> + + + + + + + + + + Contract Address & License + + + + + + + { + if (!selectedOption) return; + setValue("licenseType", selectedOption.value); + }} + value={licenseTypeOptions.find( + (option) => option.value === licenseType + )} + /> + + + + + + + + + + + Verification Method + + + Please ensure the setting is the matching with the created + contract + + + + { + if (!selectedOption) return; + setValue( + "verifyForm.option", + selectedOption.value === + EvmProgrammingLanguage.Solidity + ? VerificationOptions.UploadFiles + : VerificationOptions.UploadFile + ); + setValue("language", selectedOption.value); + }} + value={programmingLangaugeOptions.find( + (option) => option.value === language + )} + menuPortalTarget={document.body} + /> + { + if (!selectedOption) return; + setValue("compilerVersion", selectedOption.value); + }} + value={compilerVersionOptions.find( + (option) => option.value === compilerVersion + )} + menuPortalTarget={document.body} + isDisabled={!language} + /> + + + + + {language === EvmProgrammingLanguage.Solidity && ( + + )} + {language === EvmProgrammingLanguage.Vyper && ( + + )} + + + + + + )} + + ); +}; diff --git a/src/lib/pages/evm-contract-verify/types.ts b/src/lib/pages/evm-contract-verify/types.ts new file mode 100644 index 000000000..fa8a23f11 --- /dev/null +++ b/src/lib/pages/evm-contract-verify/types.ts @@ -0,0 +1,81 @@ +import { zHexAddr20 } from "lib/types"; +import { z } from "zod"; + +export enum EvmProgrammingLanguage { + Solidity = "solidity", + Vyper = "vyper", +} + +export enum VerificationOptions { + UploadFile = "upload-file", + UploadFiles = "upload-files", + ContractCode = "contract-code", + JsonInput = "json-input", + Hardhat = "hardhat", + Foundry = "foundry", +} + +// MARK - Query Params +export const zEvmContractVerifyQueryParams = z.object({ + contractAddress: zHexAddr20, +}); + +// MARK - Solidity +export const zEvmContractVerifySolidityOptionUploadFilesForm = z.object({ + option: z.literal(VerificationOptions.UploadFiles), +}); + +export const zEvmContractVerifySolidityOptionContractCodeForm = z.object({ + option: z.literal(VerificationOptions.ContractCode), +}); + +export const zEvmContractVerifySolidityOptionJsonInputForm = z.object({ + option: z.literal(VerificationOptions.JsonInput), +}); + +export const zEvmContractVerifySolidityOptionHardhatForm = z.object({ + option: z.literal(VerificationOptions.Hardhat), +}); + +export const zEvmContractVerifySolidityOptionFoundryForm = z.object({ + option: z.literal(VerificationOptions.Foundry), +}); + +// MARK - Vyper +export const zEvmContractVerifyVyperOptionUploadFileForm = z.object({ + option: z.literal(VerificationOptions.UploadFile), +}); + +export const zEvmContractVerifyVyperOptionContractCodeForm = z.object({ + option: z.literal(VerificationOptions.ContractCode), +}); + +export const zEvmContractVerifyVyperOptionJsonInputForm = z.object({ + option: z.literal(VerificationOptions.JsonInput), +}); + +// MARK - Union of all options +export const zEvmContractVerifyOptionForm = z.union([ + zEvmContractVerifyVyperOptionUploadFileForm, + zEvmContractVerifyVyperOptionContractCodeForm, + zEvmContractVerifyVyperOptionJsonInputForm, + zEvmContractVerifySolidityOptionUploadFilesForm, + zEvmContractVerifySolidityOptionContractCodeForm, + zEvmContractVerifySolidityOptionJsonInputForm, + zEvmContractVerifySolidityOptionHardhatForm, + zEvmContractVerifySolidityOptionFoundryForm, +]); + +export const zEvmContractAddressAndLicenseForm = z.object({ + contractAddress: zHexAddr20, + licenseType: z.string().refine((val) => val !== ""), + language: z.nativeEnum(EvmProgrammingLanguage), + compilerVersion: z.string().refine((val) => val !== ""), +}); + +export const zEvmContractVerifyForm = zEvmContractAddressAndLicenseForm.merge( + z.object({ + verifyForm: zEvmContractVerifyOptionForm, + }) +); +export type EvmContractVerifyForm = z.infer; diff --git a/src/pages/[network]/evm-contracts/verify.tsx b/src/pages/[network]/evm-contracts/verify.tsx new file mode 100644 index 000000000..58ac04251 --- /dev/null +++ b/src/pages/[network]/evm-contracts/verify.tsx @@ -0,0 +1,3 @@ +import { EvmContractVerify } from "lib/pages/evm-contract-verify"; + +export default EvmContractVerify; diff --git a/src/pages/evm-contracts/verify.tsx b/src/pages/evm-contracts/verify.tsx new file mode 100644 index 000000000..58ac04251 --- /dev/null +++ b/src/pages/evm-contracts/verify.tsx @@ -0,0 +1,3 @@ +import { EvmContractVerify } from "lib/pages/evm-contract-verify"; + +export default EvmContractVerify;