diff --git a/.gitignore b/.gitignore index 25b593ecb..0f3e0c9da 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ # IDEs and editors /.idea /.vscode +/.claude +/.cursor +/.taskmaster # misc .DS_Store diff --git a/src/api/client.ts b/src/api/client.ts index 8b2a5f0fb..8d9949feb 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -3,6 +3,8 @@ export enum ResponseErrorType { INVALID_INPUT = "Invalid Input", UNHANDLED = "Unhandled", TOO_MANY_REQUESTS = "Too Many Requests", + SERVICE_UNAVAILABLE = "Service Unavailable", + COMPILATION_ERROR = "Compilation Error", } export type ResponseError = {type: ResponseErrorType; message?: string}; @@ -16,6 +18,9 @@ export async function withResponseError(promise: Promise): Promise { if (error.status === 404) { throw {type: ResponseErrorType.NOT_FOUND}; } + if (error.status === 503) { + throw {type: ResponseErrorType.SERVICE_UNAVAILABLE}; + } } if ( error.message @@ -33,3 +38,50 @@ export async function withResponseError(promise: Promise): Promise { }; }); } + +export interface ModuleVerificationStatusResponse { + verified: boolean; +} + +/** Fetch module verification status for a specific contract version (upgrade_number). + * Throws: + * - NOT_FOUND (404) - no source code available, or contract upgraded (refresh page) + * - SERVICE_UNAVAILABLE (503) - verification disabled on node + * - COMPILATION_ERROR (422) - cannot verify (e.g., unsupported dependencies) + */ +export async function getModuleVerificationStatus( + nodeUrl: string, + address: string, + moduleName: string, + upgradeNumber?: number, +): Promise { + const params = upgradeNumber != null ? `?upgrade_number=${upgradeNumber}` : ""; + const url = `${nodeUrl}/v1/accounts/${address}/modules/${moduleName}/verification_status${params}`; + + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + if (response.status === 404) { + throw {type: ResponseErrorType.NOT_FOUND}; + } + if (response.status === 422) { + // Compilation error - can't verify, but not suspicious + const errorData = await response.json().catch(() => ({})); + throw {type: ResponseErrorType.COMPILATION_ERROR, message: errorData.message}; + } + if (response.status === 503) { + throw {type: ResponseErrorType.SERVICE_UNAVAILABLE}; + } + throw { + type: ResponseErrorType.UNHANDLED, + message: `HTTP error! status: ${response.status}`, + }; + } + + return await response.json(); +} diff --git a/src/api/hooks/useGetModuleVerificationStatus.ts b/src/api/hooks/useGetModuleVerificationStatus.ts new file mode 100644 index 000000000..4b2792be1 --- /dev/null +++ b/src/api/hooks/useGetModuleVerificationStatus.ts @@ -0,0 +1,47 @@ +import {useQuery, UseQueryResult} from "@tanstack/react-query"; +import { + getModuleVerificationStatus, + ModuleVerificationStatusResponse, + ResponseError, + ResponseErrorType, +} from "../client"; +import {useGlobalState} from "../../global-config/GlobalConfig"; + +/** Hook to query module bytecode verification status for a contract version (upgrade_number). */ +export function useGetModuleVerificationStatus( + address: string, + moduleName: string, + options?: {enabled?: boolean; upgradeNumber?: number}, +): UseQueryResult { + const [state] = useGlobalState(); + const {upgradeNumber, ...rest} = options ?? {}; + + return useQuery({ + queryKey: [ + "moduleVerificationStatus", + {address, moduleName, upgradeNumber}, + state.network_value, + ], + queryFn: () => + getModuleVerificationStatus( + state.network_value, + address, + moduleName, + upgradeNumber, + ), + refetchOnWindowFocus: false, + retry: (failureCount, error) => { + // Don't retry for expected error types + if ( + error.type === ResponseErrorType.NOT_FOUND || + error.type === ResponseErrorType.SERVICE_UNAVAILABLE || + error.type === ResponseErrorType.COMPILATION_ERROR + ) { + return false; + } + return failureCount < 2; + }, + ...rest, + }); +} + diff --git a/src/constants.tsx b/src/constants.tsx index e021debdc..2a1c0d45b 100644 --- a/src/constants.tsx +++ b/src/constants.tsx @@ -13,15 +13,15 @@ export const bardockTestnetUrl = import.meta.env.MOVEMENT_TESTNET_URL || `https://testnet.movementnetwork.xyz/v1`; - export const networks = { - mainnet: mainnetUrl, - testnet: "", - "bardock testnet": bardockTestnetUrl, +export const networks = { + mainnet: mainnetUrl, + testnet: "", + "bardock testnet": bardockTestnetUrl, devnet: "", - local: "http://localhost:30731", - mevmdevnet: "", - custom: "", - }; + local: "http://127.0.0.1:8080", + mevmdevnet: "", + custom: "", +}; export const availableNetworks = ["mainnet", "bardock testnet"]; diff --git a/src/pages/Account/Components/CodeSnippet.tsx b/src/pages/Account/Components/CodeSnippet.tsx index 328141334..2befee7f3 100644 --- a/src/pages/Account/Components/CodeSnippet.tsx +++ b/src/pages/Account/Components/CodeSnippet.tsx @@ -1,4 +1,13 @@ -import {Box, Button, Modal, Stack, Typography, useTheme} from "@mui/material"; +import { + Alert, + Box, + Button, + CircularProgress, + Modal, + Stack, + Typography, + useTheme, +} from "@mui/material"; import {ContentCopy, OpenInFull} from "@mui/icons-material"; import SyntaxHighlighter from "react-syntax-highlighter"; import {getPublicFunctionLineNumber, transformCode} from "../../../utils"; @@ -18,6 +27,8 @@ import { } from "../../../themes/colors/aptosColorPalette"; import {useParams} from "react-router-dom"; import {useLogEventWithBasic} from "../hooks/useLogEventWithBasic"; +import {useGetModuleVerificationStatus} from "../../../api/hooks/useGetModuleVerificationStatus"; +import {ResponseErrorType} from "../../../api/client"; function useStartingLineNumber(sourceCode?: string) { const functionToHighlight = useParams().selectedFnName; @@ -105,15 +116,39 @@ function ExpandCode({sourceCode}: {sourceCode: string | undefined}) { ); } -export function Code({bytecode}: {bytecode: string}) { +/** Displays source code with a warning if bytecode verification fails. */ +export function Code({ + bytecode, + address, + moduleName, + upgradeNumber, +}: { + bytecode: string; + address?: string; + moduleName?: string; + upgradeNumber?: number; +}) { const {selectedModuleName} = useParams(); const logEvent = useLogEventWithBasic(); + const theme = useTheme(); + + // Use the module name from props or from URL params + const moduleNameToVerify = moduleName || selectedModuleName || ""; + + // Query verification for this contract version (upgrade_number) + const { + data: verificationStatus, + isLoading: isVerificationLoading, + error: verificationError, + } = useGetModuleVerificationStatus(address || "", moduleNameToVerify, { + enabled: !!address && !!moduleNameToVerify, + upgradeNumber, + }); const TOOLTIP_TIME = 2000; // 2s const sourceCode = bytecode === "0x" ? undefined : transformCode(bytecode); - const theme = useTheme(); const [tooltipOpen, setTooltipOpen] = useState(false); async function copyCode() { @@ -136,6 +171,14 @@ export function Code({bytecode}: {bytecode: string}) { } }); + // Always show code if sourceCode exists, with warnings when appropriate + const isVerified = verificationStatus?.verified === true; + const hasVerificationFailure = verificationStatus?.verified === false; + const hasVerificationDisabled = verificationError?.type === ResponseErrorType.SERVICE_UNAVAILABLE; + const hasVerificationUnavailable = verificationError?.type === ResponseErrorType.NOT_FOUND; + const hasCompilationError = verificationError?.type === ResponseErrorType.COMPILATION_ERROR; + const shouldShowCode = !!sourceCode; + return ( Code - {sourceCode && ( @@ -196,44 +238,68 @@ export function Code({bytecode}: {bytecode: string}) { )} - {sourceCode && ( - - The source code is plain text uploaded by the deployer, which can be - different from the actual bytecode. - - )} - {!sourceCode ? ( - - Unfortunately, the source code cannot be shown because the package - publisher has chosen not to make it available + + Verifying source code... + ) : shouldShowCode ? ( + + {isVerified && ( + + Source code verified: the displayed code matches the deployed bytecode. + + )} + {hasVerificationFailure && ( + + The deployer provided source code but it does not match the deployed bytecode. The displayed code may not accurately represent what is actually running on-chain. + + )} + {hasCompilationError && ( + + This contract cannot be verified because it uses dependencies that are not part of the standard Aptos framework. The displayed code was provided by the deployer and may not match the deployed bytecode. + + )} + {(hasVerificationDisabled || hasVerificationUnavailable) && ( + + Source code verification is not available on this node. The displayed code was provided by the deployer and may not match the deployed bytecode. + + )} + + + {sourceCode} + + + ) : ( - - {sourceCode} - + + The source code cannot be shown because the package publisher has chosen not to make it available. + )} diff --git a/src/pages/Account/Tabs/ModulesTab/Contract.tsx b/src/pages/Account/Tabs/ModulesTab/Contract.tsx index 8d2d06d14..e3d87426b 100644 --- a/src/pages/Account/Tabs/ModulesTab/Contract.tsx +++ b/src/pages/Account/Tabs/ModulesTab/Contract.tsx @@ -72,6 +72,11 @@ function Contract({ const {data, isLoading, error} = useGetAccountModules(address); const {selectedModuleName, selectedFnName} = useParams(); const sortedPackages: PackageMetadata[] = useGetAccountPackages(address); + const selectedPackage = sortedPackages.find((p) => + p.modules.some((m) => m.name === selectedModuleName), + ); + const upgradeNumber = + selectedPackage != null ? Number(selectedPackage.upgrade_number) : undefined; const selectedModule = sortedPackages .flatMap((pkg) => pkg.modules) .find((module) => module.name === selectedModuleName); @@ -158,7 +163,12 @@ function Contract({ {module && fn && selectedModule && ( <> - + )} diff --git a/src/pages/Account/Tabs/ModulesTab/ViewCode.tsx b/src/pages/Account/Tabs/ModulesTab/ViewCode.tsx index 165ed7c07..a1a38bdf9 100644 --- a/src/pages/Account/Tabs/ModulesTab/ViewCode.tsx +++ b/src/pages/Account/Tabs/ModulesTab/ViewCode.tsx @@ -39,14 +39,20 @@ interface ModuleContentProps { address: string; moduleName: string; bytecode: string; + upgradeNumber?: number; } function ViewCode({address, isObject}: {address: string; isObject: boolean}) { const sortedPackages: PackageMetadata[] = useGetAccountPackages(address); - const navigate = useNavigate(); - const selectedModuleName = useParams().selectedModuleName ?? ""; + + const selectedPackage = sortedPackages.find((p) => + p.modules.some((m) => m.name === selectedModuleName), + ); + const upgradeNumber = + selectedPackage != null ? Number(selectedPackage.upgrade_number) : undefined; + useEffect(() => { if ( !selectedModuleName && @@ -98,6 +104,7 @@ function ViewCode({address, isObject}: {address: string; isObject: boolean}) { address={address} moduleName={selectedModuleName} bytecode={selectedModule.source} + upgradeNumber={upgradeNumber} /> )} @@ -179,7 +186,12 @@ function ModuleSidebar({ ); } -function ModuleContent({address, moduleName, bytecode}: ModuleContentProps) { +function ModuleContent({ + address, + moduleName, + bytecode, + upgradeNumber, +}: ModuleContentProps) { const theme = useTheme(); return ( - +