diff --git a/src/components/Toast/Toast.utils.ts b/src/components/Toast/Toast.utils.ts index e84a20c80..b6cdaeec0 100644 --- a/src/components/Toast/Toast.utils.ts +++ b/src/components/Toast/Toast.utils.ts @@ -4,19 +4,21 @@ import { QUERY_KEYS } from "utils/queryKeys" import { Buffer } from "buffer" import { keccak256 } from "ethers/lib/utils" import { omit } from "utils/rx" -import { - differenceInHours, - differenceInMinutes, - differenceInSeconds, -} from "date-fns" +import { differenceInHours, differenceInMinutes } from "date-fns" import { useRpcProvider } from "providers/rpcProvider" import request, { gql } from "graphql-request" import { useIndexerUrl } from "api/provider" -import { Parachain, SubstrateApis } from "@galacticcouncil/xcm-core" +import { + EvmParachain, + Parachain, + SubstrateApis, +} from "@galacticcouncil/xcm-core" import { chainsMap } from "@galacticcouncil/xcm-cfg" const moonbeamRpc = (chainsMap.get("moonbeam") as Parachain).ws -//const txInfoSubscan = "https://hydration.api.subscan.io/api/scan/extrinsic" +const getSubscanEndpoint = (network: string, method: string) => { + return `https://${network}.api.subscan.io/api/scan/${method}` +} type TExtrinsic = { hash: string @@ -105,6 +107,34 @@ const getExtrinsic = async (indexerUrl: string, hash: string) => { } } +const getEvmExtrinsic = async ( + indexerUrl: string, + blockNumber: number, + index: number, +) => { + return { + ...(await request<{ + extrinsics: TSuccessExtrinsic[] + }>( + indexerUrl, + gql` + query GetEvmExtrinsic($blockNumber: Int, $index: Int) { + extrinsics( + where: { + block: { height_eq: $blockNumber } + indexInBlock_eq: $index + } + ) { + success + error + } + } + `, + { blockNumber, index }, + )), + } +} + const getExtrinsicIndex = ( { extrinsics }: { extrinsics: TExtrinsic[] }, i?: number, @@ -169,63 +199,171 @@ const getWormholeTx = async (extrinsicIndex: string) => { } } +const extractKeyFromURL = (url: string, isEvm: boolean) => { + if (isEvm) { + const origin = new URL(url)?.origin + + const chain = [...chainsMap.values()].find((chain) => { + if (chain.isEvmParachain()) { + return ( + (chain as EvmParachain).client.chain.blockExplorers?.default.url === + origin + ) + } + + return false + }) + + return chain?.key + } + + const match = url.match(/^https?:\/\/([^.]+)\.subscan/) + return match ? match[1] : null +} + export const useProcessToasts = (toasts: ToastData[]) => { const indexerUrl = useIndexerUrl() const toast = useToast() + const { api, isLoaded } = useRpcProvider() useQueries({ queries: toasts.map((toastData) => ({ queryKey: QUERY_KEYS.progressToast(toastData.id), queryFn: async () => { - const secondsDiff = differenceInSeconds( + const hoursDiff = differenceInHours( new Date(), new Date(toastData.dateCreated), ) - // skip processing - if (secondsDiff < 60) return false - - const hoursDiff = differenceInHours( + const minutesDiff = differenceInMinutes( new Date(), new Date(toastData.dateCreated), ) + const isHiddenToast = minutesDiff > 10 + // move to unknown state if (hoursDiff >= 1 || !toastData.txHash?.length) { toast.remove(toastData.id) - toast.add("unknown", toastData) + toast.add("unknown", { ...toastData, hidden: true }) return false } - const res = await getExtrinsic(indexerUrl, toastData.txHash as string) - const isExtrinsic = !!res.extrinsics.length + if (toastData.xcm) { + const link = toastData.link - if (isExtrinsic) { - const isSuccess = res.extrinsics[0].success + if (link) { + const network = extractKeyFromURL(link, toastData.xcm === "evm") - // use subscan to get extrinsic info - // const txInfoRes = await fetch(txInfoSubscan, { - // method: "POST", - // body: JSON.stringify({ hash: toastData.txHash }), - // }) + if (network) { + const isSubstrate = toastData.xcm === "substrate" + const endpoint = getSubscanEndpoint( + network, + isSubstrate ? "extrinsic" : "evm/transaction", + ) - // const data: { data: { success: boolean } } = await txInfoRes.json() + const body = JSON.stringify({ + hash: toastData.txHash, + events_limit: 1, + }) - toast.remove(toastData.id) + const subscanRes = await fetch(endpoint, { + method: "POST", + body, + }) - if (isSuccess) { - toast.add("success", toastData) - } else { - toast.add("error", toastData) + const resData = await subscanRes.json() + + const tx = resData + const isFinalized = isSubstrate + ? !!tx.data?.finalized + : tx.data.success || tx.data["error_type"].length + + if (tx?.data && isFinalized) { + toast.remove(toastData.id) + + if (tx.data.success) { + toast.add("success", { ...toastData, hidden: isHiddenToast }) + } else { + toast.add("error", { ...toastData, hidden: isHiddenToast }) + } + + return true + } + + if (minutesDiff >= 5) { + toast.remove(toastData.id) + toast.add("unknown", { ...toastData, hidden: isHiddenToast }) + + return false + } + } } - return true + return false + } else { + const isEvm = + toastData.link?.includes("evm") || + toastData.link?.includes("explorer.nice.hydration.cloud") + + if (isEvm) { + const ethTx = await api.rpc.eth.getTransactionByHash( + toastData.txHash, + ) + + if (ethTx) { + const blockNumber = ethTx.blockNumber.toString() + const indexInBlock = ethTx.transactionIndex.toString() + + const { extrinsics } = await getEvmExtrinsic( + indexerUrl, + Number(blockNumber), + Number(indexInBlock), + ) + + if (!!extrinsics.length) { + const isSuccess = extrinsics[0].success + toast.remove(toastData.id) + + if (isSuccess) { + toast.add("success", { ...toastData, hidden: isHiddenToast }) + } else { + toast.add("error", { ...toastData, hidden: isHiddenToast }) + } + + return true + } + } + + return false + } else { + const res = await getExtrinsic( + indexerUrl, + toastData.txHash as string, + ) + + const isExtrinsic = !!res.extrinsics.length + + if (isExtrinsic) { + const isSuccess = res.extrinsics[0].success + + toast.remove(toastData.id) + + if (isSuccess) { + toast.add("success", { ...toastData, hidden: isHiddenToast }) + } else { + toast.add("error", { ...toastData, hidden: isHiddenToast }) + } + + return true + } + } } return false }, - refetchInterval: 10000, + enabled: isLoaded, })), }) } diff --git a/src/components/Toast/ToastProvider.tsx b/src/components/Toast/ToastProvider.tsx index cea43de44..13f2ab60f 100644 --- a/src/components/Toast/ToastProvider.tsx +++ b/src/components/Toast/ToastProvider.tsx @@ -7,16 +7,22 @@ import { AnimatePresence, domAnimation, LazyMotion } from "framer-motion" import { ToastSidebar } from "./sidebar/ToastSidebar" import { useBridgeToast, useProcessToasts } from "./Toast.utils" +import { useStore } from "state/store" export const ToastProvider: FC = ({ children }) => { const { toasts, toastsTemp, hide, sidebar, setSidebar } = useToast() + const { transactions } = useStore() const bridgeToasts = toasts.filter( (toast) => toast.bridge && toast.variant === "progress", ) const progressToasts = toasts.filter((toast) => { - return !toast.bridge && toast.variant === "progress" + return ( + !toast.bridge && + toast.variant === "progress" && + !transactions?.find((transaction) => transaction.id === toast.id) + ) }) useBridgeToast(bridgeToasts) diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index 5c6b83549..272c7ed03 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -358,10 +358,6 @@ "liquidity.reviewTransaction.modal.error.copy": "Copy error message", "liquidity.reviewTransaction.modal.error.copied": "Message copied!", "liquidity.reviewTransaction.modal.error.tip": "Not enough HDX for TIP", - "liquidity.reviewTransaction.toast.pending": "Submitting transaction...", - "liquidity.reviewTransaction.toast.success": "Transaction successful.", - "liquidity.reviewTransaction.toast.error": "Transaction failed.", - "liquidity.reviewTransaction.toast.unknown": "Transaction status unknown.", "liquidity.reviewTransaction.calldata.encoded": "Encoded call data", "liquidity.reviewTransaction.calldata.encodedHash": "Encoded call hash", "liquidity.reviewTransaction.calldata.decoded": "Decoded", @@ -1234,5 +1230,9 @@ "claimingRange.modal.warning.title": "Are you sure?", "claimingRange.modal.warning.description": " Don't claim rewards too early. Claiming forfeits any non-claimable rewards to the other LPs. Before claiming, make sure that your claim threshold is configured accordingly.", "claimingRange.modal.description1": "After joining a farm, a portion of the accumulated rewards is locked. These rewards unlock over time as you remain in the farm and follow loyalty factor curve", - "claimingRange.modal.description2": "Claiming forfeits the locked part of the rewards. Use this setting to tweak your preference over claiming faster or losing less rewards (default). This allows you to compound your rewards with ease." + "claimingRange.modal.description2": "Claiming forfeits the locked part of the rewards. Use this setting to tweak your preference over claiming faster or losing less rewards (default). This allows you to compound your rewards with ease.", + "toast.pending": "Submitting transaction...", + "toast.success": "Transaction successful.", + "toast.error": "Transaction failed.", + "toast.unknown": "Transaction status unknown." } diff --git a/src/sections/transaction/ReviewTransaction.tsx b/src/sections/transaction/ReviewTransaction.tsx index 3607e0af7..772eeec8c 100644 --- a/src/sections/transaction/ReviewTransaction.tsx +++ b/src/sections/transaction/ReviewTransaction.tsx @@ -8,7 +8,6 @@ import { ReviewTransactionError } from "./ReviewTransactionError" import { ReviewTransactionForm } from "./ReviewTransactionForm" import { ReviewTransactionPending } from "./ReviewTransactionPending" import { ReviewTransactionSuccess } from "./ReviewTransactionSuccess" -import { ReviewTransactionToast } from "./ReviewTransactionToast" import { ReviewTransactionXCallForm } from "./ReviewTransactionXCallForm" import { ReviewTransactionEvmTxForm } from "sections/transaction/ReviewTransactionEvmTxForm" import { WalletUpgradeModal } from "sections/web3-connect/upgrade/WalletUpgradeModal" @@ -29,13 +28,15 @@ export const ReviewTransaction = (props: Transaction) => { isSuccess, isError: isSendError, error: sendError, - data, - txState, + isBroadcasted, reset, - txLink, - txHash, - bridge, - } = useSendTx(props.xcallMeta) + } = useSendTx({ + id: props.id, + toast: props.toast, + onSuccess: (data) => props.onSuccess?.(data), + onError: props.onError, + xcallMeta: props.xcallMeta, + }) if (!isLoaded) return null @@ -43,7 +44,7 @@ export const ReviewTransaction = (props: Transaction) => { const error = sendError || signError const modalProps: Partial> = - isLoading || isSuccess || isError + (isLoading && isBroadcasted) || isSuccess || isError ? { title: undefined, backdrop: isError ? "error" : "default", @@ -55,32 +56,19 @@ export const ReviewTransaction = (props: Transaction) => { props.description ?? t("liquidity.reviewTransaction.modal.desc"), } - const handleTxOnClose = () => { - if (isLoading) { - setMinimizeModal(true) - return - } - - if (isSuccess) { - props.onSuccess?.(data) - } else { - props.onError?.() - } - } - const onClose = () => { - handleTxOnClose() + setMinimizeModal(true) props.onClose?.() } const onMinimizeModal = () => { - handleTxOnClose() + setMinimizeModal(true) if (!props.disableAutoClose) props.onClose?.() } const onBack = props.onBack ? () => { - handleTxOnClose() + setMinimizeModal(true) props.onBack?.() } : undefined @@ -92,22 +80,6 @@ export const ReviewTransaction = (props: Transaction) => { return ( <> - {minimizeModal && ( - - )} - { } {...modalProps} > - {isLoading ? ( - + {isLoading && isBroadcasted ? ( + ) : isSuccess ? ( ) : isError ? ( @@ -154,6 +123,7 @@ export const ReviewTransaction = (props: Transaction) => { sendPermitTx(permit) }} onSignError={setSignError} + isLoading={isLoading} /> ) : props.evmTx ? ( { sendEvmTx(data) }} onSignError={setSignError} + isLoading={isLoading} /> ) : null} diff --git a/src/sections/transaction/ReviewTransaction.utils.tsx b/src/sections/transaction/ReviewTransaction.utils.tsx index a00817580..9d484eaf1 100644 --- a/src/sections/transaction/ReviewTransaction.utils.tsx +++ b/src/sections/transaction/ReviewTransaction.utils.tsx @@ -3,7 +3,7 @@ import { TransactionResponse, Web3Provider, } from "@ethersproject/providers" -import { chainsMap } from "@galacticcouncil/xcm-cfg" +import { chainsMap, tags } from "@galacticcouncil/xcm-cfg" import { AccountId32, Hash } from "@open-web3/orml-types/interfaces" import { ApiPromise } from "@polkadot/api" import { SubmittableExtrinsic } from "@polkadot/api/types" @@ -19,9 +19,8 @@ import { import { useAssets } from "providers/assets" import { useShallow } from "hooks/useShallow" import { useRpcProvider } from "providers/rpcProvider" -import { useCallback, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" -import { useMountedState } from "react-use" import { useUserExternalTokenStore } from "sections/wallet/addToken/AddToken.utils" import { useEvmAccount, @@ -31,7 +30,7 @@ import { EthereumSigner, PermitResult, } from "sections/web3-connect/signer/EthereumSigner" -import { useSettingsStore } from "state/store" +import { ToastMessage, useSettingsStore } from "state/store" import { useToast } from "state/toasts" import { H160, @@ -44,7 +43,7 @@ import { isAnyParachain, Maybe } from "utils/helpers" import { createSubscanLink } from "utils/formatting" import { QUERY_KEYS } from "utils/queryKeys" import { useIsTestnet } from "api/provider" -import { tags } from "@galacticcouncil/xcm-cfg" +import { useMountedState } from "react-use" const EVM_PERMIT_BLOCKTIME = 20_000 @@ -196,26 +195,76 @@ export const useSendEvmTransactionMutation = ( tx?: SubmittableExtrinsic<"promise"> } > = {}, + id: string, + toast?: ToastMessage, xcallMeta?: Record, ) => { - const [txState, setTxState] = useState(null) - const [txHash, setTxHash] = useState("") - const [txData, setTxData] = useState() + const { t } = useTranslation() + const { loading, success, error, remove, sidebar } = useToast() + const [isBroadcasted, setIsBroadcasted] = useState(false) const { account } = useEvmAccount() const isTestnet = useIsTestnet() + const isMounted = useMountedState() + const sendTx = useMutation(async ({ evmTx }) => { return await new Promise(async (resolve, reject) => { try { - setTxState("Broadcast") - setTxHash(evmTx?.hash ?? "") - setTxData(evmTx?.data) + const txHash = evmTx?.hash + const txData = evmTx?.data + + const isSnowBridge = xcallMeta?.tags === tags.Tag.Snowbridge + const chain = account?.chainId ? getEvmChainById(account.chainId) : null + const link = + txHash && chain + ? getEvmTxLink(txHash, txData, chain.key, isTestnet, isSnowBridge) + : "" + + const isApproveTx = txData?.startsWith("0x095ea7b3") + + const destChain = xcallMeta?.dstChain + ? chainsMap.get(xcallMeta.dstChain) + : undefined + + const xcm = xcallMeta ? "evm" : undefined + + const bridge = + chain?.isEvmChain() || destChain?.isEvmChain() + ? chain?.key + : undefined + + loading({ + id, + title: toast?.onLoading ??

