Skip to content

Commit 524cc8b

Browse files
authored
Merge pull request #1466 from kleros/feat(web)/graph-and-rpc-error-toasts
feat(web): add-toast-for-graph-and-rpc-errors
2 parents e2a18cc + 4e32c41 commit 524cc8b

File tree

9 files changed

+83
-48
lines changed

9 files changed

+83
-48
lines changed

web/src/components/DisputeCard/index.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import DisputeInfo from "./DisputeInfo";
1414
import PeriodBanner from "./PeriodBanner";
1515
import { isUndefined } from "utils/index";
1616
import { responsiveSize } from "styles/responsiveSize";
17-
import { INVALID_DISPUTE_DATA_ERROR } from "consts/index";
17+
import { INVALID_DISPUTE_DATA_ERROR, RPC_ERROR } from "consts/index";
1818

1919
const StyledCard = styled(Card)`
2020
width: 100%;
@@ -104,12 +104,13 @@ const DisputeCard: React.FC<IDisputeCard> = ({
104104
currentPeriodIndex === 4
105105
? lastPeriodChange
106106
: getPeriodEndTimestamp(lastPeriodChange, currentPeriodIndex, court.timesPerPeriod);
107-
const { data: disputeDetails } = usePopulatedDisputeData(id, arbitrated.id as `0x${string}`);
107+
const { data: disputeDetails, isError } = usePopulatedDisputeData(id, arbitrated.id as `0x${string}`);
108108

109109
const { data: courtPolicy } = useCourtPolicy(court.id);
110110
const courtName = courtPolicy?.name;
111111
const category = disputeDetails?.category;
112112
const navigate = useNavigate();
113+
const errMsg = isError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR;
113114
return (
114115
<>
115116
{!isList || overrideIsList ? (
@@ -119,7 +120,7 @@ const DisputeCard: React.FC<IDisputeCard> = ({
119120
{isUndefined(disputeDetails) ? (
120121
<StyledSkeleton />
121122
) : (
122-
<TruncatedTitle text={disputeDetails?.title ?? INVALID_DISPUTE_DATA_ERROR} maxLength={100} />
123+
<TruncatedTitle text={disputeDetails?.title ?? errMsg} maxLength={100} />
123124
)}
124125
<DisputeInfo
125126
disputeID={id}
@@ -137,7 +138,7 @@ const DisputeCard: React.FC<IDisputeCard> = ({
137138
<PeriodBanner isCard={false} id={parseInt(id)} period={currentPeriodIndex} />
138139
<ListContainer>
139140
<ListTitle>
140-
<TruncatedTitle text={disputeDetails?.title ?? INVALID_DISPUTE_DATA_ERROR} maxLength={50} />
141+
<TruncatedTitle text={disputeDetails?.title ?? errMsg} maxLength={50} />
141142
</ListTitle>
142143
<DisputeInfo
143144
courtId={court?.id}

web/src/components/DisputePreview/DisputeContext.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Answer as IAnswer } from "context/NewDisputeContext";
77
import AliasDisplay from "./Alias";
88
import { responsiveSize } from "styles/responsiveSize";
99
import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes";
10-
import { INVALID_DISPUTE_DATA_ERROR } from "consts/index";
10+
import { INVALID_DISPUTE_DATA_ERROR, RPC_ERROR } from "consts/index";
1111

1212
const StyledH1 = styled.h1`
1313
margin: 0;
@@ -60,14 +60,14 @@ const Divider = styled.hr`
6060
`;
6161
interface IDisputeContext {
6262
disputeDetails?: DisputeDetails;
63+
isRpcError?: boolean;
6364
}
6465

