diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f8b4500..a97ea0fed 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 +- [#472](https://github.com/alleslabs/celatone-frontend/pull/472) Add json schema functionality to migrate contract - [#461](https://github.com/alleslabs/celatone-frontend/pull/461) Add json schema form - [#455](https://github.com/alleslabs/celatone-frontend/pull/455) Implement schema store and unit test - [#453](https://github.com/alleslabs/celatone-frontend/pull/453) Attach schema feature on upload complete diff --git a/src/lib/components/MotionBox.tsx b/src/lib/components/MotionBox.tsx new file mode 100644 index 000000000..1477da9c8 --- /dev/null +++ b/src/lib/components/MotionBox.tsx @@ -0,0 +1,7 @@ +import { chakra, shouldForwardProp } from "@chakra-ui/react"; +import { motion, isValidMotionProp } from "framer-motion"; + +export const MotionBox = chakra(motion.div, { + shouldForwardProp: (prop) => + isValidMotionProp(prop) || shouldForwardProp(prop), +}); diff --git a/src/lib/components/dropzone/config.ts b/src/lib/components/dropzone/config.ts index 7cada0f96..db33479f8 100644 --- a/src/lib/components/dropzone/config.ts +++ b/src/lib/components/dropzone/config.ts @@ -19,10 +19,10 @@ export const DROPZONE_CONFIG: { [key in DropzoneFileType]: DropzoneConfig } = { }, }, schema: { - accept: { "application/json": [".schema.json"] }, + accept: { "application/json": [".json"] }, text: { prettyFileType: "JSON Schema", - rawFileType: ".schema.json", + rawFileType: ".json", }, }, }; diff --git a/src/lib/components/json-schema/AttachSchemaCard.tsx b/src/lib/components/json-schema/AttachSchemaCard.tsx new file mode 100644 index 000000000..c7f73d776 --- /dev/null +++ b/src/lib/components/json-schema/AttachSchemaCard.tsx @@ -0,0 +1,73 @@ +import { Button, Flex, IconButton, Text } from "@chakra-ui/react"; + +import { CustomIcon } from "lib/components/icon"; +import { useSchemaStore } from "lib/providers/store"; +import type { CodeSchema } from "lib/stores/schema"; +import type { Option } from "lib/types"; + +import { ViewSchemaButton } from "./ViewSchemaButton"; + +interface AttachSchemaCardProps { + attached: boolean; + codeId: string; + codeHash: string; + schema: Option; + openDrawer: () => void; +} + +export const AttachSchemaCard = ({ + attached, + codeId, + codeHash, + schema, + openDrawer, +}: AttachSchemaCardProps) => { + const { deleteSchema } = useSchemaStore(); + + return ( + + {!attached ? ( + <> + Attach JSON Schema + + + ) : ( + <> + + + JSON Schema attached + + + + + deleteSchema(codeHash)} + icon={ + + } + /> + + + )} + + ); +}; diff --git a/src/lib/components/json-schema/JsonSchemaDrawer.tsx b/src/lib/components/json-schema/JsonSchemaDrawer.tsx new file mode 100644 index 000000000..968ef4b75 --- /dev/null +++ b/src/lib/components/json-schema/JsonSchemaDrawer.tsx @@ -0,0 +1,66 @@ +import { + Drawer, + DrawerOverlay, + DrawerContent, + DrawerHeader, + Flex, + Heading, + DrawerCloseButton, + DrawerBody, + Text, + Box, +} from "@chakra-ui/react"; + +import { CustomIcon } from "lib/components/icon"; + +import { UploadTemplate } from "./UploadTemplate"; + +interface JsonSchemaDrawerProps { + codeId: string; + codeHash: string; + isOpen: boolean; + onClose: () => void; +} + +export const JsonSchemaDrawer = ({ + codeId, + codeHash, + isOpen, + onClose, +}: JsonSchemaDrawerProps) => ( + + + + + + + + + Attach JSON Schema for code ID “{codeId}” + + + + Your attached JSON schema will be stored locally on your device + + + + + + + + Please note that the JSON schema you upload on our website will only + be stored locally on your device. For public projects with verified + JSON schemas, they will be visible and accessible to others. + + + + + + +); diff --git a/src/lib/pages/upload/components/UploadTemplate.tsx b/src/lib/components/json-schema/UploadTemplate.tsx similarity index 100% rename from src/lib/pages/upload/components/UploadTemplate.tsx rename to src/lib/components/json-schema/UploadTemplate.tsx diff --git a/src/lib/components/json-schema/ViewSchemaButton.tsx b/src/lib/components/json-schema/ViewSchemaButton.tsx new file mode 100644 index 000000000..b30118b47 --- /dev/null +++ b/src/lib/components/json-schema/ViewSchemaButton.tsx @@ -0,0 +1,43 @@ +import type { ButtonProps } from "@chakra-ui/react"; +import { Button } from "@chakra-ui/react"; + +import type { CodeSchema } from "lib/stores/schema"; +import type { Option } from "lib/types"; + +interface ViewSchemaButtonProps extends ButtonProps { + codeId: string; + schema: Option; +} + +export const ViewSchemaButton = ({ + codeId, + schema, + ...buttonProps +}: ViewSchemaButtonProps) => ( + +); diff --git a/src/lib/components/json-schema/contract_schema.json b/src/lib/components/json-schema/form/contract_schema.json similarity index 100% rename from src/lib/components/json-schema/contract_schema.json rename to src/lib/components/json-schema/form/contract_schema.json diff --git a/src/lib/components/json-schema/fields/MultiSchemaField.tsx b/src/lib/components/json-schema/form/fields/MultiSchemaField.tsx similarity index 100% rename from src/lib/components/json-schema/fields/MultiSchemaField.tsx rename to src/lib/components/json-schema/form/fields/MultiSchemaField.tsx diff --git a/src/lib/components/json-schema/fields/index.ts b/src/lib/components/json-schema/form/fields/index.ts similarity index 100% rename from src/lib/components/json-schema/fields/index.ts rename to src/lib/components/json-schema/form/fields/index.ts diff --git a/src/lib/components/json-schema/index.tsx b/src/lib/components/json-schema/form/index.tsx similarity index 85% rename from src/lib/components/json-schema/index.tsx rename to src/lib/components/json-schema/form/index.tsx index 2cb178e99..dc8f8c39d 100644 --- a/src/lib/components/json-schema/index.tsx +++ b/src/lib/components/json-schema/form/index.tsx @@ -23,7 +23,7 @@ import isEqual from "lodash/isEqual"; import type { FC } from "react"; import { useCallback, useMemo, useState } from "react"; -import JsonReadOnly from "../json/JsonReadOnly"; +import JsonReadOnly from "../../json/JsonReadOnly"; import { jsonPrettify } from "lib/utils"; import contractSchema from "./contract_schema.json"; @@ -102,7 +102,7 @@ export interface JsonSchemaFormProps > { schema: RJSFSchema; formId: string; - onSubmit: (data: Record) => void; + onSubmit?: (data: Record) => void; /** Onchange callback is with BROKEN data */ onChange?: (data: Record) => void; formContext?: Record; @@ -136,7 +136,7 @@ export const JsonSchemaForm: FC = ({ fixOneOfKeysCallback(values); console.log("onSubmit", values); - propsOnSubmit(values); + propsOnSubmit?.(values); }; const onChange = useCallback( @@ -154,44 +154,49 @@ export const JsonSchemaForm: FC = ({ ); return ( -
{ - // log.info(values) - onChange?.(values); - }} - onSubmit={({ formData: values }) => { - // log.info(values) - onSubmit(values); - }} - onError={() => console.error("errors")} - /> + + { + // log.info(values) + onChange?.(values); + }} + onSubmit={({ formData: values }) => { + // log.info(values) + onSubmit(values); + }} + onError={() => console.error("errors")} + /> + ); }; diff --git a/src/lib/components/json-schema/templates/ArrayFieldItemTemplate.tsx b/src/lib/components/json-schema/form/templates/ArrayFieldItemTemplate.tsx similarity index 100% rename from src/lib/components/json-schema/templates/ArrayFieldItemTemplate.tsx rename to src/lib/components/json-schema/form/templates/ArrayFieldItemTemplate.tsx diff --git a/src/lib/components/json-schema/templates/ArrayFieldTemplate.tsx b/src/lib/components/json-schema/form/templates/ArrayFieldTemplate.tsx similarity index 93% rename from src/lib/components/json-schema/templates/ArrayFieldTemplate.tsx rename to src/lib/components/json-schema/form/templates/ArrayFieldTemplate.tsx index f6c7a4054..f42cfeb25 100644 --- a/src/lib/components/json-schema/templates/ArrayFieldTemplate.tsx +++ b/src/lib/components/json-schema/form/templates/ArrayFieldTemplate.tsx @@ -63,7 +63,14 @@ export default function ArrayFieldTemplate( uiSchema={uiSchema} registry={registry} /> - + {items.length > 0 && items.map( ({ key, ...itemProps }: ArrayFieldTemplateItemType) => ( diff --git a/src/lib/components/json-schema/templates/BaseInputTemplate.tsx b/src/lib/components/json-schema/form/templates/BaseInputTemplate.tsx similarity index 95% rename from src/lib/components/json-schema/templates/BaseInputTemplate.tsx rename to src/lib/components/json-schema/form/templates/BaseInputTemplate.tsx index f3e5f464e..7703748a1 100644 --- a/src/lib/components/json-schema/templates/BaseInputTemplate.tsx +++ b/src/lib/components/json-schema/form/templates/BaseInputTemplate.tsx @@ -122,7 +122,13 @@ const BaseInputTemplate = (props: WidgetProps) => { > {displayLabel && ( - + {label} @@ -164,7 +170,7 @@ const BaseInputTemplate = (props: WidgetProps) => { ) : null} {!!schema.description && ( - + (props: WidgetProps) => { isRequired={required} isReadOnly={readonly} isInvalid={rawErrors && rawErrors.length > 0} + sx={{ "& > p": { mt: 4, mb: 2 } }} > {!!schema.description && ( (props: WidgetProps) => { autoFocus={autofocus} value={formValue} menuPosition="fixed" - selectedOptionColorScheme="gray" + chakraStyles={{ + option: (provided, state) => ({ + ...provided, + bg: state.isSelected ? "gray.800" : undefined, + color: "text.main", + _hover: { + bg: "gray.700", + }, + }), + }} /> ); diff --git a/src/lib/components/json-schema/widgets/index.ts b/src/lib/components/json-schema/form/widgets/index.ts similarity index 100% rename from src/lib/components/json-schema/widgets/index.ts rename to src/lib/components/json-schema/form/widgets/index.ts diff --git a/src/lib/components/json-schema/widgets/useDefaultChakraSelectStyle.ts b/src/lib/components/json-schema/form/widgets/useDefaultChakraSelectStyle.ts similarity index 100% rename from src/lib/components/json-schema/widgets/useDefaultChakraSelectStyle.ts rename to src/lib/components/json-schema/form/widgets/useDefaultChakraSelectStyle.ts diff --git a/src/lib/components/json-schema/index.ts b/src/lib/components/json-schema/index.ts new file mode 100644 index 000000000..9b95a8fac --- /dev/null +++ b/src/lib/components/json-schema/index.ts @@ -0,0 +1,5 @@ +export * from "./AttachSchemaCard"; +export * from "./JsonSchemaDrawer"; +export * from "./UploadTemplate"; +export * from "./ViewSchemaButton"; +export * from "./form"; diff --git a/src/lib/components/select-code/CodeSelect.tsx b/src/lib/components/select-code/CodeSelect.tsx index d28282878..09032b397 100644 --- a/src/lib/components/select-code/CodeSelect.tsx +++ b/src/lib/components/select-code/CodeSelect.tsx @@ -6,26 +6,33 @@ import { PermissionChip } from "../PermissionChip"; import type { FormStatus } from "lib/components/forms"; import { UploadIcon } from "lib/components/icon"; import { useCodeStore } from "lib/providers/store"; -import { useCodeDataByCodeId } from "lib/services/codeService"; +import type { LCDCodeInfoSuccessCallback } from "lib/services/codeService"; +import { useLCDCodeInfo } from "lib/services/codeService"; import { AccessConfigPermission } from "lib/types"; +import { isCodeId } from "lib/utils"; import { CodeSelectDrawerButton } from "./CodeSelectDrawerButton"; interface CodeSelectProps extends Omit { onCodeSelect: (code: string) => void; + setCodeHash?: LCDCodeInfoSuccessCallback; codeId: string; status: FormStatus; } export const CodeSelect = ({ onCodeSelect, + setCodeHash, codeId, status, ...componentProps }: CodeSelectProps) => { const { getCodeLocalInfo } = useCodeStore(); const name = getCodeLocalInfo(Number(codeId))?.name; - const { data: codeInfo } = useCodeDataByCodeId(codeId); + const { data: codeInfo } = useLCDCodeInfo(codeId, { + onSuccess: setCodeHash, + enabled: isCodeId(codeId), + }); const isError = status.state === "error"; return ( @@ -59,10 +66,12 @@ export const CodeSelect = ({ diff --git a/src/lib/components/select-code/CodeSelectSection.tsx b/src/lib/components/select-code/CodeSelectSection.tsx index 7a34ca692..e91d63c89 100644 --- a/src/lib/components/select-code/CodeSelectSection.tsx +++ b/src/lib/components/select-code/CodeSelectSection.tsx @@ -5,6 +5,7 @@ import type { Control, FieldPath, FieldValues } from "react-hook-form"; import { ControllerInput } from "lib/components/forms"; import type { FormStatus } from "lib/components/forms"; import { AmpEvent, AmpTrack } from "lib/services/amplitude"; +import type { LCDCodeInfoSuccessCallback } from "lib/services/codeService"; import type { Option } from "lib/types"; import { CodeSelect } from "./CodeSelect"; @@ -15,6 +16,7 @@ interface CodeSelectSectionProps { control: Control; error: Option; onCodeSelect: (codeId: string) => void; + setCodeHash?: LCDCodeInfoSuccessCallback; status: FormStatus; } @@ -24,6 +26,7 @@ export const CodeSelectSection = ({ control, error, onCodeSelect, + setCodeHash, status, }: CodeSelectSectionProps) => { const [method, setMethod] = useState<"select-existing" | "fill-manually">( @@ -55,6 +58,7 @@ export const CodeSelectSection = ({ mt={4} mb={8} onCodeSelect={onCodeSelect} + setCodeHash={setCodeHash} codeId={codeId} status={status} /> diff --git a/src/lib/model/code.ts b/src/lib/model/code.ts index 9bd8a2a60..602dda3c1 100644 --- a/src/lib/model/code.ts +++ b/src/lib/model/code.ts @@ -53,7 +53,7 @@ export const useCodeData = (codeId: string): CodeDataState => { const { currentChainId } = useCelatoneApp(); const lcdEndpoint = useBaseApiRoute("rest"); - const { data: codeInfo, isLoading } = useCodeDataByCodeId(codeId); + const { data: codeInfo, isLoading } = useCodeDataByCodeId({ codeId }); const { data: publicCodeInfo } = usePublicProjectByCodeId(codeId); const { data: publicInfoBySlug } = usePublicProjectBySlug( publicCodeInfo?.slug diff --git a/src/lib/pages/migrate/components/MessageInputSwitch.tsx b/src/lib/pages/migrate/components/MessageInputSwitch.tsx new file mode 100644 index 000000000..362b10adc --- /dev/null +++ b/src/lib/pages/migrate/components/MessageInputSwitch.tsx @@ -0,0 +1,72 @@ +import { Flex } from "@chakra-ui/react"; +import type { Dispatch, SetStateAction } from "react"; +import { useRef } from "react"; + +import { MotionBox } from "lib/components/MotionBox"; + +interface MessageInputSwitchProps { + currentTab: T; + tabs: T[]; + disabled?: boolean; + onTabChange: Dispatch>; +} + +export const MessageInputSwitch = ({ + currentTab, + tabs, + disabled = false, + onTabChange, +}: MessageInputSwitchProps) => { + const tabRefs = useRef<(HTMLDivElement | null)[]>([]); + const activeIndex = tabs.indexOf(currentTab); + return ( + + {tabs.map((tab, idx) => ( + { + tabRefs.current[idx] = el; + }} + cursor="pointer" + p="2px 10px" + fontSize="12px" + fontWeight={700} + variants={{ + active: { color: "var(--chakra-colors-text-main)" }, + inactive: { + color: "var(--chakra-colors-primary-light)", + }, + }} + initial="inactive" + animate={currentTab === tab ? "active" : "inactive"} + onClick={() => onTabChange(tab)} + zIndex={1} + textAlign="center" + > + {tab} + + ))} + + + ); +}; diff --git a/src/lib/pages/migrate/components/MigrateContract.tsx b/src/lib/pages/migrate/components/MigrateContract.tsx index 0b85aad66..f28c9ea41 100644 --- a/src/lib/pages/migrate/components/MigrateContract.tsx +++ b/src/lib/pages/migrate/components/MigrateContract.tsx @@ -1,7 +1,8 @@ -import { Button, Flex, Heading, Text } from "@chakra-ui/react"; +import { Box, Button, Flex, Heading, Text } from "@chakra-ui/react"; +import type { BoxProps } from "@chakra-ui/react"; import type { StdFee } from "@cosmjs/stargate"; -import { useQuery } from "@tanstack/react-query"; import Long from "long"; +import { observer } from "mobx-react-lite"; import { useCallback, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -9,8 +10,6 @@ import { useFabricateFee, useSimulateFeeQuery, useCurrentChain, - useBaseApiRoute, - CELATONE_QUERY_KEYS, } from "lib/app-provider"; import { useMigrateTx } from "lib/app-provider/tx/migrate"; import { EstimatedFeeRender } from "lib/components/EstimatedFeeRender"; @@ -18,12 +17,17 @@ import type { FormStatus } from "lib/components/forms"; import { CustomIcon } from "lib/components/icon"; import JsonInput from "lib/components/json/JsonInput"; import { CodeSelectSection } from "lib/components/select-code"; +import { Tooltip } from "lib/components/Tooltip"; import { useTxBroadcast } from "lib/providers/tx-broadcast"; import { AmpEvent, AmpTrack } from "lib/services/amplitude"; -import { getCodeIdInfo } from "lib/services/code"; +import type { CodeIdInfoResponse } from "lib/services/code"; +import { useLCDCodeInfo } from "lib/services/codeService"; import type { ComposedMsg, ContractAddr, HumanAddr } from "lib/types"; -import { AccessConfigPermission, MsgType } from "lib/types"; -import { composeMsg, jsonValidate } from "lib/utils"; +import { MsgType } from "lib/types"; +import { composeMsg, jsonValidate, resolvePermission } from "lib/utils"; + +import { MessageInputSwitch } from "./MessageInputSwitch"; +import { SchemaSection } from "./SchemaSection"; interface MigrateContractProps { contractAddress: ContractAddr; @@ -31,67 +35,84 @@ interface MigrateContractProps { handleBack: () => void; } -export const MigrateContract = ({ - contractAddress, - codeIdParam, - handleBack, -}: MigrateContractProps) => { - const { address } = useCurrentChain(); - const { broadcast } = useTxBroadcast(); - const lcdEndpoint = useBaseApiRoute("rest"); - - const migrateTx = useMigrateTx(); - const fabricateFee = useFabricateFee(); - - const { - control, - watch, - setValue, - formState: { errors }, - } = useForm({ - defaultValues: { codeId: codeIdParam, migrateMsg: "{}" }, - mode: "all", - }); - const { codeId, migrateMsg } = watch(); - const [status, setStatus] = useState({ state: "init" }); - const [composedTxMsg, setComposedTxMsg] = useState([]); - const [estimatedFee, setEstimatedFee] = useState(); - const [simulateError, setSimulateError] = useState(""); - const [processing, setProcessing] = useState(false); - - const enableMigrate = - !!address && - codeId.length > 0 && - jsonValidate(migrateMsg) === null && - status.state === "success"; - - const { isFetching: isSimulating } = useSimulateFeeQuery({ - enabled: composedTxMsg.length > 0, - messages: composedTxMsg, - onSuccess: (gasRes) => { - if (gasRes) setEstimatedFee(fabricateFee(gasRes)); - else setEstimatedFee(undefined); - }, - onError: (e) => { - setSimulateError(e.message); - setEstimatedFee(undefined); - }, - }); - - const { refetch } = useQuery( - [CELATONE_QUERY_KEYS.CODE_INFO, lcdEndpoint, codeId], - async () => getCodeIdInfo(lcdEndpoint, codeId), - { +enum MessageTabs { + JSON_INPUT = "JSON Input", + YOUR_SCHEMA = "Your Schema", +} + +const resolveTabDisplay = ( + current: MessageTabs, + target: MessageTabs +): BoxProps["display"] => { + return current === target ? "block" : "none"; +}; + +export const MigrateContract = observer( + ({ contractAddress, codeIdParam, handleBack }: MigrateContractProps) => { + const { address } = useCurrentChain(); + const { broadcast } = useTxBroadcast(); + const migrateTx = useMigrateTx(); + const fabricateFee = useFabricateFee(); + + const { + control, + watch, + setValue, + formState: { errors }, + } = useForm({ + defaultValues: { + codeId: codeIdParam, + codeHash: "", + msgInput: { + [MessageTabs.JSON_INPUT]: "{}", + [MessageTabs.YOUR_SCHEMA]: "{}", + }, + }, + mode: "all", + }); + const { codeId, codeHash, msgInput } = watch(); + const [tab, setTab] = useState(MessageTabs.JSON_INPUT); + const [status, setStatus] = useState({ state: "init" }); + const [composedTxMsg, setComposedTxMsg] = useState([]); + const [estimatedFee, setEstimatedFee] = useState(); + const [simulateError, setSimulateError] = useState(""); + const [processing, setProcessing] = useState(false); + + const currentInput = msgInput[tab]; + + const enableMigrate = + !!address && + codeId.length > 0 && + jsonValidate(currentInput) === null && + status.state === "success"; + + const { isFetching: isSimulating } = useSimulateFeeQuery({ + enabled: composedTxMsg.length > 0, + messages: composedTxMsg, + onSuccess: (gasRes) => { + if (gasRes) setEstimatedFee(fabricateFee(gasRes)); + else setEstimatedFee(undefined); + }, + onError: (e) => { + setSimulateError(e.message); + setEstimatedFee(undefined); + }, + }); + + const { refetch } = useLCDCodeInfo(codeId, { enabled: !!address && !!codeId.length, retry: false, cacheTime: 0, onSuccess(data) { const permission = data.code_info.instantiate_permission; + setValue("codeHash", data.code_info.data_hash.toLowerCase()); if ( - address && - (permission.permission === AccessConfigPermission.EVERYBODY || - permission.addresses.includes(address as HumanAddr) || - permission.address === address) + resolvePermission( + address as HumanAddr, + permission.permission, + permission.addresses, + permission.address + ) ) setStatus({ state: "success" }); else { @@ -107,123 +128,181 @@ export const MigrateContract = ({ setStatus({ state: "error", message: "This code ID does not exist" }); setSimulateError(""); }, - } - ); + }); - const proceed = useCallback(async () => { - AmpTrack(AmpEvent.ACTION_MIGRATE); - const stream = await migrateTx({ + const proceed = useCallback(async () => { + AmpTrack(AmpEvent.ACTION_MIGRATE); + const stream = await migrateTx({ + contractAddress, + codeId: Number(codeId), + migrateMsg: JSON.parse(currentInput), + estimatedFee, + onTxSucceed: () => setProcessing(false), + onTxFailed: () => setProcessing(false), + }); + + if (stream) { + setProcessing(true); + broadcast(stream); + } + }, [ + migrateTx, contractAddress, - codeId: Number(codeId), - migrateMsg: JSON.parse(migrateMsg), + codeId, + currentInput, estimatedFee, - onTxSucceed: () => setProcessing(false), - onTxFailed: () => setProcessing(false), - }); + broadcast, + setProcessing, + ]); - if (stream) { - setProcessing(true); - broadcast(stream); - } - }, [migrateTx, contractAddress, codeId, migrateMsg, estimatedFee, broadcast]); - - useEffect(() => { - if (codeId.length) { - setStatus({ state: "loading" }); - const timer = setTimeout(() => { - refetch(); - }, 500); - return () => clearTimeout(timer); - } - setStatus({ state: "init" }); - - return () => {}; - }, [address, codeId, refetch]); - - useEffect(() => { - if (enableMigrate) { - setSimulateError(""); - const composedMsg = composeMsg(MsgType.MIGRATE, { - sender: address as HumanAddr, - contract: contractAddress as ContractAddr, - codeId: Long.fromString(codeId), - msg: Buffer.from(migrateMsg), - }); - const timeoutId = setTimeout(() => { - setComposedTxMsg([composedMsg]); - }, 1000); - return () => clearTimeout(timeoutId); - } - return () => {}; - }, [address, codeId, contractAddress, enableMigrate, migrateMsg]); - - return ( - <> - - To Code ID - - setValue("codeId", code)} - codeId={codeId} - /> - - Migrate Message - - setValue("migrateMsg", msg)} - minLines={10} - /> - {simulateError && ( - - - - {simulateError} - - - )} - -

Transaction Fee:

- { + if (codeId.length) { + setStatus({ state: "loading" }); + const timer = setTimeout(() => { + refetch(); + }, 500); + return () => clearTimeout(timer); + } + setStatus({ state: "init" }); + + return () => {}; + }, [address, codeId, refetch]); + + useEffect(() => { + if (enableMigrate) { + setSimulateError(""); + const composedMsg = composeMsg(MsgType.MIGRATE, { + sender: address as HumanAddr, + contract: contractAddress as ContractAddr, + codeId: Long.fromString(codeId), + msg: Buffer.from(currentInput), + }); + const timeoutId = setTimeout(() => { + setComposedTxMsg([composedMsg]); + }, 1000); + return () => clearTimeout(timeoutId); + } + return () => {}; + }, [address, codeId, contractAddress, enableMigrate, currentInput]); + + return ( + <> + + To Code ID + + { + setValue("codeId", code); + }} + setCodeHash={(data: CodeIdInfoResponse) => { + setValue("codeHash", data.code_info.data_hash.toLowerCase()); + }} + codeId={codeId} /> -
- - - - - - ); -}; +

