diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b541b27e..47923e856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- [#1093](https://github.com/alleslabs/celatone-frontend/pull/1093) My past module verifications with functionalities - [#1085](https://github.com/alleslabs/celatone-frontend/pull/1085) Add my module verification details - [#1087](https://github.com/alleslabs/celatone-frontend/pull/1087) Add modules verify page - [#1085](https://github.com/alleslabs/celatone-frontend/pull/1085) Add verify module store diff --git a/src/config/project/index.ts b/src/config/project/index.ts index c4dcca27f..ed4fa6af3 100644 --- a/src/config/project/index.ts +++ b/src/config/project/index.ts @@ -10,4 +10,5 @@ export const PROJECT_CONSTANTS: ProjectConstants = { maxContractNameLength: 50, maxContractDescriptionLength: 250, maxListNameLength: 50, + maxMoveVerifyTaskRequestNoteLength: 50, }; diff --git a/src/config/project/types.ts b/src/config/project/types.ts index e0960077e..27364cc4e 100644 --- a/src/config/project/types.ts +++ b/src/config/project/types.ts @@ -10,4 +10,7 @@ export interface ProjectConstants { maxContractNameLength: number; maxContractDescriptionLength: number; maxCodeNameLength: number; + + // move verify task + maxMoveVerifyTaskRequestNoteLength: number; } diff --git a/src/lib/components/ExplorerLink.tsx b/src/lib/components/ExplorerLink.tsx index 2a455a521..011ddf551 100644 --- a/src/lib/components/ExplorerLink.tsx +++ b/src/lib/components/ExplorerLink.tsx @@ -21,7 +21,8 @@ export type LinkType = | "code_id" | "block_height" | "proposal_id" - | "pool_id"; + | "pool_id" + | "task_id"; interface ExplorerLinkProps extends FlexProps { value: string; @@ -75,6 +76,9 @@ export const getNavigationUrl = ({ case "pool_id": url = "/pools"; break; + case "task_id": + url = "/my-module-verifications"; + break; case "invalid_address": return ""; default: diff --git a/src/lib/components/state/EmptyState.tsx b/src/lib/components/state/EmptyState.tsx index 7c7f97715..d23d21392 100644 --- a/src/lib/components/state/EmptyState.tsx +++ b/src/lib/components/state/EmptyState.tsx @@ -15,6 +15,7 @@ export interface EmptyStateProps { alignItems?: FlexProps["alignItems"]; textVariant?: TextProps["variant"]; hasBorderTop?: boolean; + children?: React.ReactNode; } export const EmptyState = ({ @@ -28,6 +29,7 @@ export const EmptyState = ({ alignItems = "center", textVariant = "body1", hasBorderTop = true, + children, }: EmptyStateProps) => ( {message} + {children} ); diff --git a/src/lib/pages/modules-verify/index.tsx b/src/lib/pages/modules-verify/index.tsx index 5de7f65d7..dc3eb8222 100644 --- a/src/lib/pages/modules-verify/index.tsx +++ b/src/lib/pages/modules-verify/index.tsx @@ -17,7 +17,7 @@ import { FooterCta } from "lib/components/layouts"; import { NoMobile } from "lib/components/modal"; import PageContainer from "lib/components/PageContainer"; import { CelatoneSeo } from "lib/components/Seo"; -import { useVerifyModuleTaskStore } from "lib/providers/store"; +import { useMoveVerifyTaskStore } from "lib/providers/store"; import { useSubmitMoveVerify } from "lib/services/verification/move"; import { @@ -37,7 +37,7 @@ export const ModulesVerify = observer(() => { const { currentChainId } = useCelatoneApp(); const { mutateAsync, isError, isLoading } = useSubmitMoveVerify(); const { isOpen, onOpen, onClose } = useDisclosure(); - const { addVerifyModuleTask } = useVerifyModuleTaskStore(); + const { addMoveVerifyTask } = useMoveVerifyTaskStore(); const { control, watch, handleSubmit, setValue } = useForm({ mode: "all", @@ -68,7 +68,7 @@ export const ModulesVerify = observer(() => { if (!data) return; setValue("taskId", data.id); - addVerifyModuleTask({ + addMoveVerifyTask({ taskId: data.id, chainId: currentChainId, requestNote, diff --git a/src/lib/pages/my-module-verification-details/components/MyModuleVerificationDetailsStatusBadge.tsx b/src/lib/pages/my-module-verification-details/components/MyModuleVerificationDetailsStatusBadge.tsx index 181755868..5032cb42d 100644 --- a/src/lib/pages/my-module-verification-details/components/MyModuleVerificationDetailsStatusBadge.tsx +++ b/src/lib/pages/my-module-verification-details/components/MyModuleVerificationDetailsStatusBadge.tsx @@ -1,14 +1,19 @@ import { Tag, TagLabel } from "@chakra-ui/react"; import { ActiveDot } from "lib/components/ActiveDot"; +import { CustomIcon } from "lib/components/icon"; import { MoveVerifyTaskStatus } from "lib/services/types"; interface MyModuleVerificationDetailsStatusBadgeProps { status: MoveVerifyTaskStatus; + hasCloseBtn?: boolean; + isActiveOnVerifying?: boolean; } export const MyModuleVerificationDetailsStatusBadge = ({ status, + hasCloseBtn, + isActiveOnVerifying = true, }: MyModuleVerificationDetailsStatusBadgeProps) => { const renderStatus = () => { if (status === MoveVerifyTaskStatus.Pending) @@ -37,15 +42,16 @@ export const MyModuleVerificationDetailsStatusBadge = ({ return ( <> - + Verifying ); }; return ( - + {renderStatus()} + {hasCloseBtn && } ); }; diff --git a/src/lib/pages/my-module-verification-details/index.tsx b/src/lib/pages/my-module-verification-details/index.tsx index dc7440e84..5c377763a 100644 --- a/src/lib/pages/my-module-verification-details/index.tsx +++ b/src/lib/pages/my-module-verification-details/index.tsx @@ -6,7 +6,7 @@ import { Loading } from "lib/components/Loading"; import PageContainer from "lib/components/PageContainer"; import { CelatoneSeo } from "lib/components/Seo"; import { EmptyState } from "lib/components/state"; -import { useVerifyModuleTaskStore } from "lib/providers/store"; +import { useMoveVerifyTaskStore } from "lib/providers/store"; import { useMoveVerifyTaskInfo } from "lib/services/verification/move"; import { @@ -19,9 +19,9 @@ import { import { zMyModuleVerificationDetailsQueryParams } from "./types"; const MyModuleVerificationDetailsBody = ({ taskId }: { taskId: string }) => { - const { data, isLoading, error } = useMoveVerifyTaskInfo(taskId); - const { getVerifyModuleTask } = useVerifyModuleTaskStore(); - const verifyModuleTask = getVerifyModuleTask(taskId); + const { data, isLoading, error } = useMoveVerifyTaskInfo(taskId, !!taskId); + const { getMoveVerifyTask } = useMoveVerifyTaskStore(); + const verifyModuleTask = getMoveVerifyTask(taskId); if (isLoading) return ; if (!data || error || !verifyModuleTask) diff --git a/src/lib/pages/my-module-verifications/components/MoveVerifyTaskStatusFilter.tsx b/src/lib/pages/my-module-verifications/components/MoveVerifyTaskStatusFilter.tsx new file mode 100644 index 000000000..c469f7c86 --- /dev/null +++ b/src/lib/pages/my-module-verifications/components/MoveVerifyTaskStatusFilter.tsx @@ -0,0 +1,138 @@ +import type { InputProps } from "@chakra-ui/react"; +import { Flex, FormControl, useOutsideClick } from "@chakra-ui/react"; +import { matchSorter } from "match-sorter"; +import { forwardRef, useMemo, useRef, useState } from "react"; + +import { + DropdownContainer, + FilterChip, + FilterDropdownItem, + FilterInput, +} from "lib/components/filter"; +import { MyModuleVerificationDetailsStatusBadge } from "lib/pages/my-module-verification-details/components"; +import { MoveVerifyTaskStatus } from "lib/services/types"; +import { toggleItem } from "lib/utils"; + +export interface MoveVerifyTaskStatusFilterProps extends InputProps { + result: MoveVerifyTaskStatus[]; + minW?: string; + label?: string; + placeholder?: string; + setResult: (option: MoveVerifyTaskStatus[]) => void; + isMulti: boolean; +} + +const OPTIONS = [ + { label: "completed", value: MoveVerifyTaskStatus.Finished }, + { label: "failed", value: MoveVerifyTaskStatus.NotFound }, + { label: "pending", value: MoveVerifyTaskStatus.Pending }, + { label: "verifying", value: MoveVerifyTaskStatus.Running }, +]; + +export const MoveVerifyTaskStatusFilter = forwardRef< + HTMLInputElement, + MoveVerifyTaskStatusFilterProps +>( + ( + { + result, + minW = "50%", + setResult, + placeholder, + label, + isMulti, + }: MoveVerifyTaskStatusFilterProps, + ref + ) => { + const [keyword, setKeyword] = useState(""); + const [isDropdown, setIsDropdown] = useState(false); + const inputRef = useRef(null); + const boxRef = useRef(null); + + const dropdownValue = useMemo( + () => + keyword + ? matchSorter(OPTIONS, keyword, { + keys: ["label"], + threshold: matchSorter.rankings.CONTAINS, + }) + : OPTIONS, + [keyword] + ); + + const isOptionSelected = (option: MoveVerifyTaskStatus) => + result.some((selectedOption) => selectedOption === option); + + const selectOption = (option: MoveVerifyTaskStatus) => { + setKeyword(""); + + if (!isMulti) { + setIsDropdown(false); + + if (result[0] === option) setResult([]); + else setResult([option]); + } else { + setResult(toggleItem(result, option)); + } + }; + + useOutsideClick({ + ref: boxRef, + handler: () => setIsDropdown(false), + }); + + return ( + + + {result.map((option) => ( + + } + onSelect={() => setResult(toggleItem(result, option))} + /> + ))} + + } + /> + + {isDropdown && ( + + {!dropdownValue.length && No filter matched} + + {/* option selection section */} + {dropdownValue.map((option) => ( + + } + isOptionSelected={isOptionSelected(option.value)} + onSelect={() => selectOption(option.value)} + /> + ))} + + )} + + ); + } +); diff --git a/src/lib/pages/my-module-verifications/components/my-module-verifications-table/FileNamesCell.tsx b/src/lib/pages/my-module-verifications/components/my-module-verifications-table/FileNamesCell.tsx new file mode 100644 index 000000000..93d89e0a7 --- /dev/null +++ b/src/lib/pages/my-module-verifications/components/my-module-verifications-table/FileNamesCell.tsx @@ -0,0 +1,39 @@ +import { Flex, Text } from "@chakra-ui/react"; +import { useMemo, useState } from "react"; + +import type { MoveVerifyTaskInfo } from "../../data"; + +interface FileNamesCellProps { + task: MoveVerifyTaskInfo; +} + +export const FileNamesCell = ({ task }: FileNamesCellProps) => { + const [isHoverText, setIsHoverText] = useState(false); + + const formattedText = useMemo(() => { + const files = Object.keys(task.fileMap) + .filter((file) => !file.includes(".toml")) + .map((file) => file.slice(0, -5)); // remove ".move" extension + + if (isHoverText) return files.join(", "); + + const firstPart = files.slice(0, 3).join(", "); + const remaining = files.length - 3; + + // eslint-disable-next-line sonarjs/no-nested-template-literals + return `${firstPart}${remaining > 0 ? `, +${remaining}` : ""}`; + }, [isHoverText, task.fileMap]); + + return ( + setIsHoverText(true)} + onMouseOut={() => setIsHoverText(false)} + flexWrap="wrap" + wordBreak="break-word" + > + {formattedText} + + ); +}; diff --git a/src/lib/pages/my-module-verifications/components/my-module-verifications-table/MyModuleVerificationsHeader.tsx b/src/lib/pages/my-module-verifications/components/my-module-verifications-table/MyModuleVerificationsHeader.tsx new file mode 100644 index 000000000..6b3452de2 --- /dev/null +++ b/src/lib/pages/my-module-verifications/components/my-module-verifications-table/MyModuleVerificationsHeader.tsx @@ -0,0 +1,19 @@ +import type { GridProps } from "@chakra-ui/react"; +import { Grid } from "@chakra-ui/react"; + +import { TableHeader } from "lib/components/table"; + +interface ModulesTableHeaderProps { + templateColumns: GridProps["templateColumns"]; +} +export const MyModuleVerificationsTableHeader = ({ + templateColumns, +}: ModulesTableHeaderProps) => ( + + Request ID + Request Note + Files + Status + Verified at + +); diff --git a/src/lib/pages/my-module-verifications/components/my-module-verifications-table/MyModuleVerificationsRow.tsx b/src/lib/pages/my-module-verifications/components/my-module-verifications-table/MyModuleVerificationsRow.tsx new file mode 100644 index 000000000..4e5b2ad8a --- /dev/null +++ b/src/lib/pages/my-module-verifications/components/my-module-verifications-table/MyModuleVerificationsRow.tsx @@ -0,0 +1,66 @@ +import { Flex, Grid, Text } from "@chakra-ui/react"; +import type { GridProps } from "@chakra-ui/react"; + +import type { MoveVerifyTaskInfo } from "../../data"; +import { useInternalNavigate } from "lib/app-provider"; +import { ExplorerLink } from "lib/components/ExplorerLink"; +import { TableRow } from "lib/components/table"; +import { MyModuleVerificationDetailsStatusBadge } from "lib/pages/my-module-verification-details/components"; +import { dateFromNow, formatUTC } from "lib/utils"; + +import { FileNamesCell } from "./FileNamesCell"; +import { RequestNoteCell } from "./RequestNoteCell"; + +interface MyModuleVerificationsRowProps { + templateColumns: GridProps["templateColumns"]; + task: MoveVerifyTaskInfo; +} + +export const MyModuleVerificationsRow = ({ + templateColumns, + task, +}: MyModuleVerificationsRowProps) => { + const navigate = useInternalNavigate(); + + return ( + + navigate({ + pathname: "/my-module-verifications/[taskId]", + query: { taskId: task.taskId }, + }) + } + _hover={{ bg: "gray.900" }} + transition="all 0.25s ease-in-out" + cursor="pointer" + > + + + + + + + + + + + + + + {task.verifiedAt ? ( + + + {formatUTC(task.verifiedAt)} + + + ({dateFromNow(task.verifiedAt)}) + + + ) : ( + - + )} + + + ); +}; diff --git a/src/lib/pages/my-module-verifications/components/my-module-verifications-table/RequestNoteCell.tsx b/src/lib/pages/my-module-verifications/components/my-module-verifications-table/RequestNoteCell.tsx new file mode 100644 index 000000000..21cb7527d --- /dev/null +++ b/src/lib/pages/my-module-verifications/components/my-module-verifications-table/RequestNoteCell.tsx @@ -0,0 +1,30 @@ +import { observer } from "mobx-react-lite"; + +import type { MoveVerifyTaskInfo } from "../../data"; +import { useCelatoneApp } from "lib/app-provider"; +import { EditableCell } from "lib/components/table"; +import { useMoveVerifyTaskStore } from "lib/providers/store"; + +interface RequestNoteProps { + moveVerifyTask: MoveVerifyTaskInfo; +} + +export const RequestNoteCell = observer( + ({ moveVerifyTask }: RequestNoteProps) => { + const { constants } = useCelatoneApp(); + const { updateRequestNote } = useMoveVerifyTaskStore(); + + return ( + 0 + ? moveVerifyTask.requestNote + : "-" + } + maxLength={constants.maxMoveVerifyTaskRequestNoteLength} + onSave={(value) => updateRequestNote(moveVerifyTask.taskId, value)} + /> + ); + } +); diff --git a/src/lib/pages/my-module-verifications/components/my-module-verifications-table/index.tsx b/src/lib/pages/my-module-verifications/components/my-module-verifications-table/index.tsx new file mode 100644 index 000000000..f302c6325 --- /dev/null +++ b/src/lib/pages/my-module-verifications/components/my-module-verifications-table/index.tsx @@ -0,0 +1,98 @@ +/* eslint-disable react/button-has-type */ +import { Box, Button, Grid, Stack } from "@chakra-ui/react"; +import { observer } from "mobx-react-lite"; +import { useMemo, useState } from "react"; + +import { useMyModuleVerifications } from "../../data"; +import { MoveVerifyTaskStatusFilter } from "../MoveVerifyTaskStatusFilter"; +import { useInternalNavigate } from "lib/app-provider"; +import { CustomIcon } from "lib/components/icon"; +import InputWithIcon from "lib/components/InputWithIcon"; +import { Loading } from "lib/components/Loading"; +import { EmptyState } from "lib/components/state"; +import { TableContainer } from "lib/components/table"; +import type { MoveVerifyTaskStatus } from "lib/services/types"; + +import { MyModuleVerificationsTableHeader } from "./MyModuleVerificationsHeader"; +import { MyModuleVerificationsRow } from "./MyModuleVerificationsRow"; + +export const MyModuleVerificationsTable = observer(() => { + const navigate = useInternalNavigate(); + const [keyword, setKeyword] = useState(""); + const [statuses, setStatuses] = useState([]); + const { data, isLoading } = useMyModuleVerifications(); + + const isFiltering = keyword.length > 0 || statuses.length > 0; + + const templateColumns = "repeat(3, 1fr) 200px 1.2fr"; + + const filteredTasks = useMemo( + () => + data + ?.filter((task) => + task.taskId?.toLowerCase().includes(keyword.toLowerCase()) + ) + .filter((task) => { + if (statuses.length === 0) return true; + return statuses.includes(task.status); + }), + [data, keyword, statuses] + ); + + if (isLoading) return ; + + return ( + + + setKeyword(e.target.value)} + amptrackSection="my-published-modules-search" + size="lg" + /> + + + + {filteredTasks.length === 0 ? ( + + + + + + ) : ( + + + {filteredTasks.map((task) => ( + + ))} + + )} + + ); +}); diff --git a/src/lib/pages/my-module-verifications/data.ts b/src/lib/pages/my-module-verifications/data.ts new file mode 100644 index 000000000..1be1eb585 --- /dev/null +++ b/src/lib/pages/my-module-verifications/data.ts @@ -0,0 +1,55 @@ +import { useMoveVerifyTaskStore } from "lib/providers/store"; +import { MoveVerifyTaskStatus } from "lib/services/types"; +import { useMoveVerifyTaskInfos } from "lib/services/verification/move"; +import type { MoveVerifyTaskLocalInfo } from "lib/stores/verify-module"; + +export type MoveVerifyTaskInfo = MoveVerifyTaskLocalInfo & { + status: MoveVerifyTaskStatus; +}; + +export const useMyModuleVerifications = (): { + isLoading: boolean; + data: MoveVerifyTaskInfo[]; +} => { + const { latestMoveVerifyTasks, completeMoveVerifyTask } = + useMoveVerifyTaskStore(); + const localTasks = latestMoveVerifyTasks(); + const verificationInfos = useMoveVerifyTaskInfos( + localTasks + .filter(({ completed }) => !completed) + .map((module) => module.taskId), + ({ task, result }) => { + if ( + task.status === MoveVerifyTaskStatus.Finished || + task.status === MoveVerifyTaskStatus.NotFound + ) { + completeMoveVerifyTask(task.id, result?.verifiedAt); + } + } + ); + + return { + isLoading: verificationInfos.some( + (verificationInfo) => verificationInfo.isLoading + ), + data: localTasks.map((task) => { + if (task.completed) { + return { + ...task, + status: task.verifiedAt + ? MoveVerifyTaskStatus.Finished + : MoveVerifyTaskStatus.NotFound, + }; + } + + const fetchedTaskInfo = verificationInfos + ?.map(({ data }) => data) + ?.find((verificationInfo) => verificationInfo?.task.id === task.taskId); + + return { + ...task, + status: fetchedTaskInfo?.task.status ?? MoveVerifyTaskStatus.NotFound, + }; + }), + }; +}; diff --git a/src/lib/pages/my-module-verifications/index.tsx b/src/lib/pages/my-module-verifications/index.tsx index 3da35d265..8b808306b 100644 --- a/src/lib/pages/my-module-verifications/index.tsx +++ b/src/lib/pages/my-module-verifications/index.tsx @@ -1,3 +1,45 @@ +import { Box, Button, Flex, Heading, Text } from "@chakra-ui/react"; + +import { useInternalNavigate, useMoveConfig } from "lib/app-provider"; +import { CustomIcon } from "lib/components/icon"; +import PageContainer from "lib/components/PageContainer"; +import { CelatoneSeo } from "lib/components/Seo"; + +import { MyModuleVerificationsTable } from "./components/my-module-verifications-table"; + export const MyModuleVerifications = () => { - return
My Module Verifications
; + const navigate = useInternalNavigate(); + useMoveConfig({ shouldRedirect: true }); + + return ( + + + + + + My Past Verification + + + Display the request queue for module verifications through + InitiaScan + + + + {/* */} + + + + + + ); }; diff --git a/src/lib/providers/network-guard/index.tsx b/src/lib/providers/network-guard/index.tsx index e2f09f79a..062ee9dd9 100644 --- a/src/lib/providers/network-guard/index.tsx +++ b/src/lib/providers/network-guard/index.tsx @@ -9,8 +9,8 @@ import { useAccountStore, useCodeStore, useContractStore, + useMoveVerifyTaskStore, usePublicProjectStore, - useVerifyModuleTaskStore, } from "lib/providers/store"; import { formatUserKey } from "lib/utils"; @@ -31,8 +31,8 @@ export const NetworkGuard = observer(({ children }: NetworkGuardProps) => { const { setCodeUserKey, isCodeUserKeyExist } = useCodeStore(); const { setContractUserKey, isContractUserKeyExist } = useContractStore(); const { setProjectUserKey, isProjectUserKeyExist } = usePublicProjectStore(); - const { setVerifyModuleTaskUserKey, isVerifyModuleTaskUserKeyExist } = - useVerifyModuleTaskStore(); + const { setMoveVerifyTaskUserKey, isMoveVerifyTaskUserKeyExist } = + useMoveVerifyTaskStore(); useEffect(() => { if (isHydrated) { @@ -41,7 +41,7 @@ export const NetworkGuard = observer(({ children }: NetworkGuardProps) => { setCodeUserKey(userKey); setContractUserKey(userKey); setProjectUserKey(userKey); - setVerifyModuleTaskUserKey(userKey); + setMoveVerifyTaskUserKey(userKey); } }, [ isHydrated, @@ -50,7 +50,7 @@ export const NetworkGuard = observer(({ children }: NetworkGuardProps) => { setCodeUserKey, setContractUserKey, setProjectUserKey, - setVerifyModuleTaskUserKey, + setMoveVerifyTaskUserKey, ]); if (isHydrated && !(currentChainId in chainConfigs)) @@ -62,7 +62,7 @@ export const NetworkGuard = observer(({ children }: NetworkGuardProps) => { !isCodeUserKeyExist() || !isContractUserKeyExist() || !isProjectUserKeyExist() || - !isVerifyModuleTaskUserKeyExist() + !isMoveVerifyTaskUserKeyExist() ) return ; diff --git a/src/lib/providers/store.tsx b/src/lib/providers/store.tsx index b918f8651..5547bd7a0 100644 --- a/src/lib/providers/store.tsx +++ b/src/lib/providers/store.tsx @@ -70,7 +70,7 @@ export function useLocalChainConfigStore() { return localChainConfigStore; } -export function useVerifyModuleTaskStore() { - const { verifyModuleTaskStore } = useStore(); - return verifyModuleTaskStore; +export function useMoveVerifyTaskStore() { + const { moveVerifyTaskStore } = useStore(); + return moveVerifyTaskStore; } diff --git a/src/lib/services/chain-config/index.ts b/src/lib/services/chain-config/index.ts index 2e29f7429..e064c4736 100644 --- a/src/lib/services/chain-config/index.ts +++ b/src/lib/services/chain-config/index.ts @@ -11,5 +11,7 @@ export const useApiChainConfigs = (chainIds: string[]) => { retry: 1, refetchOnWindowFocus: false, + refetchOnMount: false, + staleTime: Infinity, } ); diff --git a/src/lib/services/verification/move/index.ts b/src/lib/services/verification/move/index.ts index 5b3d62ae1..fe537e80b 100644 --- a/src/lib/services/verification/move/index.ts +++ b/src/lib/services/verification/move/index.ts @@ -1,4 +1,4 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQueries, useQuery } from "@tanstack/react-query"; import type { UseQueryResult } from "@tanstack/react-query"; import { CELATONE_QUERY_KEYS, useCelatoneApp } from "lib/app-provider"; @@ -21,6 +21,32 @@ export const useSubmitMoveVerify = () => mutationFn: submitMoveVerify, }); +export const useMoveVerifyTaskInfos = ( + taskIds: string[], + onSuccess?: (data: MoveVerifyByTaskIdResponse) => void +) => { + const { chainConfig } = useCelatoneApp(); + const { + extra: { layer }, + } = chainConfig; + + return useQueries({ + queries: taskIds.map((taskId) => ({ + queryKey: [ + CELATONE_QUERY_KEYS.MOVE_VERIFY_TASK_BY_TASK_ID, + taskId, + layer, + ], + queryFn: () => getMoveVerifyByTaskId(taskId), + enabled: layer === "1", + retry: 0, + refetchOnWindowFocus: false, + keepPreviousData: true, + onSuccess, + })), + }); +}; + export const useMoveVerifyTaskInfo = ( taskId: string, enabled = true diff --git a/src/lib/stores/root.ts b/src/lib/stores/root.ts index e42b4032f..941ea2e03 100644 --- a/src/lib/stores/root.ts +++ b/src/lib/stores/root.ts @@ -5,7 +5,7 @@ import { ContractStore } from "./contract"; import { NetworkStore } from "./networks"; import { PublicProjectStore } from "./project"; import { SchemaStore } from "./schema"; -import { VerifyModuleTaskStore } from "./verify-module"; +import { MoveVerifyTaskStore } from "./verify-module"; export class RootStore { accountStore: AccountStore; @@ -22,7 +22,7 @@ export class RootStore { localChainConfigStore: LocalChainConfigStore; - verifyModuleTaskStore: VerifyModuleTaskStore; + moveVerifyTaskStore: MoveVerifyTaskStore; constructor() { this.accountStore = new AccountStore(); @@ -32,6 +32,6 @@ export class RootStore { this.schemaStore = new SchemaStore(); this.networkStore = new NetworkStore(); this.localChainConfigStore = new LocalChainConfigStore(); - this.verifyModuleTaskStore = new VerifyModuleTaskStore(); + this.moveVerifyTaskStore = new MoveVerifyTaskStore(); } } diff --git a/src/lib/stores/verify-module.test.ts b/src/lib/stores/verify-module.test.ts index fa2c3d076..946361dea 100644 --- a/src/lib/stores/verify-module.test.ts +++ b/src/lib/stores/verify-module.test.ts @@ -1,66 +1,103 @@ -import { VerifyModuleTaskStore } from "./verify-module"; +import { MoveVerifyTaskStore } from "./verify-module"; -let verifyModuleTaskStore: VerifyModuleTaskStore; +let moveVerifyTaskStore: MoveVerifyTaskStore; beforeAll(() => { - verifyModuleTaskStore = new VerifyModuleTaskStore(); + moveVerifyTaskStore = new MoveVerifyTaskStore(); }); -describe("VerifyModuleTaskStore initialization", () => { - test("Correctly initialize VerifyModuleTaskStore", () => { - expect(verifyModuleTaskStore instanceof VerifyModuleTaskStore).toBeTruthy(); +describe("MoveVerifyTaskStore initialization", () => { + test("Correctly initialize MoveVerifyTaskStore", () => { + expect(moveVerifyTaskStore instanceof MoveVerifyTaskStore).toBeTruthy(); }); }); -describe("isVerifyModuleTaskUserKeyExist", () => { +describe("isMoveVerifyTaskUserKeyExist", () => { test("correctly check if user key exist", () => { - expect(verifyModuleTaskStore.isVerifyModuleTaskUserKeyExist()).toBeFalsy(); - verifyModuleTaskStore.setVerifyModuleTaskUserKey("userKey"); - expect(verifyModuleTaskStore.isVerifyModuleTaskUserKeyExist()).toBeTruthy(); + expect(moveVerifyTaskStore.isMoveVerifyTaskUserKeyExist()).toBeFalsy(); + moveVerifyTaskStore.setMoveVerifyTaskUserKey("userKey"); + expect(moveVerifyTaskStore.isMoveVerifyTaskUserKeyExist()).toBeTruthy(); }); }); -describe("verifyModule", () => { +describe("verifyModuleTask", () => { const verifyModule = { taskId: "taskId", - fileMap: { file: "map" }, + fileMap: { "coin.move": "sources/coin.move", "Move.toml": "Move.toml" }, chainId: "chainId", }; - test("correctly get verify modules", () => { - expect(verifyModuleTaskStore.getVerifyModuleTasks()).toEqual([]); + test("correctly get verify module tasks", () => { + expect(moveVerifyTaskStore.latestMoveVerifyTasks()).toEqual([]); }); - test("correctly get verify modules after adding", () => { - verifyModuleTaskStore.addVerifyModuleTask(verifyModule); - expect(verifyModuleTaskStore.getVerifyModuleTasks()).toEqual([ - verifyModule, + test("correctly get verify module tasks after adding new task", () => { + moveVerifyTaskStore.addMoveVerifyTask(verifyModule); + expect( + moveVerifyTaskStore.latestMoveVerifyTasks().map((task) => ({ + taskId: task.taskId, + fileMap: task.fileMap, + chainId: task.chainId, + completed: task.completed, + })) + ).toEqual([ + { + taskId: verifyModule.taskId, + fileMap: verifyModule.fileMap, + chainId: verifyModule.chainId, + completed: false, + }, ]); }); - test("correctly get verify modules after adding multiple", () => { + test("correctly get verify module tasks after adding multiple", () => { const verifyModule1 = { taskId: "taskId1", - fileMap: { file: "map" }, + fileMap: { + "common.move": "sources/common.move", + "Move.toml": "Move.toml", + }, chainId: "chainId", }; const verifyModule2 = { taskId: "taskId2", - fileMap: { file: "map" }, + fileMap: { + "simple.move": "sources/simple.move", + "Move.toml": "Move.toml", + }, chainId: "chainId", }; - verifyModuleTaskStore.addVerifyModuleTask(verifyModule1); - verifyModuleTaskStore.addVerifyModuleTask(verifyModule2); - expect(verifyModuleTaskStore.getVerifyModuleTasks()).toEqual([ - verifyModule2, - verifyModule1, - verifyModule, - ]); + moveVerifyTaskStore.addMoveVerifyTask(verifyModule1); + moveVerifyTaskStore.addMoveVerifyTask(verifyModule2); + expect(moveVerifyTaskStore.latestMoveVerifyTasks().length).toBe(3); + + const actualVerifyModuleTask1 = moveVerifyTaskStore.getMoveVerifyTask( + verifyModule1.taskId + ); + const actualVerifyModuleTask2 = moveVerifyTaskStore.getMoveVerifyTask( + verifyModule2.taskId + ); + + expect(actualVerifyModuleTask1?.taskId).toBe(verifyModule1.taskId); + expect(actualVerifyModuleTask1?.fileMap).toEqual(verifyModule1.fileMap); + expect(actualVerifyModuleTask1?.chainId).toBe(verifyModule1.chainId); + expect(actualVerifyModuleTask1?.completed).toBeFalsy(); + + expect(actualVerifyModuleTask2?.taskId).toBe(verifyModule2.taskId); + expect(actualVerifyModuleTask2?.fileMap).toEqual(verifyModule2.fileMap); + expect(actualVerifyModuleTask2?.chainId).toBe(verifyModule2.chainId); + expect(actualVerifyModuleTask2?.completed).toBeFalsy(); }); - test("correctly check if module is verified", () => { + test("correctly check if verify module tasks is added", () => { expect( - verifyModuleTaskStore.isVerifyModuleTaskExist(verifyModule.taskId) + moveVerifyTaskStore.isMoveVerifyTaskExist(verifyModule.taskId) ).toBeTruthy(); - expect( - verifyModuleTaskStore.isVerifyModuleTaskExist("randomId") - ).toBeFalsy(); + expect(moveVerifyTaskStore.isMoveVerifyTaskExist("randomId")).toBeFalsy(); + }); + test("update verify module task", () => { + const verifiedAt = new Date(); + moveVerifyTaskStore.completeMoveVerifyTask(verifyModule.taskId, verifiedAt); + + const actual = moveVerifyTaskStore.getMoveVerifyTask(verifyModule.taskId); + expect(actual?.completed).toBeTruthy(); + expect(actual?.verifiedAt).toEqual(verifiedAt); }); }); diff --git a/src/lib/stores/verify-module.ts b/src/lib/stores/verify-module.ts index 815eb430e..1c0279572 100644 --- a/src/lib/stores/verify-module.ts +++ b/src/lib/stores/verify-module.ts @@ -3,17 +3,20 @@ import { isHydrated, makePersistable } from "mobx-persist-store"; import type { Dict } from "lib/types"; -export interface VerifyModuleLocalInfo { +export interface MoveVerifyTaskLocalInfo { taskId: string; requestNote?: string; chainId: string; fileMap: Record; + created: Date; + verifiedAt?: Date; + completed: boolean; } -export class VerifyModuleTaskStore { +export class MoveVerifyTaskStore { private userKey: string; - modules: Dict; + modules: Dict; constructor() { this.userKey = ""; @@ -22,7 +25,7 @@ export class VerifyModuleTaskStore { makeAutoObservable(this, {}, { autoBind: true }); makePersistable(this, { - name: "VerifyModuleTaskStore", + name: "MoveVerifyTaskStore", properties: ["modules"], }); } @@ -31,37 +34,61 @@ export class VerifyModuleTaskStore { return isHydrated(this); } - isVerifyModuleTaskUserKeyExist(): boolean { + isMoveVerifyTaskUserKeyExist(): boolean { return !!this.userKey; } - setVerifyModuleTaskUserKey(userKey: string) { + setMoveVerifyTaskUserKey(userKey: string) { this.userKey = userKey; } - isVerifyModuleTaskExist(taskId: string): boolean { + isMoveVerifyTaskExist(taskId: string): boolean { return ( - this.getVerifyModuleTasks().findIndex((item) => item.taskId === taskId) > - -1 + this.getMoveVerifyTasks().findIndex((item) => item.taskId === taskId) > -1 ); } - getVerifyModuleTasks(): VerifyModuleLocalInfo[] { - return this.modules[this.userKey]?.reverse() ?? []; + getMoveVerifyTasks(): MoveVerifyTaskLocalInfo[] { + return this.modules[this.userKey] ?? []; } - getVerifyModuleTask(taskId: string): VerifyModuleLocalInfo | undefined { - return this.getVerifyModuleTasks().find( - (module) => module.taskId === taskId - ); + latestMoveVerifyTasks(): MoveVerifyTaskLocalInfo[] { + return this.getMoveVerifyTasks().slice().reverse(); + } + + getMoveVerifyTask(taskId: string): MoveVerifyTaskLocalInfo | undefined { + return this.getMoveVerifyTasks().find((module) => module.taskId === taskId); } - addVerifyModuleTask(verifyModule: VerifyModuleLocalInfo): void { - if (!this.isVerifyModuleTaskExist(verifyModule.taskId)) { + addMoveVerifyTask( + verifyModule: Omit< + MoveVerifyTaskLocalInfo, + "created" | "completed" | "verifiedAt" + > + ): void { + if (!this.isMoveVerifyTaskExist(verifyModule.taskId)) { this.modules[this.userKey] = [ - ...this.getVerifyModuleTasks(), - verifyModule, + ...this.getMoveVerifyTasks(), + { ...verifyModule, created: new Date(), completed: false }, ]; } } + + completeMoveVerifyTask(taskId: string, verifiedAt?: Date): void { + const modules = this.getMoveVerifyTasks().map((module) => + module.taskId === taskId + ? { ...module, verifiedAt, completed: true } + : module + ); + this.modules[this.userKey] = modules; + } + + updateRequestNote(taskId: string, newRequestNote?: string): void { + const modules = this.getMoveVerifyTasks().map((module) => + module.taskId === taskId + ? { ...module, requestNote: newRequestNote } + : module + ); + this.modules[this.userKey] = modules; + } }