65-
export const DisputeContext: React.FC<IDisputeContext> = ({ disputeDetails }) => {
66+
export const DisputeContext: React.FC<IDisputeContext> = ({ disputeDetails, isRpcError = false }) => {
67+
const errMsg = isRpcError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR;
6668
return (
6769
<>
68-
<StyledH1>
69-
{isUndefined(disputeDetails) ? <StyledSkeleton /> : disputeDetails?.title ?? INVALID_DISPUTE_DATA_ERROR}
70-
</StyledH1>
70+
<StyledH1>{isUndefined(disputeDetails) ? <StyledSkeleton /> : disputeDetails?.title ?? errMsg}</StyledH1>
7171
{!isUndefined(disputeDetails) && (
7272
<QuestionAndDescription>
7373
<StyledReactMarkDown>{disputeDetails?.question}</StyledReactMarkDown>

web/src/consts/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ export const GENESIS_BLOCK_ARBSEPOLIA = BigInt(process.env.REACT_APP_GENESIS_BLO
2525
export const isProductionDeployment = () => process.env.REACT_APP_DEPLOYMENT !== "mainnet";
2626

2727
export const INVALID_DISPUTE_DATA_ERROR = `The dispute data is not valid, please vote "Refuse to arbitrate"`;
28+
export const RPC_ERROR = `RPC Error: Unable to fetch dispute data. Please avoid voting.`;

web/src/hooks/queries/usePopulatedDisputeData.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useQuery } from "@tanstack/react-query";
22
import { graphql } from "src/graphql";
3-
import { PublicClient } from "viem";
3+
import { HttpRequestError, PublicClient, RpcError } from "viem";
44
import { usePublicClient } from "wagmi";
55
import { getIArbitrableV2 } from "hooks/contracts/generated";
66
import { isUndefined } from "utils/index";
@@ -10,6 +10,7 @@ import { GENESIS_BLOCK_ARBSEPOLIA } from "consts/index";
1010
import { populateTemplate } from "@kleros/kleros-sdk/src/dataMappings/utils/populateTemplate";
1111
import { executeActions } from "@kleros/kleros-sdk/src/dataMappings/executeActions";
1212
import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes";
13+
import { debounceErrorToast } from "utils/debounceErrorToast";
1314

1415
const disputeTemplateQuery = graphql(`
1516
query DisputeTemplate($id: ID!) {
@@ -24,14 +25,14 @@ const disputeTemplateQuery = graphql(`
2425

2526
export const usePopulatedDisputeData = (disputeID?: string, arbitrableAddress?: `0x${string}`) => {
2627
const publicClient = usePublicClient();
27-
const { data: crossChainData } = useIsCrossChainDispute(disputeID, arbitrableAddress);
28+
const { data: crossChainData, isError } = useIsCrossChainDispute(disputeID, arbitrableAddress);
2829
const isEnabled = !isUndefined(disputeID) && !isUndefined(crossChainData) && !isUndefined(arbitrableAddress);
2930
return useQuery<DisputeDetails>({
3031
queryKey: [`DisputeTemplate${disputeID}${arbitrableAddress}`],
3132
enabled: isEnabled,
3233
staleTime: Infinity,
3334
queryFn: async () => {
34-
if (isEnabled) {
35+
if (isEnabled && !isError) {
3536
try {
3637
const { isCrossChainDispute, crossChainTemplateId } = crossChainData;
3738
const templateId = isCrossChainDispute
@@ -55,7 +56,12 @@ export const usePopulatedDisputeData = (disputeID?: string, arbitrableAddress?:
5556
const disputeDetails = populateTemplate(templateData, data);
5657

5758
return disputeDetails;
58-
} catch {
59+
} catch (error) {
60+
if (error instanceof HttpRequestError || error instanceof RpcError) {
61+
debounceErrorToast("RPC failed!, Please avoid voting.");
62+
throw Error;
63+
}
64+
5965
return {} as DisputeDetails;
6066
}
6167
} else throw Error;

web/src/hooks/useIsCrossChainDispute.ts

+38-29
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { usePublicClient } from "wagmi";
33
import { getIHomeGateway } from "hooks/contracts/generated";
44
import { isUndefined } from "utils/index";
55
import { GENESIS_BLOCK_ARBSEPOLIA } from "src/consts";
6+
import { debounceErrorToast } from "utils/debounceErrorToast";
7+
import { HttpRequestError, RpcError } from "viem";
68

79
interface IIsCrossChainDispute {
810
isCrossChainDispute: boolean;
@@ -20,36 +22,43 @@ export const useIsCrossChainDispute = (disputeID?: string, arbitrableAddress?: `
2022
staleTime: Infinity,
2123
queryFn: async () => {
2224
if (isEnabled) {
23-
const arbitrable = getIHomeGateway({
24-
address: arbitrableAddress,
25-
});
26-
const crossChainDisputeFilter = await arbitrable.createEventFilter.CrossChainDisputeIncoming(
27-
{
28-
_arbitratorDisputeID: BigInt(disputeID),
29-
},
30-
{
31-
fromBlock: GENESIS_BLOCK_ARBSEPOLIA,
32-
toBlock: "latest",
33-
}
34-
);
35-
const crossChainDisputeEvents = await publicClient.getFilterLogs({
36-
filter: crossChainDisputeFilter,
37-
});
25+
try {
26+
const arbitrable = getIHomeGateway({
27+
address: arbitrableAddress,
28+
});
29+
const crossChainDisputeFilter = await arbitrable.createEventFilter.CrossChainDisputeIncoming(
30+
{
31+
_arbitratorDisputeID: BigInt(disputeID),
32+
},
33+
{
34+
fromBlock: GENESIS_BLOCK_ARBSEPOLIA,
35+
toBlock: "latest",
36+
}
37+
);
38+
const crossChainDisputeEvents = await publicClient.getFilterLogs({
39+
filter: crossChainDisputeFilter,
40+
});
3841

39-
if (crossChainDisputeEvents.length > 0) {
40-
return {
41-
isCrossChainDispute: true,
42-
crossChainId: crossChainDisputeEvents[0].args._arbitrableChainId ?? 0n,
43-
crossChainTemplateId: crossChainDisputeEvents[0].args._templateId ?? 0n,
44-
crossChainArbitrableAddress: crossChainDisputeEvents[0].args._arbitrable ?? "0x",
45-
};
46-
} else {
47-
return {
48-
isCrossChainDispute: false,
49-
crossChainId: 0n,
50-
crossChainTemplateId: 0n,
51-
crossChainArbitrableAddress: "0x",
52-
};
42+
if (crossChainDisputeEvents.length > 0) {
43+
return {
44+
isCrossChainDispute: true,
45+
crossChainId: crossChainDisputeEvents[0].args._arbitrableChainId ?? 0n,
46+
crossChainTemplateId: crossChainDisputeEvents[0].args._templateId ?? 0n,
47+
crossChainArbitrableAddress: crossChainDisputeEvents[0].args._arbitrable ?? "0x",
48+
};
49+
} else {
50+
return {
51+
isCrossChainDispute: false,
52+
crossChainId: 0n,
53+
crossChainTemplateId: 0n,
54+
crossChainArbitrableAddress: "0x",
55+
};
56+
}
57+
} catch (error) {
58+
if (error instanceof HttpRequestError || error instanceof RpcError) {
59+
debounceErrorToast("RPC failed!, Please avoid voting.");
60+
}
61+
throw Error;
5362
}
5463
} else throw Error;
5564
},

web/src/pages/Cases/CaseDetails/Overview/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ interface IOverview {
3939

4040
const Overview: React.FC<IOverview> = ({ arbitrable, courtID, currentPeriodIndex }) => {
4141
const { id } = useParams();
42-
const { data: disputeDetails } = usePopulatedDisputeData(id, arbitrable);
42+
const { data: disputeDetails, isError } = usePopulatedDisputeData(id, arbitrable);
4343
const { data: dispute } = useDisputeDetailsQuery(id);
4444
const { data: courtPolicy } = useCourtPolicy(courtID);
4545
const { data: votingHistory } = useVotingHistory(id);
@@ -52,7 +52,7 @@ const Overview: React.FC<IOverview> = ({ arbitrable, courtID, currentPeriodIndex
5252
return (
5353
<>
5454
<Container>
55-
<DisputeContext disputeDetails={disputeDetails} />
55+
<DisputeContext disputeDetails={disputeDetails} isRpcError={isError} />
5656
<Divider />
5757

5858
<Verdict arbitrable={arbitrable} />

web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getDrawnJurorsWithCount } from "utils/getDrawnJurorsWithCount";
1111
import { useDisputeDetailsQuery } from "hooks/queries/useDisputeDetailsQuery";
1212
import VotesAccordion from "./VotesDetails";
1313
import Skeleton from "react-loading-skeleton";
14+
import { INVALID_DISPUTE_DATA_ERROR, RPC_ERROR } from "consts/index";
1415

1516
const Container = styled.div``;
1617

@@ -24,7 +25,7 @@ const VotingHistory: React.FC<{ arbitrable?: `0x${string}`; isQuestion: boolean
2425
const { data: votingHistory } = useVotingHistory(id);
2526
const { data: disputeData } = useDisputeDetailsQuery(id);
2627
const [currentTab, setCurrentTab] = useState(0);
27-
const { data: disputeDetails } = usePopulatedDisputeData(id, arbitrable);
28+
const { data: disputeDetails, isError } = usePopulatedDisputeData(id, arbitrable);
2829
const rounds = votingHistory?.dispute?.rounds;
2930

3031
const localRounds = getLocalRounds(votingHistory?.dispute?.disputeKitDispute);
@@ -45,7 +46,7 @@ const VotingHistory: React.FC<{ arbitrable?: `0x${string}`; isQuestion: boolean
4546
{isQuestion && disputeDetails.question ? (
4647
<ReactMarkdown>{disputeDetails.question}</ReactMarkdown>
4748
) : (
48-
<ReactMarkdown>{"The dispute's template is not correct please vote refuse to arbitrate"}</ReactMarkdown>
49+
<ReactMarkdown>{isError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR}</ReactMarkdown>
4950
)}
5051
<StyledTabs
5152
currentValue={currentTab}

web/src/utils/debounceErrorToast.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { toast } from "react-toastify";
2+
import { OPTIONS as toastOptions } from "utils/wrapWithToast";
3+
4+
let timeoutId: NodeJS.Timeout;
5+
export const debounceErrorToast = (msg: string) => {
6+
if (timeoutId) clearTimeout(timeoutId);
7+
8+
timeoutId = setTimeout(() => {
9+
toast.error(msg, toastOptions);
10+
}, 5000);
11+
};

web/src/utils/graphqlQueryFnHelper.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import request from "graphql-request";
22
import { arbitrumSepolia } from "wagmi/chains";
33
import { TypedDocumentNode } from "@graphql-typed-document-node/core";
4+
import { debounceErrorToast } from "./debounceErrorToast";
45

56
const CHAINID_TO_DISPUTE_TEMPLATE_SUBGRAPH = {
67
[arbitrumSepolia.id]:
@@ -18,6 +19,11 @@ export const graphqlQueryFnHelper = async (
1819
isDisputeTemplate = false,
1920
chainId = arbitrumSepolia.id
2021
) => {
21-
const url = graphqlUrl(isDisputeTemplate, chainId);
22-
return request(url, query, parametersObject);
22+
try {
23+
const url = graphqlUrl(isDisputeTemplate, chainId);
24+
return await request(url, query, parametersObject);
25+
} catch (error) {
26+
console.log("Graph error: ", { error });
27+
debounceErrorToast("Graph query error: failed to fetch data.");
28+
}
2329
};

0 commit comments

Comments
 (0)