diff --git a/src/api/hooks/useVerificationCheck.ts b/src/api/hooks/useVerificationCheck.ts new file mode 100644 index 00000000..dc423560 --- /dev/null +++ b/src/api/hooks/useVerificationCheck.ts @@ -0,0 +1,32 @@ +import {useCallback} from "react"; +import {useGlobalState} from "../../global-config/GlobalConfig"; +import {VerificationStatus} from "../../constants"; + +export interface AptosVerificationCheckDto { + network: string; + account: string; + moduleName: string; + status?: VerificationStatus; + errMsg?: string; +} +const useVerificationChecker = () => { + const [, , endpoint] = useGlobalState(); + + return useCallback( + async (params: { + network: string; + account: string; + moduleName: string; + }): Promise => { + const queryString = new URLSearchParams(params).toString(); + const res = await fetch(`${endpoint}?${queryString}`); + if (!res.ok) { + throw new Error("Verification service is not working."); + } + return res.json(); + }, + [endpoint], + ); +}; + +export default useVerificationChecker; diff --git a/src/api/hooks/useVerificationRequester.ts b/src/api/hooks/useVerificationRequester.ts new file mode 100644 index 00000000..9e641a0b --- /dev/null +++ b/src/api/hooks/useVerificationRequester.ts @@ -0,0 +1,40 @@ +import {useCallback} from "react"; +import {useGlobalState} from "../../global-config/GlobalConfig"; +import {VerificationStatus} from "../../constants"; + +export interface AptosVerificationResultDto { + network: string; + account: string; + moduleName: string; + errMsg?: string; + status?: VerificationStatus; + onChainByteCode?: string; + compiledByteCode?: string; +} + +const useVerificationRequester = () => { + const [, , endpoint] = useGlobalState(); + + return useCallback( + async (body: { + network: string; + account: string; + moduleName: string; + }): Promise => { + const res = await fetch(endpoint, { + method: "post", + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + }, + }); + if (!res.ok) { + throw new Error("Verification service is not working."); + } + return res.json(); + }, + [endpoint], + ); +}; + +export default useVerificationRequester; diff --git a/src/constants.tsx b/src/constants.tsx index 16a0cfeb..50749862 100644 --- a/src/constants.tsx +++ b/src/constants.tsx @@ -20,6 +20,15 @@ export function isValidNetworkName(value: string): value is NetworkName { return value in networks; } +export const defaultVerificationServiceUrl = + "https://verify.welldonestudio.io/aptos"; + +export enum VerificationStatus { + VERIFIED_SAME = "VERIFIED_SAME", + VERIFIED_DIFFERENT = "VERIFIED_DIFFERENT", + NOT_VERIFIED = "NOT_VERIFIED", +} + export enum Network { MAINNET = "mainnet", TESTNET = "testnet", diff --git a/src/global-config/GlobalConfig.tsx b/src/global-config/GlobalConfig.tsx index 7bd800ce..8ae0376a 100644 --- a/src/global-config/GlobalConfig.tsx +++ b/src/global-config/GlobalConfig.tsx @@ -1,10 +1,11 @@ import {AptosClient, IndexerClient} from "aptos"; -import React, {useMemo} from "react"; +import React, {Dispatch, SetStateAction, useMemo, useState} from "react"; import { FeatureName, NetworkName, defaultNetworkName, networks, + defaultVerificationServiceUrl, } from "../constants"; import { getSelectedFeatureFromLocalStorage, @@ -65,6 +66,12 @@ const initialGlobalState = deriveGlobalState({ export const GlobalStateContext = React.createContext(initialGlobalState); export const GlobalActionsContext = React.createContext({} as GlobalActions); +export const GlobalVerificationApiContext = React.createContext( + defaultVerificationServiceUrl, +); +export const GlobalVerificationApiDispatchContext = React.createContext< + Dispatch> +>(() => {}); export const GlobalStateProvider = ({ children, @@ -73,6 +80,8 @@ export const GlobalStateProvider = ({ }) => { const [selectedFeature, selectFeature] = useFeatureSelector(); const [selectedNetwork, selectNetwork] = useNetworkSelector(); + const [endpoint, setEndpoint] = useState(defaultVerificationServiceUrl); + const globalState: GlobalState = useMemo( () => deriveGlobalState({ @@ -93,7 +102,11 @@ export const GlobalStateProvider = ({ return ( - {children} + + + {children} + + ); @@ -103,4 +116,6 @@ export const useGlobalState = () => [ React.useContext(GlobalStateContext), React.useContext(GlobalActionsContext), + React.useContext(GlobalVerificationApiContext), + React.useContext(GlobalVerificationApiDispatchContext), ] as const; diff --git a/src/pages/Account/Components/CodeSnippet.tsx b/src/pages/Account/Components/CodeSnippet.tsx index d0bfe157..843cfb3c 100644 --- a/src/pages/Account/Components/CodeSnippet.tsx +++ b/src/pages/Account/Components/CodeSnippet.tsx @@ -18,6 +18,17 @@ import { } from "../../../themes/colors/aptosColorPalette"; import {useParams} from "react-router-dom"; import {useLogEventWithBasic} from "../hooks/useLogEventWithBasic"; +import {useGlobalState} from "../../../global-config/GlobalConfig"; +import {VerificationStatus} from "../../../constants"; +import useVerificationChecker, { + AptosVerificationCheckDto, +} from "../../../api/hooks/useVerificationCheck"; +import VerificationButton from "./VerificationButton"; +import VerificationServiceUrlInput from "./VerificationServiceUrlInput"; +import { + CODE_DESCRIPTION_NOT_VERIFIED, + genCodeDescription, +} from "./codeDescription"; function useStartingLineNumber(sourceCode?: string) { const functionToHighlight = useParams().selectedFnName; @@ -106,7 +117,7 @@ function ExpandCode({sourceCode}: {sourceCode: string | undefined}) { } export function Code({bytecode}: {bytecode: string}) { - const {selectedModuleName} = useParams(); + const {address, selectedModuleName} = useParams(); const logEvent = useLogEventWithBasic(); const TOOLTIP_TIME = 2000; // 2s @@ -116,6 +127,16 @@ export function Code({bytecode}: {bytecode: string}) { const theme = useTheme(); const [tooltipOpen, setTooltipOpen] = useState(false); + const checkVerification = useVerificationChecker(); + const [state, , verificationServiceEndpoint] = useGlobalState(); + const [codeDescription, setCodeDescription] = useState( + CODE_DESCRIPTION_NOT_VERIFIED, + ); + const [verificationStatus, setVerificationStatus] = + useState(VerificationStatus.NOT_VERIFIED); + const [verificationServerErr, setVerificationServerErr] = + useState(""); + async function copyCode() { if (!sourceCode) return; @@ -129,12 +150,46 @@ export function Code({bytecode}: {bytecode: string}) { const startingLineNumber = useStartingLineNumber(sourceCode); const codeBoxScrollRef = useRef(null); const LINE_HEIGHT_IN_PX = 24; - useEffect(() => { - if (codeBoxScrollRef.current) { - codeBoxScrollRef.current.scrollTop = - LINE_HEIGHT_IN_PX * startingLineNumber; - } - }); + useEffect( + () => { + if (codeBoxScrollRef.current) { + codeBoxScrollRef.current.scrollTop = + LINE_HEIGHT_IN_PX * startingLineNumber; + } + + if (state.network_name && address && selectedModuleName) { + checkVerification({ + network: state.network_name, + account: address, + moduleName: selectedModuleName, + }) + .then((dto: AptosVerificationCheckDto) => { + if (dto.errMsg) { + setVerificationStatus(VerificationStatus.NOT_VERIFIED); + setVerificationServerErr(`${dto.errMsg}`); + } + + if (!dto.status) { + setVerificationStatus(VerificationStatus.NOT_VERIFIED); + return; + } + setVerificationStatus(dto.status); + setCodeDescription(genCodeDescription(dto.status)); + setVerificationServerErr(""); + }) + .catch((reason: Error) => { + console.error(reason); + setVerificationStatus(VerificationStatus.NOT_VERIFIED); + setCodeDescription( + genCodeDescription(VerificationStatus.NOT_VERIFIED), + ); + setVerificationServerErr("Verification service is not working."); + }); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [address, selectedModuleName, verificationServiceEndpoint], + ); return ( @@ -154,7 +209,30 @@ export function Code({bytecode}: {bytecode: string}) { Code - + {verificationStatus === VerificationStatus.NOT_VERIFIED ? ( + + ) : null} + + + + + + {sourceCode && ( @@ -204,8 +282,7 @@ export function Code({bytecode}: {bytecode: string}) { marginBottom={"16px"} color={theme.palette.mode === "dark" ? grey[400] : grey[600]} > - The source code is plain text uploaded by the deployer, which can be - different from the actual bytecode. + {codeDescription} )} {!sourceCode ? ( diff --git a/src/pages/Account/Components/VerificationButton.tsx b/src/pages/Account/Components/VerificationButton.tsx new file mode 100644 index 00000000..4ce076ab --- /dev/null +++ b/src/pages/Account/Components/VerificationButton.tsx @@ -0,0 +1,112 @@ +import React, {useState} from "react"; +import {Button, CircularProgress} from "@mui/material"; +import useVerificationRequester from "../../../api/hooks/useVerificationRequester"; +import {VerificationStatus} from "../../../constants"; +import {genCodeDescription} from "./codeDescription"; + +interface VerificationButtonProps { + network: string; + account?: string; + moduleName?: string; + verificationStatus?: VerificationStatus; + setVerificationStatus: React.Dispatch< + React.SetStateAction + >; + setVerificationServerErr: React.Dispatch>; + setCodeDescription: React.Dispatch>; +} + +export default function VerificationButton({ + network, + account, + moduleName, + verificationStatus, + setVerificationStatus, + setVerificationServerErr, + setCodeDescription, +}: VerificationButtonProps) { + const [isInProgress, setIsInProgress] = useState(false); + const verificationRequester = useVerificationRequester(); + + const verifyClick = () => { + if (!(account && moduleName)) { + return; + } + setIsInProgress(true); + verificationRequester({ + network: network, + account: account, + moduleName: moduleName, + }) + .then((dto) => { + setIsInProgress(false); + if (dto.errMsg) { + setVerificationStatus(VerificationStatus.NOT_VERIFIED); + setVerificationServerErr(`${dto.errMsg}`); + } + + if (!dto.status) { + setVerificationStatus(VerificationStatus.NOT_VERIFIED); + return; + } + + setVerificationStatus(dto.status); + setCodeDescription(genCodeDescription(dto.status)); + setVerificationServerErr(""); + }) + .catch((reason: Error) => { + console.error(reason); + setVerificationStatus(VerificationStatus.NOT_VERIFIED); + setCodeDescription(genCodeDescription(VerificationStatus.NOT_VERIFIED)); + setVerificationServerErr("Verification service is not working."); + }) + .finally(() => { + setIsInProgress(false); + }); + }; + + if (verificationStatus === VerificationStatus.VERIFIED_DIFFERENT) { + return ( + + ); + } + + if (verificationStatus === VerificationStatus.VERIFIED_SAME) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/pages/Account/Components/VerificationServiceUrlInput.tsx b/src/pages/Account/Components/VerificationServiceUrlInput.tsx new file mode 100644 index 00000000..70772902 --- /dev/null +++ b/src/pages/Account/Components/VerificationServiceUrlInput.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import {Input, Stack, Typography} from "@mui/material"; +import {useGlobalState} from "../../../global-config/GlobalConfig"; +import {defaultVerificationServiceUrl} from "../../../constants"; + +interface VerificationServiceUrlInputProps { + verificationServerErr: string; +} + +export default function VerificationServiceUrlInput({ + verificationServerErr, +}: VerificationServiceUrlInputProps) { + const [, , endpoint, setEndpoint] = useGlobalState(); + return ( + + + +
+ Verification Service URL +
+ + { + setEndpoint(e.target.value); + }} + placeholder={defaultVerificationServiceUrl} + style={{height: "1.5em"}} + /> + {verificationServerErr ? ( +
+ {verificationServerErr} +
+ ) : null} +
+
+
+ ); +} diff --git a/src/pages/Account/Components/codeDescription.ts b/src/pages/Account/Components/codeDescription.ts new file mode 100644 index 00000000..c3e83f03 --- /dev/null +++ b/src/pages/Account/Components/codeDescription.ts @@ -0,0 +1,22 @@ +import {VerificationStatus} from "../../../constants"; + +export const CODE_DESCRIPTION_NOT_VERIFIED = + "The source code is plain text uploaded by the deployer, which can be different from the actual bytecode."; + +export const CODE_DESCRIPTION_VERIFIED_DIFFERENT = + "❗️Warning: This code is different from the actual bytecode."; + +export const CODE_DESCRIPTION_VERIFIED_SAME = + "The source code is same to the actual bytecode."; + +export function genCodeDescription(status: VerificationStatus) { + if (status === VerificationStatus.VERIFIED_DIFFERENT) { + return CODE_DESCRIPTION_VERIFIED_DIFFERENT; + } + + if (status === VerificationStatus.VERIFIED_SAME) { + return CODE_DESCRIPTION_VERIFIED_SAME; + } + + return CODE_DESCRIPTION_NOT_VERIFIED; +}