{t("toast.pending")}

, + link, + txHash, + bridge: isApproveTx || isSnowBridge ? undefined : bridge, + hidden: true, + xcm, + }) + + setIsBroadcasted(true) + const receipt = await evmTx.wait() - setTxState("InBlock") + + if (isMounted() && !xcm) { + success({ + title: toast?.onSuccess ??

{t("toast.success")}

, + link, + txHash, + hidden: sidebar, + }) + + remove(id) + } return resolve(evmTxReceiptToSubmittableResult(receipt)) } catch (err) { + error({ + title: toast?.onSuccess ??

{t("toast.success")}

, + hidden: sidebar, + }) reject( new TransactionError(err?.toString() ?? "Unknown error", { from: evmTx.from, @@ -229,33 +278,9 @@ export const useSendEvmTransactionMutation = ( }) }, options) - const isSnowBridge = xcallMeta?.tags === tags.Tag.Snowbridge - const chain = account?.chainId ? getEvmChainById(account.chainId) : null - const txLink = - txHash && chain - ? getEvmTxLink(txHash, txData, chain.key, isTestnet, isSnowBridge) - : "" - - const isApproveTx = txData?.startsWith("0x095ea7b3") - - const destChain = xcallMeta?.dstChain - ? chainsMap.get(xcallMeta.dstChain) - : undefined - - const bridge = - chain?.isEvmChain() || destChain?.isEvmChain() ? chain?.key : undefined - return { ...sendTx, - txState, - txLink, - txHash, - bridge: isApproveTx || isSnowBridge ? undefined : bridge, - reset: () => { - setTxState(null) - setTxHash("") - sendTx.reset() - }, + isBroadcasted, } } @@ -318,6 +343,42 @@ export const usePendingDispatchPermit = ( ) } +const getTransactionData = ( + result: ISubmittableResult, + xcallMeta?: Record, +) => { + const status = result.status + const txHash = result.txHash.toHex() + + const srcChain = chainsMap.get(xcallMeta?.srcChain ?? "hydration") + + const xcmDstChain = xcallMeta?.dstChain + ? chainsMap.get(xcallMeta.dstChain) + : undefined + + const link = + txHash && srcChain + ? createSubscanLink("extrinsic", txHash, srcChain.key) + : undefined + + const isSnowBridge = xcallMeta?.tags === tags.Tag.Snowbridge + + const bridge = + xcmDstChain?.isEvmChain() && !isSnowBridge ? "substrate" : undefined + + const xcm: "substrate" | undefined = xcallMeta ? "substrate" : undefined + + return { + status, + txHash, + srcChain, + xcmDstChain, + link, + bridge, + xcm, + } +} + export const useSendDispatchPermit = ( options: MutationObserverOptions< ISubmittableResult, @@ -326,19 +387,24 @@ export const useSendDispatchPermit = ( permit: PermitResult } > = {}, + id: string, + toast?: ToastMessage, xcallMeta?: Record, ) => { const { api } = useRpcProvider() const { wallet } = useWallet() + const { t } = useTranslation() + const { loading, success, error, remove, sidebar } = useToast() const queryClient = useQueryClient() - const [txState, setTxState] = useState(null) - const [txHash, setTxHash] = useState("") + const [isBroadcasted, setIsBroadcasted] = useState(false) - const isMounted = useMountedState() + const unsubscribeRef = useRef void)>(null) const sendTx = useMutation(async ({ permit }) => { return await new Promise(async (resolve, reject) => { try { + let isLoadingNotified = false + const extrinsic = api.tx.multiTransactionPayment.dispatchPermit( permit.message.from, permit.message.to, @@ -355,9 +421,24 @@ export const useSendDispatchPermit = ( const isInBlock = result.status.type === "InBlock" - if (isMounted()) { - setTxHash(result.txHash.toHex()) - setTxState(result.status.type) + const { status, txHash, link, bridge, xcm } = getTransactionData( + result, + xcallMeta, + ) + + if (status.isBroadcast && txHash && !isLoadingNotified) { + loading({ + id, + title: toast?.onLoading ??

{t("toast.pending")}

, + link, + txHash, + bridge, + hidden: true, + xcm, + }) + + isLoadingNotified = true + setIsBroadcasted(true) } const account = new H160(permit.message.from).toAccount() @@ -380,10 +461,30 @@ export const useSendDispatchPermit = ( }) const onComplete = createResultOnCompleteHandler(api, { - onError: async (error) => { - reject(error) + onError: async (e) => { + error({ + title: toast?.onError ??

{t("toast.error")}

, + link, + txHash, + hidden: sidebar, + }) + + remove(id) + + reject(e) }, onSuccess: async (result) => { + if (!xcm) { + success({ + title: toast?.onSuccess ??

{t("toast.success")}

, + link, + txHash, + hidden: sidebar, + }) + + remove(id) + } + resolve(result) }, onSettled: async () => { @@ -404,9 +505,15 @@ export const useSendDispatchPermit = ( }, }) + unsubscribeRef.current = unsubscribe + return onComplete(result) }) } catch (err) { + error({ + title: toast?.onSuccess ??

{t("toast.success")}

, + hidden: sidebar, + }) reject( new TransactionError( err?.toString() ?? "Unknown error", @@ -417,33 +524,15 @@ export const useSendDispatchPermit = ( }) }, options) - const isSnowBridge = xcallMeta?.tags === tags.Tag.Snowbridge - - const destChain = xcallMeta?.dstChain - ? chainsMap.get(xcallMeta.dstChain) - : undefined - - const srcChain = chainsMap.get(xcallMeta?.srcChain ?? "hydration") - - const txLink = - txHash && srcChain - ? createSubscanLink("extrinsic", txHash, srcChain.key) - : undefined - - const bridge = - destChain?.isEvmChain() && !isSnowBridge ? "substrate" : undefined + useEffect(() => { + return () => { + unsubscribeRef.current?.() + } + }, []) return { ...sendTx, - txState, - txLink, - txHash, - bridge, - reset: () => { - setTxState(null) - setTxHash("") - sendTx.reset() - }, + isBroadcasted, } } @@ -455,47 +544,90 @@ export const useSendTransactionMutation = ( tx: SubmittableExtrinsic<"promise"> } > = {}, + id: string, + toast?: ToastMessage, xcallMeta?: Record, ) => { const { api } = useRpcProvider() - const isMounted = useMountedState() - const [txState, setTxState] = useState(null) - const [txHash, setTxHash] = useState("") + const { t } = useTranslation() + const { loading, success, error, remove, sidebar } = useToast() + const [isBroadcasted, setIsBroadcasted] = useState(false) + + const unsubscribeRef = useRef void)>(null) const sendTx = useMutation(async ({ tx }) => { return await new Promise(async (resolve, reject) => { try { + let isLoadingNotified = false + const unsubscribe = await tx.send(async (result) => { if (!result || !result.status) return - if (isMounted()) { - setTxHash(result.txHash.toHex()) - setTxState(result.status.type) + const { status, txHash, srcChain, link, bridge, xcm } = + getTransactionData(result, xcallMeta) + + if (status.isBroadcast && txHash && !isLoadingNotified) { + loading({ + id, + title: toast?.onLoading ??

{t("toast.pending")}

, + link, + txHash, + bridge, + hidden: true, + xcm, + }) + + isLoadingNotified = true + setIsBroadcasted(true) } - const externalChain = - xcallMeta?.srcChain && xcallMeta.srcChain !== "hydration" - ? chainsMap.get(xcallMeta?.srcChain) - : null - const apiPromise = - externalChain && isAnyParachain(externalChain) - ? await externalChain.api + xcallMeta && + srcChain && + xcallMeta.srcChain !== "hydration" && + isAnyParachain(srcChain) + ? await srcChain.api : api const onComplete = createResultOnCompleteHandler(apiPromise, { - onError: (error) => { - reject(error) + onError: (e) => { + error({ + title: toast?.onError ??

{t("toast.error")}

, + link, + txHash, + hidden: sidebar, + }) + + remove(id) + + reject(e) }, onSuccess: (result) => { + if (!xcm) { + success({ + title: toast?.onSuccess ??

{t("toast.success")}

, + link, + txHash, + hidden: sidebar, + }) + + remove(id) + } + resolve(result) }, onSettled: unsubscribe, }) + unsubscribeRef.current = unsubscribe + return onComplete(result) }) } catch (err) { + error({ + title: toast?.onSuccess ??

{t("toast.success")}

, + hidden: sidebar, + }) reject( new TransactionError(err?.toString() ?? "Unknown error", { method: getTransactionJSON(tx)?.method, @@ -507,33 +639,15 @@ export const useSendTransactionMutation = ( }) }, options) - const isSnowBridge = xcallMeta?.tags === tags.Tag.Snowbridge - - const destChain = xcallMeta?.dstChain - ? chainsMap.get(xcallMeta.dstChain) - : undefined - - const srcChain = chainsMap.get(xcallMeta?.srcChain ?? "hydration") - - const txLink = - txHash && srcChain - ? createSubscanLink("extrinsic", txHash, srcChain.key) - : undefined - - const bridge = - destChain?.isEvmChain() && !isSnowBridge ? "substrate" : undefined + useEffect(() => { + return () => { + unsubscribeRef.current?.() + } + }, []) return { ...sendTx, - txState, - txLink, - txHash, - bridge, - reset: () => { - setTxState(null) - setTxHash("") - sendTx.reset() - }, + isBroadcasted, } } @@ -652,7 +766,19 @@ const useStoreExternalAssetsOnSign = () => { ) } -export const useSendTx = (xcallMeta?: Record) => { +export const useSendTx = ({ + id, + toast, + onSuccess, + onError, + xcallMeta, +}: { + id: string + toast?: ToastMessage + onSuccess?: (data: ISubmittableResult) => void + onError?: () => void + xcallMeta?: Record +}) => { const [txType, setTxType] = useState<"default" | "evm" | "permit" | null>( null, ) @@ -667,8 +793,14 @@ export const useSendTx = (xcallMeta?: Record) => { storeExternalAssetsOnSign(getAssetIdsFromTx(tx)) setTxType("default") }, - onSuccess: boundReferralToast.onSuccess, + onSuccess: (data) => { + boundReferralToast.onSuccess() + onSuccess?.(data) + }, + onError: () => onError?.(), }, + id, + toast, xcallMeta, ) @@ -681,8 +813,14 @@ export const useSendTx = (xcallMeta?: Record) => { } setTxType("evm") }, - onSuccess: boundReferralToast.onSuccess, + onSuccess: (data) => { + boundReferralToast.onSuccess() + onSuccess?.(data) + }, + onError: () => onError?.(), }, + id, + toast, xcallMeta, ) @@ -691,7 +829,11 @@ export const useSendTx = (xcallMeta?: Record) => { onMutate: () => { setTxType("permit") }, + onSuccess: (data) => onSuccess?.(data), + onError: () => onError?.(), }, + id, + toast, xcallMeta, ) diff --git a/src/sections/transaction/ReviewTransactionForm.tsx b/src/sections/transaction/ReviewTransactionForm.tsx index d718b1e38..56e1310a3 100644 --- a/src/sections/transaction/ReviewTransactionForm.tsx +++ b/src/sections/transaction/ReviewTransactionForm.tsx @@ -1,7 +1,7 @@ import { TransactionResponse } from "@ethersproject/providers" import { FC, useState } from "react" import { SubmittableExtrinsic } from "@polkadot/api/types" -import { useMutation } from "@tanstack/react-query" +import { useMutation, useQueryClient } from "@tanstack/react-query" import { Button } from "components/Button/Button" import { ModalScrollableContent } from "components/Modal/Modal" import { Text } from "components/Typography/Text/Text" @@ -40,6 +40,7 @@ import { WalletMode, } from "sections/web3-connect/store/useWeb3ConnectStore" import { BN_0 } from "utils/constants" +import { QUERY_KEYS } from "utils/queryKeys" type TxProps = Omit & { tx: SubmittableExtrinsic<"promise"> @@ -54,6 +55,7 @@ type Props = TxProps & { }) => void onSigned: (signed: SubmittableExtrinsic<"promise">) => void onSignError?: (error: unknown) => void + isLoading: boolean } export const ReviewTransactionForm: FC = (props) => { @@ -61,6 +63,7 @@ export const ReviewTransactionForm: FC = (props) => { const { account } = useAccount() const { setReferralCode } = useReferralCodesStore() const { toggle: toggleWeb3Modal } = useWeb3ConnectStore() + const queryClient = useQueryClient() const polkadotJSUrl = usePolkadotJSTxUrl(props.tx) @@ -137,6 +140,13 @@ export const ReviewTransactionForm: FC = (props) => { const evmTx = await wallet.signer.sendDispatch( txData, props.xcallMeta?.srcChain, + { + onNetworkSwitch: () => { + queryClient.refetchQueries( + QUERY_KEYS.evmChainInfo(account?.displayAddress ?? ""), + ) + }, + }, ) return props.onEvmSigned({ evmTx, tx }) } @@ -176,7 +186,10 @@ export const ReviewTransactionForm: FC = (props) => { wallet?.signer instanceof EthereumSigner ? evmWalletReady : true const isLoading = - transactionValues.isLoading || signTx.isLoading || isChangingFeePaymentAsset + transactionValues.isLoading || + signTx.isLoading || + isChangingFeePaymentAsset || + props.isLoading const hasMultipleFeeAssets = props.xcallMeta && props.xcallMeta?.srcChain !== HYDRATION_CHAIN_KEY ? false diff --git a/src/sections/transaction/ReviewTransactionPending.tsx b/src/sections/transaction/ReviewTransactionPending.tsx index 4a06ddbd0..2b57bcd17 100644 --- a/src/sections/transaction/ReviewTransactionPending.tsx +++ b/src/sections/transaction/ReviewTransactionPending.tsx @@ -1,4 +1,3 @@ -import { ExtrinsicStatus } from "@polkadot/types/interfaces/author" import { Button } from "components/Button/Button" import { Spacer } from "components/Spacer/Spacer" import { Heading } from "components/Typography/Heading/Heading" @@ -9,10 +8,9 @@ import { Spinner } from "components/Spinner/Spinner" type Props = { onClose: () => void - txState: ExtrinsicStatus["type"] | null } -export const ReviewTransactionPending = ({ onClose, txState }: Props) => { +export const ReviewTransactionPending = ({ onClose }: Props) => { const { t } = useTranslation() return (
{ - {txState === "Broadcast" && ( - - )} +
) } diff --git a/src/sections/transaction/ReviewTransactionToast.tsx b/src/sections/transaction/ReviewTransactionToast.tsx deleted file mode 100644 index 3f41f6c13..000000000 --- a/src/sections/transaction/ReviewTransactionToast.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useEffect, useRef } from "react" -import { useTranslation } from "react-i18next" -import { useToast } from "state/toasts" -import { ToastMessage } from "state/store" - -export function ReviewTransactionToast(props: { - id: string - link?: string - onReview?: () => void - onClose?: () => void - toastMessage?: ToastMessage - isError: boolean - isSuccess: boolean - isLoading: boolean - error: unknown - bridge: string | undefined - txHash: string -}) { - const toast = useToast() - const { t } = useTranslation() - - const { isError, isSuccess, isLoading, error, txHash } = props - const toastRef = useRef(toast) - useEffect(() => void (toastRef.current = toast), [toast]) - - const reviewRef = useRef<(typeof props)["onReview"]>(props.onReview) - useEffect(() => void (reviewRef.current = props.onReview), [props.onReview]) - - const closeRef = useRef<(typeof props)["onClose"]>(props.onClose) - useEffect(() => { - closeRef.current = props.onClose - }, [props.onClose, props.id]) - - useEffect(() => { - if (isSuccess && !props.bridge) { - // toast should be still present, even if ReviewTransaction is unmounted - toastRef.current.success({ - title: props.toastMessage?.onSuccess ?? ( -

{t("liquidity.reviewTransaction.toast.success")}

- ), - link: props.link, - txHash, - }) - - closeRef.current?.() - } - - let toRemoveId: string | undefined = undefined - - if (isError) { - toastRef.current.error({ - link: props.link, - title: props.toastMessage?.onError ?? ( -

{t("liquidity.reviewTransaction.toast.error")}

- ), - txHash, - }) - - closeRef.current?.() - } - - if (isLoading) { - toRemoveId = toastRef.current.loading({ - link: props.link, - title: props.toastMessage?.onLoading ?? ( -

{t("liquidity.reviewTransaction.toast.pending")}

- ), - bridge: props.bridge || undefined, - txHash, - }) - } - - return () => { - if (toRemoveId && !props.bridge) toastRef.current.remove(toRemoveId) - } - }, [ - t, - props.toastMessage, - isError, - error, - isSuccess, - isLoading, - props.link, - props.bridge, - txHash, - ]) - - return null -} diff --git a/src/sections/transaction/ReviewTransactionXCallForm.tsx b/src/sections/transaction/ReviewTransactionXCallForm.tsx index a4258b987..2e07b1832 100644 --- a/src/sections/transaction/ReviewTransactionXCallForm.tsx +++ b/src/sections/transaction/ReviewTransactionXCallForm.tsx @@ -24,6 +24,7 @@ type Props = TxProps & { onCancel: () => void onEvmSigned: (data: { evmTx: TransactionResponse }) => void onSignError?: (error: unknown) => void + isLoading: boolean } export const ReviewTransactionXCallForm: FC = ({ @@ -32,46 +33,51 @@ export const ReviewTransactionXCallForm: FC = ({ onEvmSigned, onCancel, onSignError, + isLoading, }) => { const { t } = useTranslation() const { account } = useEvmAccount() const { wallet } = useWallet() - const { mutate: signTx, isLoading } = useMutation(async () => { - try { - if (!account?.address) throw new Error("Missing active account") - if (!wallet) throw new Error("Missing wallet") - if (!wallet.signer) throw new Error("Missing signer") - if (!isEvmXCall(xcall)) throw new Error("Missing xcall") + const { mutate: signTx, isLoading: isSignTxLoading } = useMutation( + async () => { + try { + if (!account?.address) throw new Error("Missing active account") + if (!wallet) throw new Error("Missing wallet") + if (!wallet.signer) throw new Error("Missing signer") + if (!isEvmXCall(xcall)) throw new Error("Missing xcall") - if (wallet?.signer instanceof EthereumSigner) { - const { srcChain } = xcallMeta + if (wallet?.signer instanceof EthereumSigner) { + const { srcChain } = xcallMeta - const evmTx = await wallet.signer.sendTransaction({ - chain: srcChain, - from: account.address, - to: xcall.to, - data: xcall.data, - value: xcall.value, - }) - - const isApproveTx = evmTx.data.startsWith("0x095ea7b3") - if (isApproveTx) { - XItemCursor.reset({ - data: evmTx.data as `0x${string}`, - hash: evmTx.hash as `0x${string}`, - nonce: evmTx.nonce, - to: evmTx.to as `0x${string}`, + const evmTx = await wallet.signer.sendTransaction({ + chain: srcChain, + from: account.address, + to: xcall.to, + data: xcall.data, + value: xcall.value, }) - } - onEvmSigned({ evmTx }) + const isApproveTx = evmTx.data.startsWith("0x095ea7b3") + if (isApproveTx) { + XItemCursor.reset({ + data: evmTx.data as `0x${string}`, + hash: evmTx.hash as `0x${string}`, + nonce: evmTx.nonce, + to: evmTx.to as `0x${string}`, + }) + } + + onEvmSigned({ evmTx }) + } + } catch (error) { + onSignError?.(error) } - } catch (error) { - onSignError?.(error) - } - }) + }, + ) + + const loading = isLoading || isSignTxLoading return ( <> @@ -107,17 +113,17 @@ export const ReviewTransactionXCallForm: FC = ({