Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
# IDEs and editors
/.idea
/.vscode
/.claude
/.cursor
/.taskmaster

# misc
.DS_Store
Expand Down
52 changes: 52 additions & 0 deletions src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -16,6 +18,9 @@ export async function withResponseError<T>(promise: Promise<T>): Promise<T> {
if (error.status === 404) {
throw {type: ResponseErrorType.NOT_FOUND};
}
if (error.status === 503) {
throw {type: ResponseErrorType.SERVICE_UNAVAILABLE};
}
}
if (
error.message
Expand All @@ -33,3 +38,50 @@ export async function withResponseError<T>(promise: Promise<T>): Promise<T> {
};
});
}

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<ModuleVerificationStatusResponse> {
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();
}
47 changes: 47 additions & 0 deletions src/api/hooks/useGetModuleVerificationStatus.ts
Original file line number Diff line number Diff line change
@@ -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<ModuleVerificationStatusResponse, ResponseError> {
const [state] = useGlobalState();
const {upgradeNumber, ...rest} = options ?? {};

return useQuery<ModuleVerificationStatusResponse, ResponseError>({
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,
});
}

16 changes: 8 additions & 8 deletions src/constants.tsx

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this change is intentional, flagging it in case it affects other existing workflows

@apenzk apenzk Jan 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx for pointing this out.

The new value for local network URL matches aptos-core's default REST API port. (defined in aptos-core/config/src/config/api_config.rs as DEFAULT_PORT: u16 = 8080).

i did not understand the reason why it should be 30731

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iirc it was 30731 with the old process-compose but yes 8080 should be correct default now... think @musitdev would know with greater certainty.

Original file line number Diff line number Diff line change
Expand Up @@ -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: "",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apenzk could you clean up the indentation:

  • Line 16: export has extra leading spaces
  • Line 20: devnet is missing indentation

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

local: "http://localhost:30731",
mevmdevnet: "",
custom: "",
};
local: "http://127.0.0.1:8080",
mevmdevnet: "",
custom: "",
};

export const availableNetworks = ["mainnet", "bardock testnet"];

Expand Down
140 changes: 103 additions & 37 deletions src/pages/Account/Components/CodeSnippet.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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<boolean>(false);

async function copyCode() {
Expand All @@ -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 (
<Box>
<Stack
Expand All @@ -154,7 +197,6 @@ export function Code({bytecode}: {bytecode: string}) {
<Typography fontSize={20} fontWeight={700}>
Code
</Typography>
<StyledLearnMoreTooltip text="Please be aware that this code was provided by the owner and it could be different to the real code on blockchain. We cannot verify it." />
</Stack>
{sourceCode && (
<Stack direction="row" spacing={2}>
Expand Down Expand Up @@ -196,44 +238,68 @@ export function Code({bytecode}: {bytecode: string}) {
</Stack>
)}
</Stack>
{sourceCode && (
<Typography
variant="body1"
fontSize={14}
fontWeight={400}
marginBottom={"16px"}
color={theme.palette.mode === "dark" ? grey[400] : grey[600]}
{isVerificationLoading ? (
<Box
display="flex"
alignItems="center"
justifyContent="center"
padding={4}
>
The source code is plain text uploaded by the deployer, which can be
different from the actual bytecode.
</Typography>
)}
{!sourceCode ? (
<Box>
Unfortunately, the source code cannot be shown because the package
publisher has chosen not to make it available
<CircularProgress size={24} />
<Typography marginLeft={2}>Verifying source code...</Typography>
</Box>
) : shouldShowCode ? (
<Stack spacing={1}>
{isVerified && (
<Alert severity="success">
Source code verified: the displayed code matches the deployed bytecode.
</Alert>
)}
{hasVerificationFailure && (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add message when verification is successful:

Suggested change
{hasVerificationFailure && (
{isVerified && (
<Alert severity="success">
Source code verified: the displayed code matches the deployed bytecode.
</Alert>
)}
{hasVerificationFailure && (

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

<Alert severity="error">
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
The deployer provided source code but it does not match the deployed bytecode. The displayed code does not accurately represent what is actually running on-chain.

@apenzk apenzk Jan 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was going for the less strong word, because i would assume if there is a differing comment it would also fail? which can be construed as not being different with respect to code... is this wrong?

</Alert>
)}
{hasCompilationError && (
<Alert severity="warning">
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.
</Alert>
)}
{(hasVerificationDisabled || hasVerificationUnavailable) && (
<Alert severity="info">
Source code verification is not available on this node. The displayed code was provided by the deployer and may not match the deployed bytecode.
</Alert>
)}
<Box
sx={{
maxHeight: "100vh",
overflow: "auto",
borderRadius: 0,
backgroundColor: codeBlockColor,
}}
ref={codeBoxScrollRef}
>
<SyntaxHighlighter
language="rust"
key={theme.palette.mode}
style={
theme.palette.mode === "light" ? solarizedLight : solarizedDark
}
customStyle={{margin: 0, backgroundColor: "unset"}}
showLineNumbers
>
{sourceCode}
</SyntaxHighlighter>
</Box>
</Stack>
) : (
<Box
sx={{
maxHeight: "100vh",
overflow: "auto",
borderRadius: 0,
backgroundColor: codeBlockColor,
}}
ref={codeBoxScrollRef}
padding={2}
bgcolor={theme.palette.mode === "dark" ? grey[800] : grey[100]}
>
<SyntaxHighlighter
language="rust"
key={theme.palette.mode}
style={
theme.palette.mode === "light" ? solarizedLight : solarizedDark
}
customStyle={{margin: 0, backgroundColor: "unset"}}
showLineNumbers
>
{sourceCode}
</SyntaxHighlighter>
<Typography color={grey[500]}>
The source code cannot be shown because the package publisher has chosen not to make it available.
</Typography>
</Box>
)}
</Box>
Expand Down
12 changes: 11 additions & 1 deletion src/pages/Account/Tabs/ModulesTab/Contract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -158,7 +163,12 @@ function Contract({
{module && fn && selectedModule && (
<>
<Divider sx={{margin: "24px 0"}} />
<Code bytecode={selectedModule?.source} />
<Code
bytecode={selectedModule?.source}
address={address}
moduleName={selectedModuleName}
upgradeNumber={upgradeNumber}
/>
</>
)}
</Box>
Expand Down
Loading