From 8581abe39646589c246d45322b0898522f84d395 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Tue, 30 Jan 2024 20:01:26 +0530 Subject: [PATCH 1/4] feat(web): add-toast-for-graph-and-rpc-errors --- web/src/hooks/queries/useDisputeTemplate.ts | 8 ++- web/src/hooks/useIsCrossChainDispute.ts | 67 ++++++++++++--------- web/src/utils/debounceErrorToast.ts | 11 ++++ web/src/utils/graphqlQueryFnHelper.ts | 10 ++- 4 files changed, 63 insertions(+), 33 deletions(-) create mode 100644 web/src/utils/debounceErrorToast.ts diff --git a/web/src/hooks/queries/useDisputeTemplate.ts b/web/src/hooks/queries/useDisputeTemplate.ts index 39fbdbac2..3f22cd69e 100644 --- a/web/src/hooks/queries/useDisputeTemplate.ts +++ b/web/src/hooks/queries/useDisputeTemplate.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { graphql } from "src/graphql"; -import { PublicClient } from "viem"; +import { HttpRequestError, PublicClient } from "viem"; import { usePublicClient } from "wagmi"; import { getIArbitrableV2 } from "hooks/contracts/generated"; import { isUndefined } from "utils/index"; @@ -12,6 +12,7 @@ import { executeActions } from "@kleros/kleros-sdk/src/dataMappings/executeActio import { configureSDK } from "@kleros/kleros-sdk/src/sdk"; import { alchemyApiKey } from "context/Web3Provider"; import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; +import { debounceErrorToast } from "utils/debounceErrorToast"; const disputeTemplateQuery = graphql(` query DisputeTemplate($id: ID!) { @@ -66,7 +67,10 @@ export const useDisputeTemplate = (disputeID?: string, arbitrableAddress?: `0x${ const disputeDetailes = populateTemplate(disputeTemplateInput, data); return disputeDetailes; - } catch { + } catch (error) { + if (error instanceof HttpRequestError) { + debounceErrorToast("RPC failed!, Please avoid voting."); + } return {} as DisputeDetails; } } else throw Error; diff --git a/web/src/hooks/useIsCrossChainDispute.ts b/web/src/hooks/useIsCrossChainDispute.ts index 5720203a3..2b619ea7a 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 } 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: any) { + if (error instanceof HttpRequestError) { + debounceErrorToast("RPC failed!, Please avoid voting."); + } + throw Error; } } else throw Error; }, diff --git a/web/src/utils/debounceErrorToast.ts b/web/src/utils/debounceErrorToast.ts new file mode 100644 index 000000000..ca69b8032 --- /dev/null +++ b/web/src/utils/debounceErrorToast.ts @@ -0,0 +1,11 @@ +import { toast } from "react-toastify"; +import { OPTIONS as toastOptions } from "utils/wrapWithToast"; + +let timeoutId: NodeJS.Timeout; +export const debounceErrorToast = (msg: string) => { + 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..1406e7944 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 failed : failed to fetch data"); + } }; From 3f6aa7ea92d4c8f9b8ef5595fe3869e495721b81 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Wed, 31 Jan 2024 13:41:42 +0530 Subject: [PATCH 2/4] feat(web): display-info-when-rpc-fails --- web/src/components/DisputeCard/index.tsx | 9 +++++---- web/src/components/DisputePreview/DisputeContext.tsx | 10 +++++----- web/src/consts/index.ts | 1 + web/src/hooks/queries/useDisputeTemplate.ts | 10 ++++++---- web/src/hooks/useIsCrossChainDispute.ts | 6 +++--- web/src/pages/Cases/CaseDetails/Overview/index.tsx | 4 ++-- .../pages/Cases/CaseDetails/Voting/VotingHistory.tsx | 5 +++-- 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/web/src/components/DisputeCard/index.tsx b/web/src/components/DisputeCard/index.tsx index affa9ae21..5b30e0fe0 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 } = useDisputeTemplate(id, arbitrated.id as `0x${string}`); + const { data: disputeDetails, isError } = useDisputeTemplate(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/useDisputeTemplate.ts b/web/src/hooks/queries/useDisputeTemplate.ts index 3f22cd69e..5febeaecd 100644 --- a/web/src/hooks/queries/useDisputeTemplate.ts +++ b/web/src/hooks/queries/useDisputeTemplate.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { graphql } from "src/graphql"; -import { HttpRequestError, PublicClient } from "viem"; +import { HttpRequestError, PublicClient, RpcError } from "viem"; import { usePublicClient } from "wagmi"; import { getIArbitrableV2 } from "hooks/contracts/generated"; import { isUndefined } from "utils/index"; @@ -27,14 +27,14 @@ const disputeTemplateQuery = graphql(` export const useDisputeTemplate = (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, crossChainId, crossChainTemplateId } = crossChainData; const templateId = isCrossChainDispute @@ -68,9 +68,11 @@ export const useDisputeTemplate = (disputeID?: string, arbitrableAddress?: `0x${ return disputeDetailes; } catch (error) { - if (error instanceof HttpRequestError) { + 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 2b619ea7a..5536bd2c5 100644 --- a/web/src/hooks/useIsCrossChainDispute.ts +++ b/web/src/hooks/useIsCrossChainDispute.ts @@ -4,7 +4,7 @@ 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 } from "viem"; +import { HttpRequestError, RpcError } from "viem"; interface IIsCrossChainDispute { isCrossChainDispute: boolean; @@ -54,8 +54,8 @@ export const useIsCrossChainDispute = (disputeID?: string, arbitrableAddress?: ` crossChainArbitrableAddress: "0x", }; } - } catch (error: any) { - if (error instanceof HttpRequestError) { + } catch (error) { + if (error instanceof HttpRequestError || error instanceof RpcError) { debounceErrorToast("RPC failed!, Please avoid voting."); } throw Error; diff --git a/web/src/pages/Cases/CaseDetails/Overview/index.tsx b/web/src/pages/Cases/CaseDetails/Overview/index.tsx index ec94ca87b..063e1e63d 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 } = useDisputeTemplate(id, arbitrable); + const { data: disputeDetails, isError } = useDisputeTemplate(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 308a999df..8cbe0bd5d 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: disputeTemplate } = useDisputeTemplate(id, arbitrable); + const { data: disputeTemplate, isError } = useDisputeTemplate(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 && disputeTemplate.question ? ( {disputeTemplate.question} ) : ( - {"The dispute's template is not correct please vote refuse to arbitrate"} + {isError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR} )} Date: Wed, 31 Jan 2024 09:48:41 +0100 Subject: [PATCH 3/4] chore(web): remove whitespace before ":" --- web/src/utils/graphqlQueryFnHelper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/utils/graphqlQueryFnHelper.ts b/web/src/utils/graphqlQueryFnHelper.ts index 1406e7944..4deb63c76 100644 --- a/web/src/utils/graphqlQueryFnHelper.ts +++ b/web/src/utils/graphqlQueryFnHelper.ts @@ -23,7 +23,7 @@ export const graphqlQueryFnHelper = async ( const url = graphqlUrl(isDisputeTemplate, chainId); return await request(url, query, parametersObject); } catch (error) { - console.log("Graph error : ", { error }); - debounceErrorToast("Graph failed : failed to fetch data"); + console.log("Graph error: ", { error }); + debounceErrorToast("Graph query error: failed to fetch data."); } }; From 2e709e90d6b75b3a9e4531426f8a5e196931f929 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Wed, 31 Jan 2024 15:30:53 +0530 Subject: [PATCH 4/4] refactor(web): remove-logs --- web/src/hooks/queries/useDisputeTemplate.ts | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/web/src/hooks/queries/useDisputeTemplate.ts b/web/src/hooks/queries/useDisputeTemplate.ts index 5febeaecd..7b8006aac 100644 --- a/web/src/hooks/queries/useDisputeTemplate.ts +++ b/web/src/hooks/queries/useDisputeTemplate.ts @@ -9,8 +9,6 @@ import { useIsCrossChainDispute } from "../useIsCrossChainDispute"; 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 { configureSDK } from "@kleros/kleros-sdk/src/sdk"; -import { alchemyApiKey } from "context/Web3Provider"; import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; import { debounceErrorToast } from "utils/debounceErrorToast"; @@ -40,31 +38,23 @@ export const useDisputeTemplate = (disputeID?: string, arbitrableAddress?: `0x${ const templateId = isCrossChainDispute ? crossChainTemplateId : await getTemplateId(arbitrableAddress, disputeID, publicClient); + const { disputeTemplate } = await graphqlQueryFnHelper( disputeTemplateQuery, { id: templateId.toString() }, true ); - console.log("useDisputeTemplate:", disputeTemplate); - const disputeTemplateInput = disputeTemplate?.templateData; - const dataMappingsInput = disputeTemplate?.templateDataMappings; - configureSDK({ apiKey: alchemyApiKey }); + const templateData = disputeTemplate?.templateData; + const dataMappings = disputeTemplate?.templateDataMappings; const initialContext = { disputeID: disputeID, arbitrable: arbitrableAddress, }; - console.log("dataMappingsInput", dataMappingsInput); - let data = {}; - if (dataMappingsInput) { - const parsedMappings = JSON.parse(dataMappingsInput); - console.log("parsedMappings", parsedMappings); - data = await executeActions(parsedMappings, initialContext); - } - console.log("data", data); - const disputeDetailes = populateTemplate(disputeTemplateInput, data); + const data = dataMappings ? await executeActions(JSON.parse(dataMappings), initialContext) : {}; + const disputeDetailes = populateTemplate(templateData, data); return disputeDetailes; } catch (error) {