From c73b3ce843c7aec46dfa94a8da9486e5bdadd155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96hrlund?= Date: Mon, 25 Sep 2023 20:29:32 +0200 Subject: [PATCH 1/2] add utility hook that waits for a tableland txn & invokes callbacks --- garage/src/hooks/useWaitForTablelandTxn.ts | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 garage/src/hooks/useWaitForTablelandTxn.ts diff --git a/garage/src/hooks/useWaitForTablelandTxn.ts b/garage/src/hooks/useWaitForTablelandTxn.ts new file mode 100644 index 00000000..5d166e26 --- /dev/null +++ b/garage/src/hooks/useWaitForTablelandTxn.ts @@ -0,0 +1,45 @@ +import { useEffect, useState } from "react"; +import { useTablelandConnection } from "./useTablelandConnection"; + +export const useWaitForTablelandTxn = ( + chainId: number, + transactionHash: string | undefined, + onComplete: () => void, + onCancelled: () => void +) => { + const { validator } = useTablelandConnection(); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (validator && transactionHash) { + const controller = new AbortController(); + const signal = controller.signal; + + setIsLoading(true); + + validator + .pollForReceiptByTransactionHash( + { + chainId, + transactionHash, + }, + { interval: 2000, signal } + ) + .then((_) => { + setIsLoading(false); + onComplete(); + }) + .catch((_) => { + setIsLoading(false); + onCancelled(); + }); + + return () => { + setIsLoading(false); + controller.abort(); + }; + } + }, [chainId, transactionHash, onComplete, onCancelled]); + + return { isLoading }; +}; From 49b777c3087fb53a672850f25110c469a022f5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96hrlund?= Date: Mon, 25 Sep 2023 20:29:58 +0200 Subject: [PATCH 2/2] refactor app to use useWaitForTablelandTxn hook --- garage/src/components/SubmitMissionModal.tsx | 28 +--------- .../pages/Dashboard/modules/RigsInventory.tsx | 45 ++++------------ garage/src/pages/Proposal/index.tsx | 54 +++++++++---------- garage/src/pages/RigDetails/index.tsx | 43 ++++----------- 4 files changed, 45 insertions(+), 125 deletions(-) diff --git a/garage/src/components/SubmitMissionModal.tsx b/garage/src/components/SubmitMissionModal.tsx index 67cc839c..d2fcc801 100644 --- a/garage/src/components/SubmitMissionModal.tsx +++ b/garage/src/components/SubmitMissionModal.tsx @@ -20,10 +20,10 @@ import { } from "wagmi"; import { as0xString } from "../utils/types"; import { Mission } from "../types"; -import { useTablelandConnection } from "../hooks/useTablelandConnection"; import { TransactionStateAlert } from "./TransactionStateAlert"; import { secondaryChain, deployment } from "../env"; import { abi } from "../abis/MissionsManager"; +import { useWaitForTablelandTxn } from "../hooks/useWaitForTablelandTxn"; const { missionContractAddress } = deployment; @@ -48,8 +48,6 @@ export const SubmitMissionModal = ({ onTransactionCompleted, refresh, }: ModalProps) => { - const { validator } = useTablelandConnection(); - const initialState = { deliverables: mission.deliverables.map(({ key }) => ({ key, value: "" })), }; @@ -90,29 +88,7 @@ export const SubmitMissionModal = ({ onTransactionCompleted(isSuccess); }, [onTransactionCompleted, isTxLoading, isSuccess, data]); - useEffect(() => { - if (validator && data?.hash) { - const controller = new AbortController(); - const signal = controller.signal; - - validator - .pollForReceiptByTransactionHash( - { - chainId: secondaryChain.id, - transactionHash: data?.hash, - }, - { interval: 2000, signal } - ) - .then((_) => { - refresh(); - }) - .catch((_) => {}); - - return () => { - controller.abort(); - }; - } - }, [validator, data, refresh]); + useWaitForTablelandTxn(secondaryChain.id, data?.hash, refresh, refresh); const onInputChanged = useCallback( (e: React.ChangeEvent, atIndex: number) => { diff --git a/garage/src/pages/Dashboard/modules/RigsInventory.tsx b/garage/src/pages/Dashboard/modules/RigsInventory.tsx index ce562033..11b8926c 100644 --- a/garage/src/pages/Dashboard/modules/RigsInventory.tsx +++ b/garage/src/pages/Dashboard/modules/RigsInventory.tsx @@ -21,7 +21,6 @@ import { CheckIcon, QuestionIcon } from "@chakra-ui/icons"; import { useBlockNumber } from "wagmi"; import { useAccount } from "../../../hooks/useAccount"; import { useOwnedRigs } from "../../../hooks/useOwnedRigs"; -import { useTablelandConnection } from "../../../hooks/useTablelandConnection"; import { NFT } from "../../../hooks/useNFTs"; import { useNFTsCached } from "../../../components/NFTsContext"; import { Rig, RigWithPilots, Pilot } from "../../../types"; @@ -35,6 +34,7 @@ import { sleep } from "../../../utils/async"; import { prettyNumber } from "../../../utils/fmt"; import { firstSetValue, copySet, toggleInSet } from "../../../utils/set"; import { mainChain } from "../../../env"; +import { useWaitForTablelandTxn } from "../../../hooks/useWaitForTablelandTxn"; interface RigListItemProps { rig: RigWithPilots; @@ -141,9 +141,8 @@ const isSelectable = (rig: Rig, selectable: Selectable): boolean => { }; export const RigsInventory = (props: React.ComponentProps) => { - const { address, actingAsAddress, delegations } = useAccount(); + const { actingAsAddress } = useAccount(); const { rigs, refresh } = useOwnedRigs(actingAsAddress); - const { validator } = useTablelandConnection(); const { data: currentBlockNumber } = useBlockNumber(); const pilots = useMemo(() => { if (!rigs) return; @@ -191,39 +190,15 @@ export const RigsInventory = (props: React.ComponentProps) => { if (!pendingTx) setSelectedRigs(new Set()); }, [pendingTx]); - // Effect that waits until a tableland receipt is available for a tx hash - // and then refreshes the rig data - useEffect(() => { - if (validator && pendingTx) { - const controller = new AbortController(); - const signal = controller.signal; - - validator - .pollForReceiptByTransactionHash( - { - chainId: mainChain.id, - transactionHash: pendingTx, - }, - { interval: 2000, signal } - ) - .then((_) => { - refreshRigsAndClearPendingTx(); - }) - .catch((_) => { - clearPendingTx(); - }); - - return () => { - controller.abort(); - }; - } - }, [pendingTx, refreshRigsAndClearPendingTx, validator, clearPendingTx]); + useWaitForTablelandTxn( + mainChain.id, + pendingTx, + refreshRigsAndClearPendingTx, + clearPendingTx + ); - const { - trainRigsModal, - pilotRigsModal, - parkRigsModal, - } = useGlobalFlyParkModals(); + const { trainRigsModal, pilotRigsModal, parkRigsModal } = + useGlobalFlyParkModals(); const openTrainModal = useCallback(() => { if (rigs?.length && selectedRigs.size) { diff --git a/garage/src/pages/Proposal/index.tsx b/garage/src/pages/Proposal/index.tsx index 84a3a920..db65f3d3 100644 --- a/garage/src/pages/Proposal/index.tsx +++ b/garage/src/pages/Proposal/index.tsx @@ -42,7 +42,6 @@ import { proposalStatus, } from "../../components/ProposalStatusBadge"; import { useProposal, Result, Vote } from "../../hooks/useProposal"; -import { useTablelandConnection } from "../../hooks/useTablelandConnection"; import { useAddressVotingPower } from "../../hooks/useAddressVotingPower"; import { TOPBAR_HEIGHT } from "../../Topbar"; import { prettyNumber, truncateWalletAddress } from "../../utils/fmt"; @@ -50,6 +49,7 @@ import { as0xString } from "../../utils/types"; import { ProposalWithOptions, ProposalStatus } from "../../types"; import { deployment, secondaryChain } from "../../env"; import { abi } from "../../abis/VotingRegistry"; +import { useWaitForTablelandTxn } from "../../hooks/useWaitForTablelandTxn"; const ipfsGatewayBaseUrl = "https://nftstorage.link"; @@ -87,7 +87,6 @@ const CastVote = ({ (v) => v.address.toLowerCase() === address?.toLowerCase() ); - const { validator } = useTablelandConnection(); const { votingPower } = useAddressVotingPower(address, proposal.id); const { data: blockNumber } = useBlockNumber(); @@ -155,34 +154,29 @@ const CastVote = ({ hash: data?.hash, }); - useEffect(() => { - if (validator && data?.hash) { - const controller = new AbortController(); - const signal = controller.signal; - - validator - .pollForReceiptByTransactionHash( - { - chainId: secondaryChain.id, - transactionHash: data?.hash, - }, - { interval: 2000, signal } - ) - .then((_) => { - toast({ - title: "Vote successful", - status: "success", - duration: 5_000, - }); - refresh(); - }) - .catch((_) => {}); - - return () => { - controller.abort(); - }; - } - }, [validator, data, refresh, toast]); + const onTxnCompleted = useCallback(() => { + toast({ + title: "Vote successful", + status: "success", + duration: 5_000, + }); + refresh(); + }, [toast, refresh]); + + const onTxnFailed = useCallback(() => { + toast({ + title: "Not able to fetch tableland receipt, please refresh the page.", + status: "warning", + duration: 5_000, + }); + }, [toast, refresh]); + + useWaitForTablelandTxn( + secondaryChain.id, + data?.hash, + onTxnCompleted, + onTxnFailed + ); const isMobile = useBreakpointValue( { base: true, lg: false }, diff --git a/garage/src/pages/RigDetails/index.tsx b/garage/src/pages/RigDetails/index.tsx index c859633d..b1b19a9b 100644 --- a/garage/src/pages/RigDetails/index.tsx +++ b/garage/src/pages/RigDetails/index.tsx @@ -40,7 +40,6 @@ import { RigDisplay } from "../../components/RigDisplay"; import { FlightLog } from "./modules/FlightLog"; import { Pilots } from "./modules/Pilots"; import { RigAttributes } from "./modules/RigAttributes"; -import { useTablelandConnection } from "../../hooks/useTablelandConnection"; import { useRig } from "../../hooks/useRig"; import { findNFT } from "../../utils/nfts"; import { prettyNumber, truncateWalletAddress } from "../../utils/fmt"; @@ -52,6 +51,7 @@ import { abi } from "../../abis/TablelandRigs"; import { ReactComponent as OpenseaMark } from "../../assets/opensea-mark.svg"; import { ReactComponent as TablelandMark } from "../../assets/tableland.svg"; import { ReactComponent as FilecoinMark } from "../../assets/filecoin-mark.svg"; +import { useWaitForTablelandTxn } from "../../hooks/useWaitForTablelandTxn"; const { contractAddress } = deployment; @@ -267,7 +267,6 @@ export const RigDetails = () => { const { actingAsAddress } = useAccount(); const { data: currentBlockNumber } = useBlockNumber(); const { rig, refresh: refreshRig } = useRig(id || ""); - const { validator } = useTablelandConnection(); const { data: contractData, refetch } = useContractReads({ allowFailure: false, @@ -325,42 +324,18 @@ export const RigDetails = () => { sleep(500).then((_) => setPendingTx(undefined)); }, [refresh, setPendingTx]); - // Effect that waits until a tableland receipt is available for a tx hash - // and then refreshes the rig data - useEffect(() => { - if (validator && pendingTx) { - const controller = new AbortController(); - const signal = controller.signal; - - validator - .pollForReceiptByTransactionHash( - { - chainId: mainChain.id, - transactionHash: pendingTx, - }, - { interval: 2000, signal } - ) - .then((_) => { - refreshRigAndClearPendingTx(); - }) - .catch((_) => { - clearPendingTx(); - }); - - return () => { - controller.abort(); - }; - } - }, [pendingTx, refreshRigAndClearPendingTx, validator, clearPendingTx]); + useWaitForTablelandTxn( + mainChain.id, + pendingTx, + refreshRigAndClearPendingTx, + clearPendingTx + ); const currentNFT = rig?.currentPilot && nfts && findNFT(rig.currentPilot, nfts); - const { - trainRigsModal, - pilotRigsModal, - parkRigsModal, - } = useGlobalFlyParkModals(); + const { trainRigsModal, pilotRigsModal, parkRigsModal } = + useGlobalFlyParkModals(); const onOpenTrainModal = useCallback(() => { if (rig) trainRigsModal.openModal([rig], setPendingTx);