From 65e240155fb3b86ac71be84d9bd48e40cad841f2 Mon Sep 17 00:00:00 2001 From: kairoski03 Date: Wed, 15 Nov 2023 18:54:09 +0900 Subject: [PATCH 1/2] Add verification feature --- src/api/hooks/useVerificationCheck.ts | 32 +++++ src/api/hooks/useVerificationRequester.ts | 39 +++++++ src/constants.tsx | 8 ++ src/global-config/GlobalConfig.tsx | 19 ++- src/pages/Account/Components/CodeSnippet.tsx | 78 ++++++++++++- .../Account/Components/VerificationButton.tsx | 109 ++++++++++++++++++ .../VerificationServiceUrlInput.tsx | 49 ++++++++ .../Account/Components/codeDescription.ts | 22 ++++ 8 files changed, 350 insertions(+), 6 deletions(-) create mode 100644 src/api/hooks/useVerificationCheck.ts create mode 100644 src/api/hooks/useVerificationRequester.ts create mode 100644 src/pages/Account/Components/VerificationButton.tsx create mode 100644 src/pages/Account/Components/VerificationServiceUrlInput.tsx create mode 100644 src/pages/Account/Components/codeDescription.ts 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..67be97a8 --- /dev/null +++ b/src/api/hooks/useVerificationRequester.ts @@ -0,0 +1,39 @@ +import {useCallback} from "react"; +import {useGlobalState} from "../../global-config/GlobalConfig"; + +export interface AptosVerificationResultDto { + network: string; + account: string; + moduleName: string; + errMsg?: string; + status?: "VERIFIED_SAME" | "VERIFIED_DIFFERENT" | "NOT_VERIFIED"; + 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 a88d9c9b..21c79b17 100644 --- a/src/constants.tsx +++ b/src/constants.tsx @@ -19,6 +19,14 @@ export function isValidNetworkName(value: string): value is NetworkName { return value in networks; } +export const defaultVerificationServiceUrl = + "https://verify.welldonestudio.io/aptos"; + +export type VerificationStatus = + | "VERIFIED_SAME" + | "VERIFIED_DIFFERENT" + | "NOT_VERIFIED"; + export enum Network { MAINNET = "mainnet", TESTNET = "testnet", diff --git a/src/global-config/GlobalConfig.tsx b/src/global-config/GlobalConfig.tsx index b3bbc7ae..ef0bcd86 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, @@ -59,6 +60,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, @@ -67,6 +74,8 @@ export const GlobalStateProvider = ({ }) => { const [selectedFeature, selectFeature] = useFeatureSelector(); const [selectedNetwork, selectNetwork] = useNetworkSelector(); + const [endpoint, setEndpoint] = useState(defaultVerificationServiceUrl); + const globalState: GlobalState = useMemo( () => deriveGlobalState({ @@ -87,7 +96,11 @@ export const GlobalStateProvider = ({ return ( - {children} + + + {children} + + ); @@ -97,4 +110,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..c997cd41 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("NOT_VERIFIED"); + const [verificationServerErr, setVerificationServerErr] = + useState(""); + async function copyCode() { if (!sourceCode) return; @@ -134,7 +155,36 @@ export function Code({bytecode}: {bytecode: string}) { 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("NOT_VERIFIED"); + setVerificationServerErr(`${dto.errMsg}`); + } + + if (!dto.status) { + setVerificationStatus("NOT_VERIFIED"); + return; + } + + setVerificationStatus(dto.status); + setCodeDescription(genCodeDescription(dto.status)); + setVerificationServerErr(""); + }) + .catch((reason: Error) => { + console.error(reason); + setVerificationStatus("NOT_VERIFIED"); + setCodeDescription(genCodeDescription("NOT_VERIFIED")); + setVerificationServerErr("Verification service is not working."); + }); + } + }, [address, selectedModuleName, verificationServiceEndpoint]); return ( @@ -155,6 +205,27 @@ export function Code({bytecode}: {bytecode: string}) { Code + + + + + + {sourceCode && ( @@ -204,8 +275,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..623651bf --- /dev/null +++ b/src/pages/Account/Components/VerificationButton.tsx @@ -0,0 +1,109 @@ +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("NOT_VERIFIED"); + setVerificationServerErr(`${dto.errMsg}`); + } + + if (!dto.status) { + setVerificationStatus("NOT_VERIFIED"); + return; + } + + setVerificationStatus(dto.status); + setCodeDescription(genCodeDescription(dto.status)); + setVerificationServerErr(""); + }) + .catch((reason: Error) => { + console.error(reason); + setVerificationStatus("NOT_VERIFIED"); + setCodeDescription(genCodeDescription("NOT_VERIFIED")); + setVerificationServerErr("Verification service is not working."); + }) + .finally(() => { + setIsInProgress(false); + }); + }; + + if (verificationStatus === "VERIFIED_DIFFERENT") { + return ( + + ); + } + + if (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..1276ca46 --- /dev/null +++ b/src/pages/Account/Components/VerificationServiceUrlInput.tsx @@ -0,0 +1,49 @@ +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..6fd6e501 --- /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 === "VERIFIED_DIFFERENT") { + return CODE_DESCRIPTION_VERIFIED_DIFFERENT; + } + + if (status === "VERIFIED_SAME") { + return CODE_DESCRIPTION_VERIFIED_SAME; + } + + return CODE_DESCRIPTION_NOT_VERIFIED; +} From da958927f213913e82c173d2c66144b6a802c5c4 Mon Sep 17 00:00:00 2001 From: kairoski03 Date: Thu, 11 Jan 2024 15:50:47 +0900 Subject: [PATCH 2/2] refactor verification related status, text, style and fix lint (#631) --- src/api/hooks/useVerificationRequester.ts | 3 +- src/constants.tsx | 9 ++- src/pages/Account/Components/CodeSnippet.tsx | 77 ++++++++++--------- .../Account/Components/VerificationButton.tsx | 27 ++++--- .../VerificationServiceUrlInput.tsx | 10 ++- .../Account/Components/codeDescription.ts | 4 +- 6 files changed, 75 insertions(+), 55 deletions(-) diff --git a/src/api/hooks/useVerificationRequester.ts b/src/api/hooks/useVerificationRequester.ts index 67be97a8..9e641a0b 100644 --- a/src/api/hooks/useVerificationRequester.ts +++ b/src/api/hooks/useVerificationRequester.ts @@ -1,12 +1,13 @@ 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?: "VERIFIED_SAME" | "VERIFIED_DIFFERENT" | "NOT_VERIFIED"; + status?: VerificationStatus; onChainByteCode?: string; compiledByteCode?: string; } diff --git a/src/constants.tsx b/src/constants.tsx index 66eaa570..50749862 100644 --- a/src/constants.tsx +++ b/src/constants.tsx @@ -23,10 +23,11 @@ export function isValidNetworkName(value: string): value is NetworkName { export const defaultVerificationServiceUrl = "https://verify.welldonestudio.io/aptos"; -export type VerificationStatus = - | "VERIFIED_SAME" - | "VERIFIED_DIFFERENT" - | "NOT_VERIFIED"; +export enum VerificationStatus { + VERIFIED_SAME = "VERIFIED_SAME", + VERIFIED_DIFFERENT = "VERIFIED_DIFFERENT", + NOT_VERIFIED = "NOT_VERIFIED", +} export enum Network { MAINNET = "mainnet", diff --git a/src/pages/Account/Components/CodeSnippet.tsx b/src/pages/Account/Components/CodeSnippet.tsx index c997cd41..843cfb3c 100644 --- a/src/pages/Account/Components/CodeSnippet.tsx +++ b/src/pages/Account/Components/CodeSnippet.tsx @@ -133,7 +133,7 @@ export function Code({bytecode}: {bytecode: string}) { CODE_DESCRIPTION_NOT_VERIFIED, ); const [verificationStatus, setVerificationStatus] = - useState("NOT_VERIFIED"); + useState(VerificationStatus.NOT_VERIFIED); const [verificationServerErr, setVerificationServerErr] = useState(""); @@ -150,41 +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; - } - - if (state.network_name && address && selectedModuleName) { - checkVerification({ - network: state.network_name, - account: address, - moduleName: selectedModuleName, - }) - .then((dto: AptosVerificationCheckDto) => { - if (dto.errMsg) { - setVerificationStatus("NOT_VERIFIED"); - setVerificationServerErr(`${dto.errMsg}`); - } + useEffect( + () => { + if (codeBoxScrollRef.current) { + codeBoxScrollRef.current.scrollTop = + LINE_HEIGHT_IN_PX * startingLineNumber; + } - if (!dto.status) { - setVerificationStatus("NOT_VERIFIED"); - return; - } - - setVerificationStatus(dto.status); - setCodeDescription(genCodeDescription(dto.status)); - setVerificationServerErr(""); + if (state.network_name && address && selectedModuleName) { + checkVerification({ + network: state.network_name, + account: address, + moduleName: selectedModuleName, }) - .catch((reason: Error) => { - console.error(reason); - setVerificationStatus("NOT_VERIFIED"); - setCodeDescription(genCodeDescription("NOT_VERIFIED")); - setVerificationServerErr("Verification service is not working."); - }); - } - }, [address, selectedModuleName, verificationServiceEndpoint]); + .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 ( @@ -204,7 +209,9 @@ export function Code({bytecode}: {bytecode: string}) { Code - + {verificationStatus === VerificationStatus.NOT_VERIFIED ? ( + + ) : null} { setIsInProgress(false); if (dto.errMsg) { - setVerificationStatus("NOT_VERIFIED"); + setVerificationStatus(VerificationStatus.NOT_VERIFIED); setVerificationServerErr(`${dto.errMsg}`); } if (!dto.status) { - setVerificationStatus("NOT_VERIFIED"); + setVerificationStatus(VerificationStatus.NOT_VERIFIED); return; } @@ -56,8 +56,8 @@ export default function VerificationButton({ }) .catch((reason: Error) => { console.error(reason); - setVerificationStatus("NOT_VERIFIED"); - setCodeDescription(genCodeDescription("NOT_VERIFIED")); + setVerificationStatus(VerificationStatus.NOT_VERIFIED); + setCodeDescription(genCodeDescription(VerificationStatus.NOT_VERIFIED)); setVerificationServerErr("Verification service is not working."); }) .finally(() => { @@ -65,28 +65,30 @@ export default function VerificationButton({ }); }; - if (verificationStatus === "VERIFIED_DIFFERENT") { + if (verificationStatus === VerificationStatus.VERIFIED_DIFFERENT) { return ( ); } - if (verificationStatus === "VERIFIED_SAME") { + if (verificationStatus === VerificationStatus.VERIFIED_SAME) { return ( ); } @@ -96,13 +98,14 @@ export default function VerificationButton({ type="submit" disabled={isInProgress} variant="contained" - sx={{width: "10rem", height: "3rem"}} + sx={{width: "16rem", height: "3rem"}} onClick={verifyClick} + style={{textTransform: "none"}} > {verificationStatus === undefined || isInProgress ? ( ) : ( - "Verify" + "Verify Source Code" )} ); diff --git a/src/pages/Account/Components/VerificationServiceUrlInput.tsx b/src/pages/Account/Components/VerificationServiceUrlInput.tsx index 1276ca46..70772902 100644 --- a/src/pages/Account/Components/VerificationServiceUrlInput.tsx +++ b/src/pages/Account/Components/VerificationServiceUrlInput.tsx @@ -40,7 +40,15 @@ export default function VerificationServiceUrlInput({ style={{height: "1.5em"}} /> {verificationServerErr ? ( -
{verificationServerErr}
+
+ {verificationServerErr} +
) : null}
diff --git a/src/pages/Account/Components/codeDescription.ts b/src/pages/Account/Components/codeDescription.ts index 6fd6e501..c3e83f03 100644 --- a/src/pages/Account/Components/codeDescription.ts +++ b/src/pages/Account/Components/codeDescription.ts @@ -10,11 +10,11 @@ export const CODE_DESCRIPTION_VERIFIED_SAME = "The source code is same to the actual bytecode."; export function genCodeDescription(status: VerificationStatus) { - if (status === "VERIFIED_DIFFERENT") { + if (status === VerificationStatus.VERIFIED_DIFFERENT) { return CODE_DESCRIPTION_VERIFIED_DIFFERENT; } - if (status === "VERIFIED_SAME") { + if (status === VerificationStatus.VERIFIED_SAME) { return CODE_DESCRIPTION_VERIFIED_SAME; }