From 264c444b9e91fe99ec80fc44b862cee918051407 Mon Sep 17 00:00:00 2001 From: Ramida J Date: Tue, 3 Jan 2023 17:05:50 +0700 Subject: [PATCH 01/13] feat(pages): add ui for send asset in execute contract page --- CHANGELOG.md | 2 +- src/lib/components/ContractSelectSection.tsx | 1 - .../forms}/AssetInput.tsx | 2 +- src/lib/components/forms/index.ts | 1 + .../modal/select-contract/SelectContract.tsx | 8 +- .../pages/execute/components/ExecuteArea.tsx | 56 +- src/lib/pages/execute/index.tsx | 2 +- src/lib/pages/instantiate/component/index.ts | 1 - src/lib/pages/instantiate/instantiate.tsx | 3 +- src/lib/pages/query/index.tsx | 9 +- yarn.lock | 16272 ++++++++-------- 11 files changed, 8126 insertions(+), 8231 deletions(-) rename src/lib/{pages/instantiate/component => components/forms}/AssetInput.tsx (95%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 059d53025..e7b8d1624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Features - +- [#61](https://github.com/alleslabs/celatone-frontend/pull/61) Add UI for send asset in execute contract page - [#59](https://github.com/alleslabs/celatone-frontend/pull/58) Wireup code name,description, and cta section - [#53](https://github.com/alleslabs/celatone-frontend/pull/53) Show contract description in contract details page - [#58](https://github.com/alleslabs/celatone-frontend/pull/58) Wireup top section in contract details page diff --git a/src/lib/components/ContractSelectSection.tsx b/src/lib/components/ContractSelectSection.tsx index 489596002..f58f836d0 100644 --- a/src/lib/components/ContractSelectSection.tsx +++ b/src/lib/components/ContractSelectSection.tsx @@ -159,7 +159,6 @@ export const ContractSelectSection = observer( const notSelected = contractAddress.length === 0; return ( - {notSelected ? "SELECT CONTRACT" : "CHANGE"} + {!notSelected && } + {notSelected ? "Select Contract" : "Change Contract"} {listSlug.length === 0 || !contractList ? ( - + Select Contract diff --git a/src/lib/pages/execute/components/ExecuteArea.tsx b/src/lib/pages/execute/components/ExecuteArea.tsx index 667562222..e0808c58a 100644 --- a/src/lib/pages/execute/components/ExecuteArea.tsx +++ b/src/lib/pages/execute/components/ExecuteArea.tsx @@ -11,6 +11,7 @@ import { useExecuteContractTx } from "lib/app-provider/tx/execute"; import { ContractCmdButton } from "lib/components/ContractCmdButton"; import CopyButton from "lib/components/CopyButton"; import { EstimatedFeeRender } from "lib/components/EstimatedFeeRender"; +import { AssetInput, TextInput } from "lib/components/forms"; import JsonInput from "lib/components/json/JsonInput"; import { useContractStore } from "lib/hooks"; import { useTxBroadcast } from "lib/providers/tx-broadcast"; @@ -112,12 +113,12 @@ export const ExecuteArea = ({ }); return ( - + {cmds.length ? ( button": { marginInlineStart: "0 !important", @@ -140,21 +141,44 @@ export const ExecuteArea = ({ ) )} - - {error && ( - - - - {error} + + + + Execute Messages - - )} - + + {error && ( + + + + {error} + + + )} + + + + Send Assets + + null} + setCurrencyValue={() => null} + assetOptions={[]} + amountInput={ null} />} + /> + + + + diff --git a/src/lib/pages/execute/index.tsx b/src/lib/pages/execute/index.tsx index b228fa237..2dca1ffdd 100644 --- a/src/lib/pages/execute/index.tsx +++ b/src/lib/pages/execute/index.tsx @@ -76,7 +76,7 @@ const Execute = () => { > BACK - + Execute Contract diff --git a/src/lib/pages/instantiate/component/index.ts b/src/lib/pages/instantiate/component/index.ts index 216c81d9c..b32172090 100644 --- a/src/lib/pages/instantiate/component/index.ts +++ b/src/lib/pages/instantiate/component/index.ts @@ -1,4 +1,3 @@ -export * from "./AssetInput"; export * from "./code-select/CodeSelect"; export * from "./FailedModal"; export * from "./Footer"; diff --git a/src/lib/pages/instantiate/instantiate.tsx b/src/lib/pages/instantiate/instantiate.tsx index dcf53759d..ecd094da7 100644 --- a/src/lib/pages/instantiate/instantiate.tsx +++ b/src/lib/pages/instantiate/instantiate.tsx @@ -16,6 +16,7 @@ import { useFieldArray, useForm } from "react-hook-form"; import { useFabricateFee, useSimulateFee } from "lib/app-provider"; import { useInstantiateTx } from "lib/app-provider/tx/instantiate"; import { ControllerInput, TextInput } from "lib/components/forms"; +import { AssetInput } from "lib/components/forms/AssetInput"; import JsonInput from "lib/components/json/JsonInput"; import { Stepper } from "lib/components/stepper"; import WasmPageContainer from "lib/components/WasmPageContainer"; @@ -30,7 +31,7 @@ import { microfy, } from "lib/utils"; -import { AssetInput, CodeSelect, FailedModal, Footer } from "./component"; +import { CodeSelect, FailedModal, Footer } from "./component"; import type { InstantiateRedoMsg } from "./types"; interface InstantiatePageProps { diff --git a/src/lib/pages/query/index.tsx b/src/lib/pages/query/index.tsx index 3bcc8296a..90c197e09 100644 --- a/src/lib/pages/query/index.tsx +++ b/src/lib/pages/query/index.tsx @@ -1,5 +1,5 @@ import { ArrowBackIcon } from "@chakra-ui/icons"; -import { Heading, Button, Box, Flex, Spacer } from "@chakra-ui/react"; +import { Heading, Button, Box, Flex } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useRouter } from "next/router"; @@ -97,11 +97,10 @@ const Query = () => { > BACK - - - Query + + + Query Contract - + + ))} + + + + + + + + + ); +}; + +export default Footer; diff --git a/src/lib/pages/execute/components/ExecuteArea.tsx b/src/lib/pages/execute/components/ExecuteArea.tsx index e0808c58a..12dc08a27 100644 --- a/src/lib/pages/execute/components/ExecuteArea.tsx +++ b/src/lib/pages/execute/components/ExecuteArea.tsx @@ -1,6 +1,7 @@ import { Box, Flex, Button, ButtonGroup, Icon, Text } from "@chakra-ui/react"; import type { StdFee } from "@cosmjs/stargate"; import { useWallet } from "@cosmos-kit/react"; +import type { SetStateAction } from "react"; import { useCallback, useEffect, useState } from "react"; import { IoIosWarning } from "react-icons/io"; import { MdInput } from "react-icons/md"; @@ -11,7 +12,7 @@ import { useExecuteContractTx } from "lib/app-provider/tx/execute"; import { ContractCmdButton } from "lib/components/ContractCmdButton"; import CopyButton from "lib/components/CopyButton"; import { EstimatedFeeRender } from "lib/components/EstimatedFeeRender"; -import { AssetInput, TextInput } from "lib/components/forms"; +import { AssetInput, TextInput, SelectInput } from "lib/components/forms"; import JsonInput from "lib/components/json/JsonInput"; import { useContractStore } from "lib/hooks"; import { useTxBroadcast } from "lib/providers/tx-broadcast"; @@ -23,12 +24,14 @@ import { composeMsg, jsonPrettify, jsonValidate } from "lib/utils"; interface ExecuteAreaProps { contractAddress: ContractAddr; initialMsg: string; + initialFundMsg?: string; cmds: [string, string][]; } export const ExecuteArea = ({ contractAddress, initialMsg, + initialFundMsg, cmds, }: ExecuteAreaProps) => { const { address = "" } = useWallet(); @@ -39,10 +42,27 @@ export const ExecuteArea = ({ const [fee, setFee] = useState(); const [msg, setMsg] = useState(initialMsg); + const [fundMsg, setFundMsg] = useState(initialFundMsg); const [error, setError] = useState(""); const [composedTxMsg, setComposedTxMsg] = useState([]); const [processing, setProcessing] = useState(false); - + const [attachFundOption, setAttachFundOption] = useState(""); + const attachFundOptions = [ + { label: "Not sending funds", value: "null", disabled: false }, + { + label: "Select asset and fill amount", + value: "fill", + disabled: false, + }, + { + label: "Provide JSON Asset List", + value: "json", + disabled: false, + }, + ]; + const handleAttachFundOption = (e: SetStateAction) => { + setAttachFundOption(e); + }; const enableExecute = !!( msg.trim().length && jsonValidate(msg) === null && @@ -81,7 +101,7 @@ export const ExecuteArea = ({ }, [contractAddress, fee, msg, addActivity, executeTx, broadcast]); useEffect(() => setMsg(initialMsg), [initialMsg]); - + useEffect(() => setFundMsg(initialFundMsg), [initialFundMsg]); useEffect(() => { if (enableExecute) { setError(""); @@ -141,8 +161,8 @@ export const ExecuteArea = ({ ) )} - - + + Execute Messages @@ -161,21 +181,41 @@ export const ExecuteArea = ({ )} - + Send Assets - null} - setCurrencyValue={() => null} - assetOptions={[]} - amountInput={ null} />} - /> - + + + + {/* TODO: Add asset (input) */} + {attachFundOption === "fill" && ( + + null} + setCurrencyValue={() => null} + assetOptions={[]} + amountInput={ null} />} + /> + + + )} + {/* TODO: Add asset (json) */} + {attachFundOption === "json" && ( + + + + )} diff --git a/src/lib/pages/execute/index.tsx b/src/lib/pages/execute/index.tsx index 2dca1ffdd..b804f188b 100644 --- a/src/lib/pages/execute/index.tsx +++ b/src/lib/pages/execute/index.tsx @@ -90,12 +90,10 @@ const Execute = () => { subtitle="You need to connect your wallet to perform this action" mb={8} /> - - Date: Thu, 12 Jan 2023 13:26:49 +0700 Subject: [PATCH 03/13] fix(components): add change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7b8d1624..ef2907ab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Features +- [#79](https://github.com/alleslabs/celatone-frontend/pull/79) Add dropdown menu and json to attach fund - [#61](https://github.com/alleslabs/celatone-frontend/pull/61) Add UI for send asset in execute contract page - [#59](https://github.com/alleslabs/celatone-frontend/pull/58) Wireup code name,description, and cta section - [#53](https://github.com/alleslabs/celatone-frontend/pull/53) Show contract description in contract details page From 132d4ba82fa8866df6aef54e93e26e38dc929259 Mon Sep 17 00:00:00 2001 From: Ramida J Date: Mon, 16 Jan 2023 11:39:20 +0700 Subject: [PATCH 04/13] fix(components): fix option wording --- src/lib/pages/execute/components/ExecuteArea.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/pages/execute/components/ExecuteArea.tsx b/src/lib/pages/execute/components/ExecuteArea.tsx index 3d28373d8..cc6740f32 100644 --- a/src/lib/pages/execute/components/ExecuteArea.tsx +++ b/src/lib/pages/execute/components/ExecuteArea.tsx @@ -48,14 +48,14 @@ export const ExecuteArea = ({ const [processing, setProcessing] = useState(false); const [attachFundOption, setAttachFundOption] = useState(""); const attachFundOptions = [ - { label: "Not sending funds", value: "null", disabled: false }, + { label: "No funds attached", value: "null", disabled: false }, { - label: "Select asset and fill amount", + label: "Select from default assets", value: "fill", disabled: false, }, { - label: "Provide JSON Asset List", + label: "Provide asset list as JSON", value: "json", disabled: false, }, From 49a7e3a184e45e83739cc102d70665f13f141feb Mon Sep 17 00:00:00 2001 From: Ramida J Date: Thu, 19 Jan 2023 17:21:04 +0700 Subject: [PATCH 05/13] fix(components): combine option conditaion --- src/lib/components/forms/SelectInput.tsx | 4 +++- src/lib/pages/execute/components/ExecuteArea.tsx | 7 ++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/lib/components/forms/SelectInput.tsx b/src/lib/components/forms/SelectInput.tsx index c94e00e05..9877a43b3 100644 --- a/src/lib/components/forms/SelectInput.tsx +++ b/src/lib/components/forms/SelectInput.tsx @@ -14,6 +14,8 @@ import type { MutableRefObject, ReactNode } from "react"; import { useEffect, useRef, useState } from "react"; import { MdArrowDropDown } from "react-icons/md"; +import type { Option } from "lib/types"; + const ITEM_HEIGHT = 57; interface SelectInputProps { @@ -60,7 +62,7 @@ export const SelectInput = ({ const [selected, setSelected] = useState( () => options.find((asset) => asset.value === initialSelected)?.label ?? "" ); - const [inputRefWidth, setInputRefWidth] = useState(); + const [inputRefWidth, setInputRefWidth] = useState>(); useOutsideClick({ ref: optionRef, handler: () => isOpen && onClose(), diff --git a/src/lib/pages/execute/components/ExecuteArea.tsx b/src/lib/pages/execute/components/ExecuteArea.tsx index 5a9651828..7d14fb0b6 100644 --- a/src/lib/pages/execute/components/ExecuteArea.tsx +++ b/src/lib/pages/execute/components/ExecuteArea.tsx @@ -241,8 +241,7 @@ export const ExecuteArea = ({ initialSelected="null" /> - {/* TODO: Add asset (input) */} - {attachFundOption === "fill" && ( + {attachFundOption === "fill" ? ( {fields.map((field, idx) => ( - )} - {/* TODO: Add asset (json) */} - {attachFundOption === "json" && ( + ) : ( From 215a56baf25184be5d11cfc1b83aa1c1271b2c0b Mon Sep 17 00:00:00 2001 From: bkioshn Date: Tue, 21 Feb 2023 12:35:08 +0700 Subject: [PATCH 06/13] feat: wireup json attach funds in instantiate and execute page --- src/lib/components/WasmPageContainer.tsx | 2 +- src/lib/components/forms/SelectInput.tsx | 24 +- src/lib/components/fund/index.tsx | 93 +++++++ src/lib/components/fund/jsonFund.tsx | 21 ++ src/lib/components/fund/selectFund.tsx | 81 ++++++ src/lib/hooks/index.ts | 1 + src/lib/hooks/useAttachFunds.ts | 34 +++ .../pages/execute/components/ExecuteArea.tsx | 257 ++++++++---------- src/lib/pages/execute/index.tsx | 54 ++-- src/lib/pages/execute/types.ts | 9 - .../pages/instantiate/component/Footer.tsx | 9 +- src/lib/pages/instantiate/instantiate.tsx | 163 +++++------ src/lib/pages/past-txs/hooks/useRedo.ts | 2 +- src/lib/services/amplitude.tsx | 9 +- src/lib/types/currency/funds.ts | 13 + src/lib/types/currency/index.ts | 1 + 16 files changed, 481 insertions(+), 292 deletions(-) create mode 100644 src/lib/components/fund/index.tsx create mode 100644 src/lib/components/fund/jsonFund.tsx create mode 100644 src/lib/components/fund/selectFund.tsx create mode 100644 src/lib/hooks/useAttachFunds.ts delete mode 100644 src/lib/pages/execute/types.ts create mode 100644 src/lib/types/currency/funds.ts diff --git a/src/lib/components/WasmPageContainer.tsx b/src/lib/components/WasmPageContainer.tsx index 4921936b2..47d439d60 100644 --- a/src/lib/components/WasmPageContainer.tsx +++ b/src/lib/components/WasmPageContainer.tsx @@ -12,7 +12,7 @@ const WasmPageContainer = ({ children }: WasmPageContainerProps) => { align="center" width="540px" mx="auto" - my="48px" + my="90px" direction="column" > {children} diff --git a/src/lib/components/forms/SelectInput.tsx b/src/lib/components/forms/SelectInput.tsx index 60dff3225..fca032ead 100644 --- a/src/lib/components/forms/SelectInput.tsx +++ b/src/lib/components/forms/SelectInput.tsx @@ -12,7 +12,7 @@ import { InputLeftElement, } from "@chakra-ui/react"; import type { MutableRefObject, ReactNode } from "react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, useMemo } from "react"; import type { IconType } from "react-icons/lib"; import { MdArrowDropDown } from "react-icons/md"; @@ -33,6 +33,7 @@ interface SelectInputProps { placeholder?: string; initialSelected: string; hasDivider?: boolean; + helperTextComponent?: JSX.Element; } interface SelectItemProps { @@ -67,13 +68,16 @@ export const SelectInput = ({ placeholder = "", initialSelected, hasDivider = false, + helperTextComponent, }: SelectInputProps) => { const optionRef = useRef() as MutableRefObject; const inputRef = useRef() as MutableRefObject; const { isOpen, onClose, onOpen } = useDisclosure(); - const [selected, setSelected] = useState( - () => options.find((item) => item.value === initialSelected)?.label ?? "" + const initialSelectedValue = useMemo( + () => options.find((item) => item.value === initialSelected)?.label ?? "", + [initialSelected, options] ); + const [selected, setSelected] = useState(initialSelectedValue); const [inputRefWidth, setInputRefWidth] = useState>(); useOutsideClick({ ref: optionRef, @@ -86,6 +90,11 @@ export const SelectInput = ({ setInputRefWidth(inputRef.current.clientWidth); } }, [inputRef]); + + useEffect(() => { + setSelected(initialSelectedValue); + }, [initialSelectedValue]); + return ( @@ -168,6 +177,15 @@ export const SelectInput = ({ {label} ))} + + {helperTextComponent} + ); diff --git a/src/lib/components/fund/index.tsx b/src/lib/components/fund/index.tsx new file mode 100644 index 000000000..a12a6fb6a --- /dev/null +++ b/src/lib/components/fund/index.tsx @@ -0,0 +1,93 @@ +import { Flex, Text } from "@chakra-ui/react"; +import type { Control, UseFormSetValue } from "react-hook-form"; +import { useWatch } from "react-hook-form"; + +import { SelectInput } from "lib/components/forms"; +import type { AttachFundsState } from "lib/types"; +import { AttachFundsType } from "lib/types"; + +import { JsonFund } from "./jsonFund"; +import { SelectFund } from "./selectFund"; + +interface AttachFundContentProps { + control: Control; + setValue: UseFormSetValue; +} + +const attachFundOptions = [ + { + label: "No funds attached", + value: AttachFundsType.ATTACH_FUNDS_NULL, + disabled: false, + }, + { + label: "Select asset and fill amount", + value: AttachFundsType.ATTACH_FUNDS_SELECT, + disabled: false, + }, + { + label: "Provide JSON Asset List", + value: AttachFundsType.ATTACH_FUNDS_JSON, + disabled: false, + }, +]; + +const AttachFundContent = ({ control, setValue }: AttachFundContentProps) => { + const [assetsSelect, assetsJson, attachFundOption] = useWatch({ + control, + name: ["assetsSelect", "assetsJson", "attachFundOption"], + }); + + if (attachFundOption === AttachFundsType.ATTACH_FUNDS_SELECT) { + return ( + + ); + } + + if (attachFundOption === AttachFundsType.ATTACH_FUNDS_JSON) { + return ; + } + + return null; +}; + +interface AttachFundProps { + control: Control; + attachFundOption: AttachFundsType; + setValue: UseFormSetValue; +} + +export const AttachFund = ({ + control, + setValue, + attachFundOption, +}: AttachFundProps) => { + return ( + <> + + + setValue("attachFundOption", value) + } + initialSelected={attachFundOption.toString()} + helperTextComponent={ + + Only the input values in your selected{" "} + + ‘Attach funds’ + {" "} + option will be used. + + } + /> + + + + ); +}; diff --git a/src/lib/components/fund/jsonFund.tsx b/src/lib/components/fund/jsonFund.tsx new file mode 100644 index 000000000..0c27cf9cb --- /dev/null +++ b/src/lib/components/fund/jsonFund.tsx @@ -0,0 +1,21 @@ +import { Box } from "@chakra-ui/react"; +import type { UseFormSetValue } from "react-hook-form"; + +import JsonInput from "lib/components/json/JsonInput"; +import type { AttachFundsState } from "lib/types"; + +interface JsonFundProps { + setValue: UseFormSetValue; + assetsJson: string; +} +export const JsonFund = ({ setValue, assetsJson }: JsonFundProps) => { + const handleSetFundMsg = (value: string) => { + setValue("assetsJson", value); + }; + + return ( + + + + ); +}; diff --git a/src/lib/components/fund/selectFund.tsx b/src/lib/components/fund/selectFund.tsx new file mode 100644 index 000000000..d97ddde8a --- /dev/null +++ b/src/lib/components/fund/selectFund.tsx @@ -0,0 +1,81 @@ +import { Box, Button } from "@chakra-ui/react"; +import type { Coin } from "@cosmjs/stargate"; +import { useMemo } from "react"; +import type { Control, UseFormSetValue } from "react-hook-form"; +import { useFieldArray } from "react-hook-form"; + +import { useNativeTokensInfo } from "lib/app-provider"; +import { AssetInput, ControllerInput } from "lib/components/forms"; +import type { AttachFundsState } from "lib/types"; + +interface SelectFundProps { + control: Control; + setValue: UseFormSetValue; + assetsSelect: Coin[]; +} +export const SelectFund = ({ + control, + setValue, + assetsSelect, +}: SelectFundProps) => { + const nativeTokensInfo = useNativeTokensInfo(); + const { fields, append, remove } = useFieldArray({ + control, + name: "assetsSelect", + }); + + const selectedAssets = assetsSelect.map((asset) => asset.denom); + + const assetOptions = useMemo( + () => + nativeTokensInfo.map((asset) => ({ + label: asset.symbol, + value: asset.base, + disabled: selectedAssets.includes(asset.base), + })), + [nativeTokensInfo, selectedAssets] + ); + + const rules = { + pattern: { + value: /^[0-9]+([.][0-9]{0,6})?$/i, + message: 'Invalid amount. e.g. "100.00"', + }, + }; + + return ( + + {fields.map((field, idx) => ( + remove(idx)} + setCurrencyValue={(newVal: string) => + setValue(`assetsSelect.${idx}.denom`, newVal) + } + assetOptions={assetOptions} + initialSelected={field.denom} + amountInput={ + + } + /> + ))} + + + ); +}; diff --git a/src/lib/hooks/index.ts b/src/lib/hooks/index.ts index b207469e7..2b863f3c4 100644 --- a/src/lib/hooks/index.ts +++ b/src/lib/hooks/index.ts @@ -7,3 +7,4 @@ export * from "./useDummyWallet"; export * from "./useAddress"; export * from "./useCodeFilter"; export * from "./useChainId"; +export * from "./useAttachFunds"; diff --git a/src/lib/hooks/useAttachFunds.ts b/src/lib/hooks/useAttachFunds.ts new file mode 100644 index 000000000..cbc1a22d9 --- /dev/null +++ b/src/lib/hooks/useAttachFunds.ts @@ -0,0 +1,34 @@ +import type { Coin } from "@cosmjs/stargate"; +import { useCallback } from "react"; + +import { AttachFundsType } from "lib/types"; +import { fabricateFunds } from "lib/utils"; + +interface AttachFundsParams { + attachFundOption: AttachFundsType; + assetsJson: string; + assetsSelect: Coin[]; +} + +export const useAttachFunds = ({ + attachFundOption, + assetsJson, + assetsSelect, +}: AttachFundsParams) => { + return useCallback(() => { + if (attachFundOption === AttachFundsType.ATTACH_FUNDS_SELECT) { + return fabricateFunds(assetsSelect); + } + if (attachFundOption === AttachFundsType.ATTACH_FUNDS_JSON) { + try { + if (JSON.parse(assetsJson)) { + return JSON.parse(assetsJson) as Coin[]; + } + } catch { + // comment just to avoid eslint no-empty + } + } + + return []; + }, [assetsJson, assetsSelect, attachFundOption]); +}; diff --git a/src/lib/pages/execute/components/ExecuteArea.tsx b/src/lib/pages/execute/components/ExecuteArea.tsx index 877fa1ea4..e0cf847d3 100644 --- a/src/lib/pages/execute/components/ExecuteArea.tsx +++ b/src/lib/pages/execute/components/ExecuteArea.tsx @@ -1,54 +1,47 @@ import { Box, Flex, Button, ButtonGroup, Text } from "@chakra-ui/react"; -import type { StdFee } from "@cosmjs/stargate"; +import type { Coin, StdFee } from "@cosmjs/stargate"; import { useWallet } from "@cosmos-kit/react"; import dynamic from "next/dynamic"; -import type { SetStateAction } from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { useFieldArray, useFormState, useWatch } from "react-hook-form"; -import type { Control, UseFormSetValue } from "react-hook-form"; +import { useForm, useFormState } from "react-hook-form"; import { MdInput } from "react-icons/md"; -import type { ExecutePageState } from "../types"; -import { - useFabricateFee, - useNativeTokensInfo, - useExecuteContractTx, -} from "lib/app-provider"; +import { useFabricateFee, useExecuteContractTx } from "lib/app-provider"; import { useSimulateFeeQuery } from "lib/app-provider/queries"; import { ContractCmdButton } from "lib/components/ContractCmdButton"; import { CopyButton } from "lib/components/CopyButton"; import { ErrorMessageRender } from "lib/components/ErrorMessageRender"; import { EstimatedFeeRender } from "lib/components/EstimatedFeeRender"; -import { AssetInput, ControllerInput, SelectInput } from "lib/components/forms"; +import { AttachFund } from "lib/components/fund"; import JsonInput from "lib/components/json/JsonInput"; -import { useContractStore } from "lib/hooks"; +import { useAttachFunds, useContractStore } from "lib/hooks"; import { useTxBroadcast } from "lib/providers/tx-broadcast"; import { AmpEvent, AmpTrack, AmpTrackAction } from "lib/services/amplitude"; import type { Activity } from "lib/stores/contract"; -import type { ComposedMsg, ContractAddr, HumanAddr } from "lib/types"; -import { MsgType } from "lib/types"; -import { - composeMsg, - fabricateFunds, - jsonPrettify, - jsonValidate, -} from "lib/utils"; +import type { + AttachFundsState, + ComposedMsg, + ContractAddr, + HumanAddr, +} from "lib/types"; +import { AttachFundsType, MsgType } from "lib/types"; +import { composeMsg, jsonPrettify, jsonValidate } from "lib/utils"; const CodeSnippet = dynamic(() => import("lib/components/modal/CodeSnippet"), { ssr: false, }); interface ExecuteAreaProps { - control: Control; - setValue: UseFormSetValue; - initialFundMsg?: string; + contractAddress: ContractAddr; + initialMsg: string; + initialFunds: Coin[]; cmds: [string, string][]; } export const ExecuteArea = ({ - control, - setValue, - initialFundMsg, + contractAddress, + initialMsg, + initialFunds, cmds, }: ExecuteAreaProps) => { const { address } = useWallet(); @@ -56,66 +49,76 @@ export const ExecuteArea = ({ const executeTx = useExecuteContractTx(); const { broadcast } = useTxBroadcast(); const { addActivity } = useContractStore(); - const nativeTokensInfo = useNativeTokensInfo(); + const [fee, setFee] = useState(); + const [msg, setMsg] = useState(initialMsg); - const [contractAddress, initialMsg, assets] = useWatch({ - control, - name: ["contractAddress", "initialMsg", "assets"], - }); + const [error, setError] = useState(); + const [composedTxMsg, setComposedTxMsg] = useState([]); + const [processing, setProcessing] = useState(false); + + const assetDefault = useMemo( + () => ({ + assetsSelect: [{ denom: "", amount: "" }] as Coin[], + assetsJson: jsonPrettify(`[{ "denom": "", "amount": "" }]`), + attachFundOption: AttachFundsType.ATTACH_FUNDS_NULL, + }), + [] + ); - const { fields, append, remove } = useFieldArray({ - control, - name: "assets", + const { control, setValue, watch, reset } = useForm({ + mode: "all", + defaultValues: assetDefault, }); + + /** + * @remarks + * Handle when there is an initialFunds + */ + useEffect(() => { + if (initialFunds.length) { + setValue("assetsJson", jsonPrettify(JSON.stringify(initialFunds))); + setValue("attachFundOption", AttachFundsType.ATTACH_FUNDS_JSON); + } else { + reset(assetDefault); + } + }, [assetDefault, initialFunds, reset, setValue]); + const { errors } = useFormState({ control }); - const selectedAssets = assets.map((asset) => asset.denom); + const { assetsJson, assetsSelect, attachFundOption } = watch(); - const assetOptions = useMemo( - () => - nativeTokensInfo.map((asset) => ({ - label: asset.symbol, - value: asset.base, - disabled: selectedAssets.includes(asset.base), - })), - [nativeTokensInfo, selectedAssets] - ); + const validateAssetsSelect = !errors.assetsSelect; + const validateAssetsJson = + !errors.assetsJson && jsonValidate(assetsJson) === null; - const [fee, setFee] = useState(); - const [msg, setMsg] = useState(initialMsg); - const [fundMsg, setFundMsg] = useState(initialFundMsg); - const [error, setError] = useState(); - const [composedTxMsg, setComposedTxMsg] = useState([]); - const [processing, setProcessing] = useState(false); - const [attachFundOption, setAttachFundOption] = useState(""); - const attachFundOptions = [ - { label: "No funds attached", value: "null", disabled: false }, - { - label: "Select from default assets", - value: "fill", - disabled: false, - }, - { - label: "Provide asset list as JSON", - value: "json", - disabled: false, - }, - ]; - const handleAttachFundOption = (e: SetStateAction) => { - setAttachFundOption(e); - }; - const enableExecute = - !!( + const enableExecute = useMemo(() => { + const generalCheck = !!( msg.trim().length && jsonValidate(msg) === null && address && contractAddress - ) && !errors.assets; + ); + if (attachFundOption === AttachFundsType.ATTACH_FUNDS_SELECT) { + return generalCheck && validateAssetsSelect; + } + if (attachFundOption === AttachFundsType.ATTACH_FUNDS_JSON) { + return generalCheck && validateAssetsJson; + } + return generalCheck; + }, [ + address, + attachFundOption, + contractAddress, + msg, + validateAssetsJson, + validateAssetsSelect, + ]); const { isFetching } = useSimulateFeeQuery({ enabled: composedTxMsg.length > 0, messages: composedTxMsg, onSuccess: (gasRes) => { + setError(undefined); if (gasRes) setFee(fabricateFee(gasRes)); else setFee(undefined); }, @@ -125,13 +128,14 @@ export const ExecuteArea = ({ }, }); - const proceed = useCallback(async () => { - AmpTrackAction( - AmpEvent.ACTION_EXECUTE, - assets.filter((asset) => Number(asset.amount) && asset.denom).length - ); - const funds = fabricateFunds(assets); + const funds = useAttachFunds({ + attachFundOption, + assetsJson, + assetsSelect, + }); + const proceed = useCallback(async () => { + AmpTrackAction(AmpEvent.ACTION_EXECUTE, funds.length, attachFundOption); const stream = await executeTx({ onTxSucceed: (userKey: string, activity: Activity) => { addActivity(userKey, activity); @@ -141,27 +145,33 @@ export const ExecuteArea = ({ estimatedFee: fee, contractAddress, msg: JSON.parse(msg), - funds, + funds: funds(), }); if (stream) { setProcessing(true); broadcast(stream); } - }, [contractAddress, fee, msg, assets, addActivity, executeTx, broadcast]); + }, [ + funds, + executeTx, + fee, + contractAddress, + msg, + attachFundOption, + addActivity, + broadcast, + ]); useEffect(() => setMsg(initialMsg), [initialMsg]); - useEffect(() => setFundMsg(initialFundMsg), [initialFundMsg]); + + const assetsSelectString = JSON.stringify(assetsSelect); useEffect(() => { if (enableExecute) { - setError(undefined); - - const funds = fabricateFunds(assets); - const composedMsg = composeMsg(MsgType.EXECUTE, { sender: address as HumanAddr, contract: contractAddress as ContractAddr, msg: Buffer.from(msg), - funds, + funds: funds(), }); const timeoutId = setTimeout(() => { @@ -170,7 +180,15 @@ export const ExecuteArea = ({ return () => clearTimeout(timeoutId); } return () => {}; - }, [address, contractAddress, enableExecute, msg, assets]); + }, [ + address, + contractAddress, + enableExecute, + msg, + funds, + assetsJson, + assetsSelectString, + ]); useEffect(() => { const keydownHandler = (e: KeyboardEvent) => { @@ -219,9 +237,6 @@ export const ExecuteArea = ({ )} - - Execute Messages - } - - Send Assets - - - - - {attachFundOption === "fill" ? ( - - {fields.map((field, idx) => ( - remove(idx)} - setCurrencyValue={(newVal: string) => - setValue(`assets.${idx}.denom`, newVal) - } - assetOptions={assetOptions} - initialSelected={field.denom} - amountInput={ - /** - * @remarks refactor along with instantiate page - */ - - } - /> - ))} - - - ) : ( - - - - )} + diff --git a/src/lib/pages/execute/index.tsx b/src/lib/pages/execute/index.tsx index b0d50e37d..f227f28f1 100644 --- a/src/lib/pages/execute/index.tsx +++ b/src/lib/pages/execute/index.tsx @@ -1,8 +1,8 @@ import { ChevronRightIcon } from "@chakra-ui/icons"; import { Heading, Button, Box, Flex } from "@chakra-ui/react"; +import type { Coin } from "@cosmjs/stargate"; import { useRouter } from "next/router"; -import { useCallback, useEffect } from "react"; -import { useForm } from "react-hook-form"; +import { useCallback, useEffect, useState } from "react"; import { useExecuteCmds, useInternalNavigate } from "lib/app-provider"; import { BackButton } from "lib/components/button"; @@ -20,28 +20,23 @@ import { } from "lib/utils"; import { ExecuteArea } from "./components/ExecuteArea"; -import type { ExecutePageState } from "./types"; const Execute = () => { const router = useRouter(); const navigate = useInternalNavigate(); - const { control, setValue, watch } = useForm({ - mode: "all", - defaultValues: { - contractAddress: "", - initialMsg: "", - assets: [{ denom: "", amount: "" }], - }, - }); - const watchContractAddress = watch("contractAddress"); + const [initialMsg, setInitialMsg] = useState(""); + const [contractAddress, setContractAddress] = useState(""); + const [initialFunds, setInitialFunds] = useState([]); - const { isFetching, execCmds } = useExecuteCmds(watchContractAddress); + const { isFetching, execCmds } = useExecuteCmds( + contractAddress as ContractAddr + ); const goToQuery = () => { navigate({ pathname: "/query", query: { - ...(watchContractAddress && { contract: watchContractAddress }), + ...(contractAddress && { contract: contractAddress }), }, }); }; @@ -53,30 +48,32 @@ const Execute = () => { query: { ...(contract && { contract }) }, options: { shallow: true }, }); + setInitialMsg(""); + setInitialFunds([]); }, [navigate] ); + const msgParam = getFirstQueryParam(router.query.msg); + useEffect(() => { if (router.isReady) { const contractAddressParam = getFirstQueryParam( router.query.contract ) as ContractAddr; - const msgParam = getFirstQueryParam(router.query.msg); - let decodeMsg = libDecode(msgParam); - if (decodeMsg && jsonValidate(decodeMsg) !== null) { - onContractSelect(contractAddressParam); - decodeMsg = ""; - } - const jsonMsg = jsonPrettify(decodeMsg); + const decodeMsg = libDecode(msgParam); - setValue("contractAddress", contractAddressParam); - setValue("initialMsg", jsonMsg); + if (decodeMsg && !jsonValidate(decodeMsg)) { + const jsonMsg = JSON.parse(decodeMsg); + setInitialMsg(jsonPrettify(JSON.stringify(jsonMsg.msg))); + setInitialFunds(jsonMsg.funds); + } + setContractAddress(contractAddressParam); AmpTrackToExecute(!!contractAddressParam, !!msgParam); } - }, [router, onContractSelect, setValue]); + }, [router, onContractSelect, msgParam]); return ( @@ -103,11 +100,16 @@ const Execute = () => { /> - + ); }; diff --git a/src/lib/pages/execute/types.ts b/src/lib/pages/execute/types.ts deleted file mode 100644 index 2901ec795..000000000 --- a/src/lib/pages/execute/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Coin } from "@cosmjs/stargate"; - -import type { ContractAddr } from "lib/types"; - -export interface ExecutePageState { - contractAddress: ContractAddr; - initialMsg: string; - assets: Coin[]; -} diff --git a/src/lib/pages/instantiate/component/Footer.tsx b/src/lib/pages/instantiate/component/Footer.tsx index 9ff87290d..6ffdd2044 100644 --- a/src/lib/pages/instantiate/component/Footer.tsx +++ b/src/lib/pages/instantiate/component/Footer.tsx @@ -11,14 +11,7 @@ interface FooterProps { export const Footer = ({ onInstantiate, disabled, loading }: FooterProps) => { const router = useRouter(); return ( - + +