diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ee029c13..ea95d52ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Features
- [#1211](https://github.com/alleslabs/celatone-frontend/pull/1211) Implement evm gas refund logic
+- [#1210](https://github.com/alleslabs/celatone-frontend/pull/1210) Add EVM contract details code preview
- [#1209](https://github.com/alleslabs/celatone-frontend/pull/1209) Implement evm contract details interaction
- [#1208](https://github.com/alleslabs/celatone-frontend/pull/1208) Implement evm interaction section
- [#1207](https://github.com/alleslabs/celatone-frontend/pull/1207) Add EVM contract details compiler settings
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6384af363..66395bf9c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -163,7 +163,7 @@ importers:
version: 1.1.0(next@14.2.23(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(svelte@4.2.19)
ace-builds:
specifier: ^1.12.5
- version: 1.37.3
+ version: 1.37.5
axios:
specifier: ^1.7.4
version: 1.7.9
@@ -4763,8 +4763,8 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
- ace-builds@1.37.3:
- resolution: {integrity: sha512-LXMNR57LGyUaJZoAXqVuy6x/ipQvj62iKUHv0DlOb57DNRiV3ZIvYNTVeBHCkUeQAc3BBmyLWC2uvMixSbhj9Q==}
+ ace-builds@1.37.5:
+ resolution: {integrity: sha512-VMJ4Cnhq6L9dwvOCyuyyvQuiVTSwdZC7zDKJBBBJJax0wGQ7MvzQZFoi0gMmCm2I4Zuv/ZbtwU/dlglIhCNLhw==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@@ -17501,7 +17501,7 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
- ace-builds@1.37.3: {}
+ ace-builds@1.37.5: {}
acorn-jsx@5.3.2(acorn@7.4.1):
dependencies:
@@ -22549,7 +22549,7 @@ snapshots:
react-ace@10.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
- ace-builds: 1.37.3
+ ace-builds: 1.37.5
diff-match-patch: 1.0.5
lodash.get: 4.4.2
lodash.isequal: 4.5.0
diff --git a/src/lib/components/editor/Editor.tsx b/src/lib/components/editor/Editor.tsx
new file mode 100644
index 000000000..858f04cb5
--- /dev/null
+++ b/src/lib/components/editor/Editor.tsx
@@ -0,0 +1,27 @@
+import type { EditorProps, Monaco } from "@monaco-editor/react";
+import MonacoEditor from "@monaco-editor/react";
+
+import { moveLanguageConfig, moveTokenProvider } from "./moveSyntax";
+
+const loadMoveSyntax = (monaco: Monaco) => {
+ monaco.languages.register({ id: "move" });
+ monaco.languages.onLanguage("move", () => {
+ monaco.languages.setMonarchTokensProvider("move", moveTokenProvider);
+ monaco.languages.setLanguageConfiguration("move", moveLanguageConfig);
+ });
+};
+
+export const Editor = ({ ...props }: EditorProps) => (
+
+);
diff --git a/src/lib/components/editor/EditorFileBody.tsx b/src/lib/components/editor/EditorFileBody.tsx
new file mode 100644
index 000000000..330e58775
--- /dev/null
+++ b/src/lib/components/editor/EditorFileBody.tsx
@@ -0,0 +1,45 @@
+import { Flex, FlexProps, Text } from "@chakra-ui/react";
+import { SourceTreeNode } from "./types";
+import { CustomIcon } from "../icon";
+
+interface EditorFileBodyProps extends FlexProps {
+ node: SourceTreeNode;
+ initialFilePath: string;
+ isNoWrap?: boolean;
+}
+
+export const EditorFileBody = ({
+ node,
+ initialFilePath,
+ isNoWrap,
+ ...props
+}: EditorFileBodyProps) => (
+
+ {node.isFolder ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+ {initialFilePath === node.path && (
+
+ )}
+
+ >
+ )}
+
+ {node.name}
+
+
+);
diff --git a/src/lib/components/editor/EditorSidebar.tsx b/src/lib/components/editor/EditorSidebar.tsx
new file mode 100644
index 000000000..beab290e1
--- /dev/null
+++ b/src/lib/components/editor/EditorSidebar.tsx
@@ -0,0 +1,94 @@
+import { useState } from "react";
+import { SourceTreeNode } from "./types";
+import { Box, Flex } from "@chakra-ui/react";
+import { EditorFileBody } from "./EditorFileBody";
+import { Nullable } from "lib/types";
+
+export interface EditorSidebarProps {
+ sourceTreeNode: SourceTreeNode[];
+ selectedFile: Nullable;
+ initialFilePath: string;
+ onClick: (node: SourceTreeNode) => void;
+}
+
+const EditorSidebarSelectedMark = () => (
+
+
+
+);
+
+export const EditorSidebar = ({
+ sourceTreeNode,
+ initialFilePath,
+ onClick,
+ selectedFile,
+}: EditorSidebarProps) => {
+ const [tree, setTree] = useState(sourceTreeNode);
+
+ const handleUpdateIsOpen = (
+ node: SourceTreeNode,
+ tree: SourceTreeNode[]
+ ): SourceTreeNode[] =>
+ tree.map((n) => {
+ if (n.path === node.path) {
+ return { ...n, isOpen: !n.isOpen };
+ }
+
+ return { ...n, children: handleUpdateIsOpen(node, n.children) };
+ });
+
+ const onUpdateIsOpen = (node: SourceTreeNode) => {
+ const updatedTree = handleUpdateIsOpen(node, tree);
+ setTree(updatedTree);
+ };
+
+ return tree.map((node) => {
+ const isSelected = node.path === selectedFile?.path;
+
+ return (
+
+
+ (node.isFolder ? onUpdateIsOpen(node) : onClick(node))}
+ >
+ {isSelected && }
+
+
+ {node.children.length > 0 && node.isOpen && (
+
+ )}
+
+ );
+ });
+};
diff --git a/src/lib/components/editor/EditorTop.tsx b/src/lib/components/editor/EditorTop.tsx
new file mode 100644
index 000000000..f0f331fc8
--- /dev/null
+++ b/src/lib/components/editor/EditorTop.tsx
@@ -0,0 +1,106 @@
+import { Box, Flex, Text } from "@chakra-ui/react";
+import { SourceTreeNode } from "./types";
+import { EditorFileBody } from "./EditorFileBody";
+import { CustomIcon } from "../icon";
+import { Nullable } from "lib/types";
+import { Fragment } from "react";
+
+interface EditorTopProps {
+ filesList: SourceTreeNode[];
+ selectedFile: Nullable;
+ initialFilePath: string;
+ onClick: (index: number) => void;
+ onRemove: (node: SourceTreeNode, index: number) => void;
+}
+
+const vsCodeDarkColor = "#1E1E1E";
+
+export const EditorTop = ({
+ filesList,
+ selectedFile,
+ initialFilePath,
+ onClick,
+ onRemove,
+}: EditorTopProps) => (
+
+
+ {filesList.map((node, index) => {
+ const isInitialFilePath = initialFilePath === node.path;
+ const isSelected = selectedFile?.path === node.path;
+
+ return (
+ onClick(index)}
+ sx={{
+ borderWidth: "1px",
+ borderTopColor: isSelected ? "primary.main" : vsCodeDarkColor,
+ borderLeftColor: "transparent",
+ borderRightColor: "gray.700",
+ borderBottomColor: isSelected ? vsCodeDarkColor : "gray.700",
+ bgColor: isSelected ? vsCodeDarkColor : "gray.900",
+ "&:first-of-type": {
+ borderLeftColor: vsCodeDarkColor,
+ },
+ }}
+ >
+
+ {!isInitialFilePath && (
+ {
+ e.stopPropagation();
+ onRemove(node, index);
+ }}
+ />
+ )}
+
+ );
+ })}
+
+ {selectedFile && (
+
+
+ {selectedFile.path.split("/").map((path, index) => (
+
+ {index !== 0 && (
+
+ )}
+
+ {path}
+
+
+ ))}
+
+
+ )}
+
+);
diff --git a/src/lib/components/editor/FullEditor.tsx b/src/lib/components/editor/FullEditor.tsx
new file mode 100644
index 000000000..f0c09857f
--- /dev/null
+++ b/src/lib/components/editor/FullEditor.tsx
@@ -0,0 +1,108 @@
+import { Box, Grid, Stack, Text } from "@chakra-ui/react";
+import { generateSourceTree } from "./helpers";
+import { FilePath, SourceTreeNode } from "./types";
+import { EditorSidebar } from "./EditorSidebar";
+import { useEffect, useState } from "react";
+import { EditorTop } from "./EditorTop";
+import { Nullable } from "lib/types";
+import { Editor } from "./Editor";
+import { useMobile } from "lib/app-provider";
+import {
+ FullEditorSidebarMobile,
+ FullEditorSidebarMobileProps,
+} from "./FullEditorSidebarMobile";
+
+interface FullEditorProps
+ extends Pick {
+ filesPath: FilePath[];
+ initialFilePath: string;
+}
+
+export const FullEditor = ({
+ filesPath,
+ initialFilePath,
+ isOpen,
+ onClose,
+}: FullEditorProps) => {
+ const isMobile = useMobile();
+ const [filesList, setFilesList] = useState([]);
+ const [selectedFile, setSelectedFile] =
+ useState>(null);
+ const generatedSourceTree = generateSourceTree(filesPath, initialFilePath);
+
+ const handleFindInitialFile = (tree: SourceTreeNode[]) => {
+ return tree.forEach((node) => {
+ if (node.path === initialFilePath) {
+ setFilesList([node]);
+ setSelectedFile(node);
+ return node;
+ } else if (node.children.length > 0) {
+ handleFindInitialFile(node.children);
+ }
+
+ return undefined;
+ });
+ };
+
+ useEffect(() => {
+ handleFindInitialFile(generatedSourceTree);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [setFilesList]);
+
+ const handleUpdateFilesList = (node: SourceTreeNode) => {
+ setSelectedFile(node);
+ onClose();
+ const foundNode = filesList.find((n) => n.path === node.path);
+ if (foundNode) return;
+ setFilesList([...filesList, node]);
+ };
+
+ const handleOnRemove = (node: SourceTreeNode, index: number) => {
+ const filteredFiles = filesList.filter((_, i) => i !== index);
+ setFilesList(filteredFiles);
+ if (selectedFile?.path === node.path)
+ setSelectedFile(filteredFiles[filteredFiles.length - 1]);
+ };
+
+ return (
+
+ {isMobile ? (
+
+ ) : (
+
+ Files
+
+
+
+
+ )}
+
+ setSelectedFile(filesList[index])}
+ onRemove={handleOnRemove}
+ filesList={filesList}
+ initialFilePath={initialFilePath}
+ />
+
+
+
+ );
+};
diff --git a/src/lib/components/editor/FullEditorSidebarMobile.tsx b/src/lib/components/editor/FullEditorSidebarMobile.tsx
new file mode 100644
index 000000000..a85cf2b7c
--- /dev/null
+++ b/src/lib/components/editor/FullEditorSidebarMobile.tsx
@@ -0,0 +1,39 @@
+import {
+ Box,
+ Drawer,
+ DrawerBody,
+ DrawerCloseButton,
+ DrawerContent,
+ DrawerHeader,
+ DrawerOverlay,
+ Heading,
+} from "@chakra-ui/react";
+import { EditorSidebar, EditorSidebarProps } from "./EditorSidebar";
+
+export interface FullEditorSidebarMobileProps extends EditorSidebarProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export const FullEditorSidebarMobile = ({
+ isOpen,
+ onClose,
+ ...props
+}: FullEditorSidebarMobileProps) => (
+
+
+
+
+
+ File Tree
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/src/lib/components/editor/helpers.ts b/src/lib/components/editor/helpers.ts
new file mode 100644
index 000000000..0350c3e9c
--- /dev/null
+++ b/src/lib/components/editor/helpers.ts
@@ -0,0 +1,40 @@
+import { last, split } from "lodash";
+import { EXTENSION_LIB, FilePath, SourceTreeNode } from "./types";
+
+export const generateSourceTree = (
+ filesPath: FilePath[],
+ initialFilePath: string
+): SourceTreeNode[] => {
+ const root: SourceTreeNode[] = [];
+
+ filesPath.forEach(({ path, code }) => {
+ const parts = path.split("/");
+ let currentLevel = root;
+
+ parts.forEach((part, index) => {
+ let existingNode = currentLevel.find((node) => node.name === part);
+
+ if (!existingNode) {
+ const extension = last(split(part, "."));
+ const isFolder = extension ? !EXTENSION_LIB.includes(extension) : false;
+ const isInitializeNodePath = initialFilePath === path;
+ const isOpen = index === 0 ? true : false || isInitializeNodePath;
+
+ existingNode = {
+ name: part,
+ isOpen,
+ children: [],
+ isFolder,
+ treeLevel: index,
+ code,
+ path: parts.slice(0, index + 1).join("/"),
+ };
+ currentLevel.push(existingNode);
+ }
+
+ currentLevel = existingNode.children;
+ });
+ });
+
+ return root;
+};
diff --git a/src/lib/components/module/moveSyntax.ts b/src/lib/components/editor/moveSyntax.ts
similarity index 100%
rename from src/lib/components/module/moveSyntax.ts
rename to src/lib/components/editor/moveSyntax.ts
diff --git a/src/lib/components/editor/types.ts b/src/lib/components/editor/types.ts
new file mode 100644
index 000000000..d02559d10
--- /dev/null
+++ b/src/lib/components/editor/types.ts
@@ -0,0 +1,14 @@
+export const EXTENSION_LIB = ["sol"];
+
+export interface FilePath {
+ path: string;
+ code: string;
+}
+
+export interface SourceTreeNode extends FilePath {
+ name: string;
+ isOpen: boolean;
+ isFolder: boolean;
+ treeLevel: number;
+ children: SourceTreeNode[];
+}
diff --git a/src/lib/components/icon/SvgIcon.tsx b/src/lib/components/icon/SvgIcon.tsx
index bb4db85b6..5d817f59e 100644
--- a/src/lib/components/icon/SvgIcon.tsx
+++ b/src/lib/components/icon/SvgIcon.tsx
@@ -351,6 +351,37 @@ export const ICONS = {
),
viewBox: "0 -3.5 16 16",
},
+ "code-file": {
+ svg: (
+ <>
+
+
+
+
+ >
+ ),
+ viewBox: "0 0 16 17",
+ },
collection: {
svg: (
<>
diff --git a/src/lib/components/module/ModuleSourceCode.tsx b/src/lib/components/module/ModuleSourceCode.tsx
index 0a2281545..acc7788aa 100644
--- a/src/lib/components/module/ModuleSourceCode.tsx
+++ b/src/lib/components/module/ModuleSourceCode.tsx
@@ -11,8 +11,6 @@ import {
Heading,
Text,
} from "@chakra-ui/react";
-import type { Monaco } from "@monaco-editor/react";
-import MonacoEditor from "@monaco-editor/react";
import { AppLink } from "../AppLink";
import { CopyButton } from "../copy";
@@ -22,16 +20,7 @@ import type { MoveVerifyInfoResponse } from "lib/services/types";
import { MoveVerifyStatus } from "lib/types";
import type { Nullish } from "lib/types";
import { formatUTC } from "lib/utils";
-
-import { moveLanguageConfig, moveTokenProvider } from "./moveSyntax";
-
-const loadMoveSyntax = (monaco: Monaco) => {
- monaco.languages.register({ id: "move" });
- monaco.languages.onLanguage("move", () => {
- monaco.languages.setMonarchTokensProvider("move", moveTokenProvider);
- monaco.languages.setLanguageConfiguration("move", moveLanguageConfig);
- });
-};
+import { Editor } from "../editor/Editor";
interface ModuleSourceCodeProps {
verificationData: Nullish;
@@ -120,14 +109,7 @@ export const ModuleSourceCode = ({
borderColor="gray.700"
borderRadius="8px"
>
-
+
diff --git a/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/ContractByteCode.tsx b/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/ContractByteCode.tsx
index 53cbe55f9..cfef46bd8 100644
--- a/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/ContractByteCode.tsx
+++ b/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/ContractByteCode.tsx
@@ -1,4 +1,4 @@
-import { Heading, Stack } from "@chakra-ui/react";
+import { Heading, Stack, Text } from "@chakra-ui/react";
import { TextReadOnly } from "lib/components/json/TextReadOnly";
import { Option } from "lib/types";
@@ -8,7 +8,7 @@ export interface ContractByteCodeProps {
}
export const ContractByteCode = ({
- byteCode = "",
+ byteCode,
deployedByteCode,
}: ContractByteCodeProps) => (
@@ -16,7 +16,13 @@ export const ContractByteCode = ({
ByteCode
-
+ {byteCode ? (
+
+ ) : (
+
+ -
+
+ )}
diff --git a/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/ContractCode.tsx b/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/ContractCode.tsx
new file mode 100644
index 000000000..515576f0e
--- /dev/null
+++ b/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/ContractCode.tsx
@@ -0,0 +1,66 @@
+import {
+ Badge,
+ Button,
+ Flex,
+ Heading,
+ Stack,
+ useDisclosure,
+} from "@chakra-ui/react";
+import { FullEditor } from "lib/components/editor/FullEditor";
+import { TextReadOnly } from "lib/components/json/TextReadOnly";
+import { EvmVerifyInfoSourceFile } from "lib/services/types";
+
+interface ContractCodeProps {
+ sourceFiles: EvmVerifyInfoSourceFile[];
+ contractPath: string;
+ constructorArguments: string;
+}
+
+export const ContractCode = ({
+ sourceFiles,
+ contractPath,
+ constructorArguments,
+}: ContractCodeProps) => {
+ const { isOpen, onClose, onOpen } = useDisclosure();
+
+ return (
+
+
+
+
+
+ Contract source code
+
+ {sourceFiles.length}
+
+
+
+ ({
+ path: file.sourcePath,
+ code: file.evmSourceFile.content,
+ }))}
+ initialFilePath={contractPath}
+ isOpen={isOpen}
+ onClose={onClose}
+ />
+
+
+
+ Constructor Arguments
+
+
+
+
+ );
+};
diff --git a/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/index.tsx b/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/index.tsx
index 342f51ccb..e3ed468b7 100644
--- a/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/index.tsx
+++ b/src/lib/pages/evm-contract-details/components/evm-contract-details-contract/index.tsx
@@ -8,6 +8,7 @@ import { EvmContractDetailsContractTabs } from "../../types";
import { ContractAbi } from "./ContractAbi";
import { ContractByteCode, ContractByteCodeProps } from "./ContractByteCode";
import { ContractCompiler } from "./ContractCompiler";
+import { ContractCode } from "./ContractCode";
interface EvmContractDetailsContractProps extends ContractByteCodeProps {
contractAddress: HexAddr20;
@@ -39,6 +40,13 @@ export const EvmContractDetailsContract = ({
currentTab={currentTab}
/>
+ {currentTab === EvmContractDetailsContractTabs.Code && (
+
+ )}
{currentTab === EvmContractDetailsContractTabs.Compiler && (
)}
diff --git a/src/lib/pages/evm-contract-details/index.tsx b/src/lib/pages/evm-contract-details/index.tsx
index 8f63657de..01dc85961 100644
--- a/src/lib/pages/evm-contract-details/index.tsx
+++ b/src/lib/pages/evm-contract-details/index.tsx
@@ -176,8 +176,8 @@ const EvmContractDetailsBody = ({
diff --git a/src/lib/pages/interact/index.tsx b/src/lib/pages/interact/index.tsx
index a091b6467..05aac4800 100644
--- a/src/lib/pages/interact/index.tsx
+++ b/src/lib/pages/interact/index.tsx
@@ -155,7 +155,7 @@ const InteractBody = ({
(selectedModuleInput: IndexedModule, fn?: ExposedFunction) => {
setModule(selectedModuleInput);
setSelectedFn(fn);
- handleSetSelectedType(fn?.is_view ?? true ? "view" : "execute");
+ handleSetSelectedType((fn?.is_view ?? true) ? "view" : "execute");
navigate({
pathname: "/interact",
diff --git a/src/lib/services/types/verification/evm.ts b/src/lib/services/types/verification/evm.ts
index 8e70f27c5..f9da5a816 100644
--- a/src/lib/services/types/verification/evm.ts
+++ b/src/lib/services/types/verification/evm.ts
@@ -45,6 +45,7 @@ const zEvmVerifyInfoSourceFile = z
}),
})
.transform(snakeToCamel);
+export type EvmVerifyInfoSourceFile = z.infer;
export const zEvmVerifyInfo = z
.object({
diff --git a/src/lib/styles/globals.css b/src/lib/styles/globals.css
index d0f97a9ce..20023d047 100644
--- a/src/lib/styles/globals.css
+++ b/src/lib/styles/globals.css
@@ -27,3 +27,8 @@ div[data-floating-ui-portal] {
visibility: hidden;
background-color: unset;
}
+
+/* For update on window shrink down */
+.monaco-editor {
+ position: absolute !important;
+}