diff --git a/web/src/components/DisputeCard/index.tsx b/web/src/components/DisputeCard/index.tsx index fb4fb0edc..978dd69de 100644 --- a/web/src/components/DisputeCard/index.tsx +++ b/web/src/components/DisputeCard/index.tsx @@ -14,7 +14,7 @@ import DisputeInfo from "./DisputeInfo"; import PeriodBanner from "./PeriodBanner"; import { isUndefined } from "utils/index"; import { responsiveSize } from "styles/responsiveSize"; -import { INVALID_DISPUTE_DATA_ERROR } from "consts/index"; +import { INVALID_DISPUTE_DATA_ERROR, RPC_ERROR } from "consts/index"; const StyledCard = styled(Card)` width: 100%; @@ -104,12 +104,13 @@ const DisputeCard: React.FC = ({ currentPeriodIndex === 4 ? lastPeriodChange : getPeriodEndTimestamp(lastPeriodChange, currentPeriodIndex, court.timesPerPeriod); - const { data: disputeDetails } = usePopulatedDisputeData(id, arbitrated.id as `0x${string}`); + const { data: disputeDetails, isError } = usePopulatedDisputeData(id, arbitrated.id as `0x${string}`); const { data: courtPolicy } = useCourtPolicy(court.id); const courtName = courtPolicy?.name; const category = disputeDetails?.category; const navigate = useNavigate(); + const errMsg = isError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR; return ( <> {!isList || overrideIsList ? ( @@ -119,7 +120,7 @@ const DisputeCard: React.FC = ({ {isUndefined(disputeDetails) ? ( ) : ( - + )} = ({ - + = ({ disputeDetails }) => { +export const DisputeContext: React.FC = ({ disputeDetails, isRpcError = false }) => { + const errMsg = isRpcError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR; return ( <> - - {isUndefined(disputeDetails) ? : disputeDetails?.title ?? INVALID_DISPUTE_DATA_ERROR} - + {isUndefined(disputeDetails) ? : disputeDetails?.title ?? errMsg} {!isUndefined(disputeDetails) && ( {disputeDetails?.question} diff --git a/web/src/consts/index.ts b/web/src/consts/index.ts index 77410987b..ffd10d323 100644 --- a/web/src/consts/index.ts +++ b/web/src/consts/index.ts @@ -25,3 +25,4 @@ export const GENESIS_BLOCK_ARBSEPOLIA = BigInt(process.env.REACT_APP_GENESIS_BLO export const isProductionDeployment = () => process.env.REACT_APP_DEPLOYMENT !== "mainnet"; export const INVALID_DISPUTE_DATA_ERROR = `The dispute data is not valid, please vote "Refuse to arbitrate"`; +export const RPC_ERROR = `RPC Error: Unable to fetch dispute data. Please avoid voting.`; diff --git a/web/src/hooks/queries/usePopulatedDisputeData.ts b/web/src/hooks/queries/usePopulatedDisputeData.ts index b48980c58..068c8d862 100644 --- a/web/src/hooks/queries/usePopulatedDisputeData.ts +++ b/web/src/hooks/queries/usePopulatedDisputeData.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { graphql } from "src/graphql"; -import { PublicClient } from "viem"; +import { HttpRequestError, PublicClient, RpcError } from "viem"; import { usePublicClient } from "wagmi"; import { getIArbitrableV2 } from "hooks/contracts/generated"; import { isUndefined } from "utils/index"; @@ -10,6 +10,7 @@ import { GENESIS_BLOCK_ARBSEPOLIA } from "consts/index"; import { populateTemplate } from "@kleros/kleros-sdk/src/dataMappings/utils/populateTemplate"; import { executeActions } from "@kleros/kleros-sdk/src/dataMappings/executeActions"; import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; +import { debounceErrorToast } from "utils/debounceErrorToast"; const disputeTemplateQuery = graphql(` query DisputeTemplate($id: ID!) { @@ -24,14 +25,14 @@ const disputeTemplateQuery = graphql(` export const usePopulatedDisputeData = (disputeID?: string, arbitrableAddress?: `0x${string}`) => { const publicClient = usePublicClient(); - const { data: crossChainData } = useIsCrossChainDispute(disputeID, arbitrableAddress); + const { data: crossChainData, isError } = useIsCrossChainDispute(disputeID, arbitrableAddress); const isEnabled = !isUndefined(disputeID) && !isUndefined(crossChainData) && !isUndefined(arbitrableAddress); return useQuery({ queryKey: [`DisputeTemplate${disputeID}${arbitrableAddress}`], enabled: isEnabled, staleTime: Infinity, queryFn: async () => { - if (isEnabled) { + if (isEnabled && !isError) { try { const { isCrossChainDispute, crossChainTemplateId } = crossChainData; const templateId = isCrossChainDispute @@ -55,7 +56,12 @@ export const usePopulatedDisputeData = (disputeID?: string, arbitrableAddress?: const disputeDetails = populateTemplate(templateData, data); return disputeDetails; - } catch { + } catch (error) { + if (error instanceof HttpRequestError || error instanceof RpcError) { + debounceErrorToast("RPC failed!, Please avoid voting."); + throw Error; + } + return {} as DisputeDetails; } } else throw Error; diff --git a/web/src/hooks/useIsCrossChainDispute.ts b/web/src/hooks/useIsCrossChainDispute.ts index 5720203a3..5536bd2c5 100644 --- a/web/src/hooks/useIsCrossChainDispute.ts +++ b/web/src/hooks/useIsCrossChainDispute.ts @@ -3,6 +3,8 @@ import { usePublicClient } from "wagmi"; import { getIHomeGateway } from "hooks/contracts/generated"; import { isUndefined } from "utils/index"; import { GENESIS_BLOCK_ARBSEPOLIA } from "src/consts"; +import { debounceErrorToast } from "utils/debounceErrorToast"; +import { HttpRequestError, RpcError } from "viem"; interface IIsCrossChainDispute { isCrossChainDispute: boolean; @@ -20,36 +22,43 @@ export const useIsCrossChainDispute = (disputeID?: string, arbitrableAddress?: ` staleTime: Infinity, queryFn: async () => { if (isEnabled) { - const arbitrable = getIHomeGateway({ - address: arbitrableAddress, - }); - const crossChainDisputeFilter = await arbitrable.createEventFilter.CrossChainDisputeIncoming( - { - _arbitratorDisputeID: BigInt(disputeID), - }, - { - fromBlock: GENESIS_BLOCK_ARBSEPOLIA, - toBlock: "latest", - } - ); - const crossChainDisputeEvents = await publicClient.getFilterLogs({ - filter: crossChainDisputeFilter, - }); + try { + const arbitrable = getIHomeGateway({ + address: arbitrableAddress, + }); + const crossChainDisputeFilter = await arbitrable.createEventFilter.CrossChainDisputeIncoming( + { + _arbitratorDisputeID: BigInt(disputeID), + }, + { + fromBlock: GENESIS_BLOCK_ARBSEPOLIA, + toBlock: "latest", + } + ); + const crossChainDisputeEvents = await publicClient.getFilterLogs({ + filter: crossChainDisputeFilter, + }); - if (crossChainDisputeEvents.length > 0) { - return { - isCrossChainDispute: true, - crossChainId: crossChainDisputeEvents[0].args._arbitrableChainId ?? 0n, - crossChainTemplateId: crossChainDisputeEvents[0].args._templateId ?? 0n, - crossChainArbitrableAddress: crossChainDisputeEvents[0].args._arbitrable ?? "0x", - }; - } else { - return { - isCrossChainDispute: false, - crossChainId: 0n, - crossChainTemplateId: 0n, - crossChainArbitrableAddress: "0x", - }; + if (crossChainDisputeEvents.length > 0) { + return { + isCrossChainDispute: true, + crossChainId: crossChainDisputeEvents[0].args._arbitrableChainId ?? 0n, + crossChainTemplateId: crossChainDisputeEvents[0].args._templateId ?? 0n, + crossChainArbitrableAddress: crossChainDisputeEvents[0].args._arbitrable ?? "0x", + }; + } else { + return { + isCrossChainDispute: false, + crossChainId: 0n, + crossChainTemplateId: 0n, + crossChainArbitrableAddress: "0x", + }; + } + } catch (error) { + if (error instanceof HttpRequestError || error instanceof RpcError) { + debounceErrorToast("RPC failed!, Please avoid voting."); + } + throw Error; } } else throw Error; }, diff --git a/web/src/pages/Cases/CaseDetails/Overview/index.tsx b/web/src/pages/Cases/CaseDetails/Overview/index.tsx index 8bd5e7c3c..e2ea704c1 100644 --- a/web/src/pages/Cases/CaseDetails/Overview/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Overview/index.tsx @@ -39,7 +39,7 @@ interface IOverview { const Overview: React.FC = ({ arbitrable, courtID, currentPeriodIndex }) => { const { id } = useParams(); - const { data: disputeDetails } = usePopulatedDisputeData(id, arbitrable); + const { data: disputeDetails, isError } = usePopulatedDisputeData(id, arbitrable); const { data: dispute } = useDisputeDetailsQuery(id); const { data: courtPolicy } = useCourtPolicy(courtID); const { data: votingHistory } = useVotingHistory(id); @@ -52,7 +52,7 @@ const Overview: React.FC = ({ arbitrable, courtID, currentPeriodIndex return ( <> - + diff --git a/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx b/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx index 07b2a2d0c..c8fd3da9f 100644 --- a/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx +++ b/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx @@ -11,6 +11,7 @@ import { getDrawnJurorsWithCount } from "utils/getDrawnJurorsWithCount"; import { useDisputeDetailsQuery } from "hooks/queries/useDisputeDetailsQuery"; import VotesAccordion from "./VotesDetails"; import Skeleton from "react-loading-skeleton"; +import { INVALID_DISPUTE_DATA_ERROR, RPC_ERROR } from "consts/index"; const Container = styled.div``; @@ -24,7 +25,7 @@ const VotingHistory: React.FC<{ arbitrable?: `0x${string}`; isQuestion: boolean const { data: votingHistory } = useVotingHistory(id); const { data: disputeData } = useDisputeDetailsQuery(id); const [currentTab, setCurrentTab] = useState(0); - const { data: disputeDetails } = usePopulatedDisputeData(id, arbitrable); + const { data: disputeDetails, isError } = usePopulatedDisputeData(id, arbitrable); const rounds = votingHistory?.dispute?.rounds; const localRounds = getLocalRounds(votingHistory?.dispute?.disputeKitDispute); @@ -45,7 +46,7 @@ const VotingHistory: React.FC<{ arbitrable?: `0x${string}`; isQuestion: boolean {isQuestion && disputeDetails.question ? ( {disputeDetails.question} ) : ( - {"The dispute's template is not correct please vote refuse to arbitrate"} + {isError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR} )} { + if (timeoutId) clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + toast.error(msg, toastOptions); + }, 5000); +}; diff --git a/web/src/utils/graphqlQueryFnHelper.ts b/web/src/utils/graphqlQueryFnHelper.ts index 47235f63c..4deb63c76 100644 --- a/web/src/utils/graphqlQueryFnHelper.ts +++ b/web/src/utils/graphqlQueryFnHelper.ts @@ -1,6 +1,7 @@ import request from "graphql-request"; import { arbitrumSepolia } from "wagmi/chains"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; +import { debounceErrorToast } from "./debounceErrorToast"; const CHAINID_TO_DISPUTE_TEMPLATE_SUBGRAPH = { [arbitrumSepolia.id]: @@ -18,6 +19,11 @@ export const graphqlQueryFnHelper = async ( isDisputeTemplate = false, chainId = arbitrumSepolia.id ) => { - const url = graphqlUrl(isDisputeTemplate, chainId); - return request(url, query, parametersObject); + try { + const url = graphqlUrl(isDisputeTemplate, chainId); + return await request(url, query, parametersObject); + } catch (error) { + console.log("Graph error: ", { error }); + debounceErrorToast("Graph query error: failed to fetch data."); + } };