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; +}