Transaction Fee:

+ + + + + + + + ); + } +); diff --git a/src/lib/pages/migrate/components/SchemaSection.tsx b/src/lib/pages/migrate/components/SchemaSection.tsx new file mode 100644 index 000000000..a735a2a60 --- /dev/null +++ b/src/lib/pages/migrate/components/SchemaSection.tsx @@ -0,0 +1,87 @@ +import { Button, Flex, Text, useDisclosure } from "@chakra-ui/react"; +import { observer } from "mobx-react-lite"; + +import { + ViewSchemaButton, + AttachSchemaCard, + JsonSchemaDrawer, +} from "lib/components/json-schema/"; +import { JsonSchemaForm } from "lib/components/json-schema/form"; +import { useSchemaStore } from "lib/providers/store"; + +interface SchemaSectionProps { + codeHash: string; + codeId: string; + setSchemaInput: (input: string) => void; +} + +export const SchemaSection = observer( + ({ codeHash, codeId, setSchemaInput }: SchemaSectionProps) => { + const { getSchemaByCodeHash } = useSchemaStore(); + const jsonSchema = getSchemaByCodeHash(codeHash); + const { isOpen, onClose, onOpen } = useDisclosure(); + return ( + + {jsonSchema?.migrate ? ( + <> + + + You are using a locally attached JSON Schema + + + + + + + setSchemaInput(JSON.stringify(data))} + /> + + ) : ( + <> + + {jsonSchema + ? "Attached JSON Schema doesn’t have MigrateMsg" + : `You haven't attached the JSON Schema for code ${codeId} yet`} + + + {jsonSchema + ? "Please fill in Migrate Message manually or change the schema" + : "Your attached JSON schema will be stored locally on your device"} + + + + )} + + + ); + } +); diff --git a/src/lib/pages/upload/completed.tsx b/src/lib/pages/upload/completed.tsx index 2847303cb..2a11275da 100644 --- a/src/lib/pages/upload/completed.tsx +++ b/src/lib/pages/upload/completed.tsx @@ -21,6 +21,7 @@ export const UploadComplete = observer(({ txResult }: UploadCompleteProps) => { const { getSchemaByCodeHash } = useSchemaStore(); const schema = getSchemaByCodeHash(txResult.codeHash); const attached = Boolean(schema); + return ( @@ -93,6 +94,7 @@ export const UploadComplete = observer(({ txResult }: UploadCompleteProps) => { } w="full" + mt={attached ? 8 : 0} mb={4} onClick={() => { navigate({ diff --git a/src/lib/pages/upload/components/UploadSchema.tsx b/src/lib/pages/upload/components/UploadSchema.tsx index 901695049..9a3e6a503 100644 --- a/src/lib/pages/upload/components/UploadSchema.tsx +++ b/src/lib/pages/upload/components/UploadSchema.tsx @@ -1,26 +1,9 @@ -import { - Box, - Button, - Drawer, - DrawerBody, - DrawerCloseButton, - DrawerContent, - DrawerHeader, - DrawerOverlay, - Flex, - Heading, - IconButton, - Text, - useDisclosure, -} from "@chakra-ui/react"; +import { useDisclosure } from "@chakra-ui/react"; -import { CustomIcon } from "lib/components/icon"; -import { useSchemaStore } from "lib/providers/store"; +import { AttachSchemaCard, JsonSchemaDrawer } from "lib/components/json-schema"; import type { CodeSchema } from "lib/stores/schema"; import type { Option } from "lib/types"; -import { UploadTemplate } from "./UploadTemplate"; - interface UploadSchemaContentInterface { attached: boolean; schema: Option; @@ -28,135 +11,28 @@ interface UploadSchemaContentInterface { codeHash: string; } -const Content = ({ +export const UploadSchema = ({ attached, schema, codeId, codeHash, }: UploadSchemaContentInterface) => { const { isOpen, onOpen, onClose } = useDisclosure(); - const { deleteSchema } = useSchemaStore(); return ( <> - {!attached ? ( - <> - Attach JSON Schema - - - ) : ( - <> - - - JSON Schema attached - - - - - } - /> - deleteSchema(codeHash)} - icon={ - - } - /> - - - )} - - - - - - - - - Attach JSON Schema for code ID “{codeId}” - - - - Your attached JSON schema will be stored locally on your device - - - - - - - - Please note that the JSON schema you upload on our website will - only be stored locally on your device. For public projects with - verified JSON schemas, they will be visible and accessible to - others. - - - - - - + + ); }; - -export const UploadSchema = (props: UploadSchemaContentInterface) => { - const { attached } = props; - return ( - - - - ); -}; diff --git a/src/lib/services/code.ts b/src/lib/services/code.ts index d06302e29..ebdb2e299 100644 --- a/src/lib/services/code.ts +++ b/src/lib/services/code.ts @@ -2,7 +2,7 @@ import axios from "axios"; import type { Addr, AccessConfigPermission } from "lib/types"; -interface CodeIdInfoResponse { +export interface CodeIdInfoResponse { code_info: { code_id: string; creator: Addr; diff --git a/src/lib/services/codeService.ts b/src/lib/services/codeService.ts index 8e38c4dec..255800d4a 100644 --- a/src/lib/services/codeService.ts +++ b/src/lib/services/codeService.ts @@ -1,10 +1,11 @@ /* eslint-disable sonarjs/no-identical-functions */ -import type { UseQueryResult } from "@tanstack/react-query"; +import type { UseQueryOptions, UseQueryResult } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"; import { useCallback } from "react"; import { CELATONE_QUERY_KEYS, + useBaseApiRoute, useCelatoneApp, useWasmConfig, } from "lib/app-provider"; @@ -28,6 +29,9 @@ import type { } from "lib/types"; import { isCodeId, parseDateOpt, parseTxHashOpt } from "lib/utils"; +import type { CodeIdInfoResponse } from "./code"; +import { getCodeIdInfo } from "./code"; + export const useCodeListQuery = (): UseQueryResult => { const { indexerGraphClient } = useCelatoneApp(); @@ -129,10 +133,18 @@ export const useCodeListByCodeIds = ( ); }; -export const useCodeDataByCodeId = ( - codeId: string, - enabled = true -): UseQueryResult | null> => { +interface CodeDataByCodeIdParams { + codeId: string; + enabled?: boolean; +} + +export const useCodeDataByCodeId = ({ + codeId, + enabled = true, +}: CodeDataByCodeIdParams): UseQueryResult | null> => { const { indexerGraphClient } = useCelatoneApp(); const queryFn = useCallback(async () => { @@ -260,3 +272,18 @@ export const useCodeListCountByWalletAddress = ( } ); }; + +export type LCDCodeInfoSuccessCallback = (data: CodeIdInfoResponse) => void; + +export const useLCDCodeInfo = ( + codeId: string, + options?: Omit, "queryKey"> +): UseQueryResult => { + const lcdEndpoint = useBaseApiRoute("rest"); + const queryFn = async () => getCodeIdInfo(lcdEndpoint, codeId); + return useQuery( + [CELATONE_QUERY_KEYS.CODE_INFO, lcdEndpoint, codeId], + queryFn, + options + ); +}; diff --git a/src/lib/services/searchService.ts b/src/lib/services/searchService.ts index 89bd780aa..4497e539b 100644 --- a/src/lib/services/searchService.ts +++ b/src/lib/services/searchService.ts @@ -54,10 +54,10 @@ export const useSearchHandler = ( const getAddressType = useGetAddressType(); const addressType = getAddressType(debouncedKeyword); const { data: txData, isFetching: txFetching } = useTxData(debouncedKeyword); - const { data: codeData, isFetching: codeFetching } = useCodeDataByCodeId( - debouncedKeyword, - isWasm && isCodeId(debouncedKeyword) - ); + const { data: codeData, isFetching: codeFetching } = useCodeDataByCodeId({ + codeId: debouncedKeyword, + enabled: isWasm && isCodeId(debouncedKeyword), + }); const { data: contractData, isFetching: contractFetching } = useQuery( [CELATONE_QUERY_KEYS.CONTRACT_INFO, lcdEndpoint, debouncedKeyword], async () => queryContract(lcdEndpoint, debouncedKeyword as ContractAddr), diff --git a/src/lib/utils/codePermission.ts b/src/lib/utils/codePermission.ts index 0226ba131..f2236fd8e 100644 --- a/src/lib/utils/codePermission.ts +++ b/src/lib/utils/codePermission.ts @@ -6,10 +6,13 @@ import { truncate } from "./truncate"; export const resolvePermission = ( address: Option, permission: AccessConfigPermission = AccessConfigPermission.UNKNOWN, - permissionAddresses: PermissionAddresses = [] + permissionAddresses: PermissionAddresses = [], + permissionAddress = "" ): boolean => permission === AccessConfigPermission.EVERYBODY || - (address ? permissionAddresses.includes(address) : false); + (address + ? permissionAddresses.includes(address) || permissionAddress === address + : false); export const getPermissionHelper = ( address: Option,