diff --git a/CHANGELOG.md b/CHANGELOG.md index edc1cc12d..5e8174370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +- [#416](https://github.com/alleslabs/celatone-frontend/pull/416) Remove the old redundant useSimulateFee hook - [#413](https://github.com/alleslabs/celatone-frontend/pull/413) Add jest test cases for date utils - [#404](https://github.com/alleslabs/celatone-frontend/pull/404) Use internal navigate instead of app link for block navigation - [#396](https://github.com/alleslabs/celatone-frontend/pull/396) Refactor useConfig, disable wasm related tabs on the public project page diff --git a/src/lib/app-fns/tx/instantiate.tsx b/src/lib/app-fns/tx/instantiate.tsx index e0ce180ce..83dda08a4 100644 --- a/src/lib/app-fns/tx/instantiate.tsx +++ b/src/lib/app-fns/tx/instantiate.tsx @@ -23,6 +23,7 @@ interface InstantiateTxParams { funds: Coin[]; client: SigningCosmWasmClient; onTxSucceed?: (txInfo: InstantiateResult, contractLabel: string) => void; + onTxFailed?: () => void; } export const instantiateContractTx = ({ @@ -35,6 +36,7 @@ export const instantiateContractTx = ({ funds, client, onTxSucceed, + onTxFailed, }: InstantiateTxParams): Observable => { return pipe( sendingTx(fee), @@ -51,5 +53,5 @@ export const instantiateContractTx = ({ // TODO: this is type hack return null as unknown as TxResultRendering; } - )().pipe(catchTxError()); + )().pipe(catchTxError(onTxFailed)); }; diff --git a/src/lib/app-provider/hooks/index.ts b/src/lib/app-provider/hooks/index.ts index daffb8fc8..440865dd7 100644 --- a/src/lib/app-provider/hooks/index.ts +++ b/src/lib/app-provider/hooks/index.ts @@ -8,7 +8,6 @@ export * from "./useMediaQuery"; export * from "./useNetworkChange"; export * from "./useRestrictedInput"; export * from "./useSelectChain"; -export * from "./useSimulateFee"; export * from "./useTokensInfo"; export * from "./useChainRecordAsset"; export * from "./useBaseApiRoute"; diff --git a/src/lib/app-provider/hooks/useSimulateFee.ts b/src/lib/app-provider/hooks/useSimulateFee.ts deleted file mode 100644 index 97dbe9d86..000000000 --- a/src/lib/app-provider/hooks/useSimulateFee.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useCallback, useState } from "react"; - -import type { Gas } from "lib/types"; -import type { ComposedMsg } from "lib/types/tx"; - -import { useCurrentChain } from "./useCurrentChain"; - -// TODO: remove this hook after migrating to useQuery version -export const useSimulateFee = () => { - const { address, getSigningCosmWasmClient } = useCurrentChain(); - - const [loading, setLoading] = useState(false); - - const simulate = useCallback( - async (messages: ComposedMsg[], memo?: string) => { - setLoading(true); - const client = await getSigningCosmWasmClient(); - if (!client || !address) { - setLoading(false); - return undefined; - } - try { - const fee = (await client.simulate(address, messages, memo)) as Gas; - setLoading(false); - return fee; - } catch (e) { - setLoading(false); - throw e; - } - }, - [address, getSigningCosmWasmClient, setLoading] - ); - - return { simulate, loading }; -}; diff --git a/src/lib/app-provider/tx/instantiate.ts b/src/lib/app-provider/tx/instantiate.ts index 0acb6a777..ef3935eef 100644 --- a/src/lib/app-provider/tx/instantiate.ts +++ b/src/lib/app-provider/tx/instantiate.ts @@ -6,13 +6,14 @@ import { useCurrentChain } from "../hooks"; import { instantiateContractTx } from "lib/app-fns/tx/instantiate"; export interface InstantiateStreamParams { - onTxSucceed?: (txResult: InstantiateResult, contractLabel: string) => void; estimatedFee: StdFee | undefined; codeId: number; initMsg: object; label: string; admin: string; funds: Coin[]; + onTxSucceed?: (txResult: InstantiateResult, contractLabel: string) => void; + onTxFailed?: () => void; } export const useInstantiateTx = () => { @@ -20,13 +21,14 @@ export const useInstantiateTx = () => { return useCallback( async ({ - onTxSucceed, estimatedFee, codeId, initMsg, label, admin, funds, + onTxSucceed, + onTxFailed, }: InstantiateStreamParams) => { const client = await getSigningCosmWasmClient(); if (!address || !client) @@ -43,6 +45,7 @@ export const useInstantiateTx = () => { funds, client, onTxSucceed, + onTxFailed, }); }, [address, getSigningCosmWasmClient] diff --git a/src/lib/components/button/ResendButton.tsx b/src/lib/components/button/ResendButton.tsx index 111d8e14e..33b28effb 100644 --- a/src/lib/components/button/ResendButton.tsx +++ b/src/lib/components/button/ResendButton.tsx @@ -2,10 +2,14 @@ import { Button } from "@chakra-ui/react"; import type { EncodeObject } from "@cosmjs/proto-signing"; import { useCallback, useState } from "react"; -import { useFabricateFee, useResendTx, useSimulateFee } from "lib/app-provider"; +import { + useFabricateFee, + useResendTx, + useSimulateFeeQuery, +} from "lib/app-provider"; import { useTxBroadcast } from "lib/providers/tx-broadcast"; import { AmpEvent, AmpTrack } from "lib/services/amplitude"; -import type { Message, Msg } from "lib/types"; +import type { Gas, Message, Msg, Option } from "lib/types"; import { camelToSnake, encode } from "lib/utils"; interface ResendButtonProps { @@ -29,27 +33,34 @@ const formatMsgs = (messages: Message[]) => export const ResendButton = ({ messages }: ResendButtonProps) => { const fabricateFee = useFabricateFee(); - const { simulate } = useSimulateFee(); const resendTx = useResendTx(); const { broadcast } = useTxBroadcast(); const [isProcessing, setIsProcessing] = useState(false); + const composedMsgs = formatMsgs(messages); - const proceed = useCallback(async () => { - AmpTrack(AmpEvent.ACTION_RESEND); - const formatedMsgs = formatMsgs(messages); - const estimatedGasUsed = await simulate(formatedMsgs); + const proceed = useCallback( + async (estimatedGasUsed: Option>) => { + AmpTrack(AmpEvent.ACTION_RESEND); + const stream = await resendTx({ + onTxSucceed: () => setIsProcessing(false), + onTxFailed: () => setIsProcessing(false), + estimatedFee: estimatedGasUsed + ? fabricateFee(estimatedGasUsed) + : undefined, + messages: composedMsgs, + }); + if (stream) broadcast(stream); + }, + [broadcast, composedMsgs, fabricateFee, resendTx] + ); - const stream = await resendTx({ - onTxSucceed: () => setIsProcessing(false), - onTxFailed: () => setIsProcessing(false), - estimatedFee: estimatedGasUsed - ? fabricateFee(estimatedGasUsed) - : undefined, - messages: formatedMsgs, - }); - if (stream) broadcast(stream); - }, [broadcast, fabricateFee, messages, resendTx, simulate]); + const { isFetching: isSimulating } = useSimulateFeeQuery({ + enabled: isProcessing, + messages: composedMsgs, + onSuccess: (estimatedGasUsed) => proceed(estimatedGasUsed), + onError: () => setIsProcessing(false), + }); return ( - - - -); diff --git a/src/lib/pages/instantiate/component/index.ts b/src/lib/pages/instantiate/component/index.ts index 1f544d6f6..0420bd267 100644 --- a/src/lib/pages/instantiate/component/index.ts +++ b/src/lib/pages/instantiate/component/index.ts @@ -1,2 +1,2 @@ -export * from "./FailedModal"; export * from "./Footer"; +export * from "./InstantiateOffchainForm"; diff --git a/src/lib/pages/instantiate/instantiate.tsx b/src/lib/pages/instantiate/instantiate.tsx index d00bd1790..4ae5359ec 100644 --- a/src/lib/pages/instantiate/instantiate.tsx +++ b/src/lib/pages/instantiate/instantiate.tsx @@ -1,5 +1,6 @@ -import { Heading, Text } from "@chakra-ui/react"; +import { Flex, Heading, Text } from "@chakra-ui/react"; import type { InstantiateResult } from "@cosmjs/cosmwasm-stargate"; +import type { StdFee } from "@cosmjs/stargate"; import { useQuery } from "@tanstack/react-query"; import Long from "long"; import { useRouter } from "next/router"; @@ -8,21 +9,23 @@ import { useForm } from "react-hook-form"; import { useFabricateFee, - useSimulateFee, useInstantiateTx, useCelatoneApp, useValidateAddress, + useSimulateFeeQuery, useCurrentChain, useBaseApiRoute, } from "lib/app-provider"; import { AssignMe } from "lib/components/AssignMe"; import { ConnectWalletAlert } from "lib/components/ConnectWalletAlert"; +import { EstimatedFeeRender } from "lib/components/EstimatedFeeRender"; import type { FormStatus } from "lib/components/forms"; import { ControllerInput } from "lib/components/forms"; import { AttachFund } from "lib/components/fund"; import { defaultAsset, defaultAssetJsonStr } from "lib/components/fund/data"; import type { AttachFundsState } from "lib/components/fund/types"; import { AttachFundsType } from "lib/components/fund/types"; +import { CustomIcon } from "lib/components/icon"; import JsonInput from "lib/components/json/JsonInput"; import { CodeSelectSection } from "lib/components/select-code"; import { Stepper } from "lib/components/stepper"; @@ -35,7 +38,7 @@ import { AmpTrackToInstantiate, } from "lib/services/amplitude"; import { getCodeIdInfo } from "lib/services/code"; -import type { HumanAddr } from "lib/types"; +import type { ComposedMsg, HumanAddr } from "lib/types"; import { AccessConfigPermission, MsgType } from "lib/types"; import { composeMsg, @@ -45,7 +48,7 @@ import { libDecode, } from "lib/utils"; -import { FailedModal, Footer } from "./component"; +import { Footer } from "./component"; import type { InstantiateRedoMsg } from "./types"; interface InstantiatePageState { @@ -75,7 +78,6 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { const lcdEndpoint = useBaseApiRoute("rest"); const postInstantiateTx = useInstantiateTx(); - const { simulate } = useSimulateFee(); const fabricateFee = useFabricateFee(); const { broadcast } = useTxBroadcast(); const { validateUserAddress, validateContractAddress } = useValidateAddress(); @@ -83,9 +85,11 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { // ------------------------------------------// // ------------------STATES------------------// // ------------------------------------------// - const [simulating, setSimulating] = useState(false); const [status, setStatus] = useState({ state: "init" }); - + const [composedTxMsg, setComposedTxMsg] = useState([]); + const [estimatedFee, setEstimatedFee] = useState(); + const [simulateError, setSimulateError] = useState(""); + const [processing, setProcessing] = useState(false); // ------------------------------------------// // ----------------FORM HOOKS----------------// // ------------------------------------------// @@ -94,7 +98,6 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { formState: { errors: formErrors }, setValue, watch, - handleSubmit, } = useForm({ mode: "all", defaultValues: { @@ -102,16 +105,9 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { label: "", adminAddress: "", initMsg: "", - simulateError: "", }, }); - - const { - codeId, - adminAddress: watchAdminAddress, - initMsg: watchInitMsg, - simulateError, - } = watch(); + const { codeId, label, adminAddress, initMsg } = watch(); const { control: assetsControl, @@ -125,20 +121,16 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { attachFundsOption: AttachFundsType.ATTACH_FUNDS_NULL, }, }); - const { assetsSelect, assetsJsonStr, attachFundsOption } = watchAssets(); - const disableInstantiate = useMemo(() => { - return ( - !codeId || - !address || - !!jsonValidate(watchInitMsg) || - Object.keys(formErrors).length > 0 || - status.state !== "success" - ); - // formErrors change doesnt trigger this effect - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [codeId, address, watchInitMsg, Object.keys(formErrors), status.state]); + const enableInstantiate = useMemo( + () => + !!address && + codeId.length > 0 && + jsonValidate(initMsg) === null && + status.state === "success", + [address, codeId.length, initMsg, status.state] + ); const funds = getAttachFunds({ attachFundsOption, @@ -146,6 +138,19 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { assetsSelect, }); + 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( ["query", lcdEndpoint, codeId], async () => getCodeIdInfo(lcdEndpoint, codeId), @@ -179,51 +184,37 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { // ------------------------------------------// // ----------------FUNCTIONS-----------------// // ------------------------------------------// - const proceed = useCallback(() => { - handleSubmit(async ({ adminAddress, label, initMsg }) => { - AmpTrackAction(AmpEvent.ACTION_EXECUTE, funds.length, attachFundsOption); - - setSimulating(true); - - const msg = composeMsg(MsgType.INSTANTIATE, { - sender: address as HumanAddr, - admin: adminAddress as HumanAddr, - codeId: Long.fromString(codeId), - label, - msg: Buffer.from(initMsg), - funds, - }); - try { - const estimatedFee = await simulate([msg]); - const stream = await postInstantiateTx({ - estimatedFee: estimatedFee ? fabricateFee(estimatedFee) : undefined, - codeId: Number(codeId), - initMsg: JSON.parse(initMsg), - label, - admin: adminAddress, - funds, - onTxSucceed: onComplete, - }); + const proceed = useCallback(async () => { + AmpTrackAction(AmpEvent.ACTION_EXECUTE, funds.length, attachFundsOption); + const stream = await postInstantiateTx({ + codeId: Number(codeId), + initMsg: JSON.parse(initMsg), + label, + admin: adminAddress, + funds, + estimatedFee, + onTxSucceed: (txResult, contractLabel) => { + setProcessing(false); + onComplete(txResult, contractLabel); + }, + onTxFailed: () => setProcessing(false), + }); - if (stream) broadcast(stream); - setSimulating(false); - } catch (e) { - setValue("simulateError", (e as Error).message); - setSimulating(false); - } - })(); + if (stream) { + setProcessing(true); + broadcast(stream); + } }, [ - funds, - handleSubmit, + adminAddress, attachFundsOption, - address, + broadcast, codeId, - simulate, - postInstantiateTx, - fabricateFee, + estimatedFee, + funds, + initMsg, + label, onComplete, - broadcast, - setValue, + postInstantiateTx, ]); // ------------------------------------------// @@ -242,6 +233,35 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { return () => {}; }, [address, codeId, refetch]); + useEffect(() => { + if (enableInstantiate) { + setSimulateError(""); + const composedMsg = composeMsg(MsgType.INSTANTIATE, { + sender: address as HumanAddr, + admin: adminAddress as HumanAddr, + codeId: Long.fromString(codeId), + label, + msg: Buffer.from(initMsg), + funds, + }); + const timeoutId = setTimeout(() => { + setComposedTxMsg([composedMsg]); + }, 1000); + return () => clearTimeout(timeoutId); + } + return () => {}; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + address, + adminAddress, + codeId, + enableInstantiate, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(funds), + initMsg, + label, + ]); + useEffect(() => { if (codeIdQuery) setValue("codeId", codeIdQuery); if (msgQuery) { @@ -321,14 +341,14 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { placeholder={`ex. ${exampleContractAddress}`} helperText="The contract's admin will be able to migrate and update future admins." variant="floating" - error={validateAdmin(watchAdminAddress)} + error={validateAdmin(adminAddress)} helperAction={ { AmpTrack(AmpEvent.USE_ASSIGN_ME); setValue("adminAddress", address); }} - isDisable={watchAdminAddress === address} + isDisable={adminAddress === address} /> } /> @@ -336,7 +356,7 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { Instantiate Message setValue("initMsg", newVal)} minLines={10} /> @@ -349,18 +369,38 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { attachFundsOption={attachFundsOption} /> + {simulateError && ( + + + + {simulateError} + + + )} + +

Transaction Fee:

+ +