Skip to content

Commit 1ff6beb

Browse files
committed
feat: recognise the new dispute kits, matching by address, resolver UX
1 parent f4ed9a0 commit 1ff6beb

File tree

11 files changed

+264
-39
lines changed

11 files changed

+264
-39
lines changed

web/src/consts/index.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,9 @@ export const RPC_ERROR = `RPC Error: Unable to fetch dispute data. Please avoid
4343

4444
export const spamEvidencesIds: string[] = (import.meta.env.REACT_APP_SPAM_EVIDENCES_IDS ?? "").split(",");
4545

46-
export const getDisputeKitName = (id: number): string | undefined => {
47-
const universityDisputeKits: Record<number, string> = { 1: "Classic Dispute Kit" };
48-
const neoDisputeKits: Record<number, string> = { 1: "Classic Dispute Kit" };
49-
const testnetDisputeKits: Record<number, string> = { 1: "Classic Dispute Kit" };
50-
const devnetDisputeKits: Record<number, string> = { 1: "Classic Dispute Kit", 2: "Shutter Dispute Kit" };
51-
52-
if (isKlerosUniversity()) return universityDisputeKits[id];
53-
if (isKlerosNeo()) return neoDisputeKits[id];
54-
if (isTestnetDeployment()) return testnetDisputeKits[id];
55-
return devnetDisputeKits[id];
56-
};
46+
export enum DisputeKits {
47+
Classic = "Classic Dispute Kit",
48+
Shutter = "Shutter Dispute Kit",
49+
Gated = "Gated Dispute Kit",
50+
GatedShutter = "Gated Shutter Dispute Kit",
51+
}

web/src/hooks/queries/useDisputeDetailsQuery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const disputeDetailsQuery = graphql(`
3030
nbVotes
3131
disputeKit {
3232
id
33+
address
3334
}
3435
}
3536
currentRoundIndex

web/src/hooks/queries/useSupportedDisputeKits.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { useQuery } from "@tanstack/react-query";
2+
23
import { useGraphqlBatcher } from "context/GraphqlBatcher";
4+
35
import { graphql } from "src/graphql";
46
import { SupportedDisputeKitsQuery } from "src/graphql/graphql";
57

@@ -8,6 +10,7 @@ const supportedDisputeKitsQuery = graphql(`
810
court(id: $id) {
911
supportedDisputeKits {
1012
id
13+
address
1114
}
1215
}
1316
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { useEffect, useState } from "react";
2+
3+
import { useChainId } from "wagmi";
4+
5+
import { DisputeKits } from "consts/index";
6+
7+
interface UseDisputeKitAddressesParams {
8+
disputeKitAddress?: string;
9+
}
10+
11+
interface UseDisputeKitAddressesAllReturn {
12+
availableDisputeKits: Record<string, DisputeKits>;
13+
isLoading: boolean;
14+
error: string | null;
15+
}
16+
17+
const DISPUTE_KIT_CONFIG = {
18+
[DisputeKits.Classic]: "disputeKitClassicAddress",
19+
[DisputeKits.Shutter]: "disputeKitShutterAddress",
20+
[DisputeKits.Gated]: "disputeKitGatedAddress",
21+
[DisputeKits.GatedShutter]: "disputeKitGatedShutterAddress",
22+
} as const;
23+
24+
/**
25+
* Hook to get dispute kit name based on address
26+
* @param disputeKitAddress - Optional specific dispute kit address to identify
27+
* @returns The human-readable name of the dispute kit and loading state
28+
*/
29+
export const useDisputeKitAddresses = ({ disputeKitAddress }: UseDisputeKitAddressesParams = {}) => {
30+
const chainId = useChainId();
31+
const [disputeKitName, setDisputeKitName] = useState<DisputeKits | undefined>(undefined);
32+
const [isLoading, setIsLoading] = useState(true);
33+
const [error, setError] = useState<string | null>(null);
34+
35+
useEffect(() => {
36+
const loadDisputeKitName = async () => {
37+
try {
38+
setIsLoading(true);
39+
setError(null);
40+
41+
// If no dispute kit address is provided, we can't determine the type
42+
if (!disputeKitAddress) {
43+
setDisputeKitName(undefined);
44+
setIsLoading(false);
45+
return;
46+
}
47+
48+
// If no chainId, we can't look up from generated contracts
49+
if (!chainId) {
50+
setDisputeKitName(undefined);
51+
setIsLoading(false);
52+
return;
53+
}
54+
55+
// Dynamic import to handle cases where generated contracts might not be available
56+
try {
57+
const generatedContracts = await import("hooks/contracts/generated");
58+
59+
// Check each dispute kit to see if the address matches
60+
for (const [humanName, contractKey] of Object.entries(DISPUTE_KIT_CONFIG)) {
61+
const addressMapping = generatedContracts[contractKey as keyof typeof generatedContracts];
62+
63+
if (addressMapping && typeof addressMapping === "object" && chainId in addressMapping) {
64+
const contractAddress = addressMapping[chainId as keyof typeof addressMapping] as string;
65+
if (
66+
contractAddress &&
67+
typeof contractAddress === "string" &&
68+
contractAddress.toLowerCase() === disputeKitAddress.toLowerCase()
69+
) {
70+
setDisputeKitName(humanName as DisputeKits);
71+
return;
72+
}
73+
}
74+
}
75+
76+
// If no address matches, return undefined
77+
setDisputeKitName(undefined);
78+
} catch {
79+
// If we can't import generated contracts, return undefined
80+
setDisputeKitName(undefined);
81+
}
82+
} catch (err) {
83+
console.error("Failed to determine dispute kit name:", err);
84+
setError("Failed to determine dispute kit type");
85+
setDisputeKitName(undefined);
86+
} finally {
87+
setIsLoading(false);
88+
}
89+
};
90+
91+
loadDisputeKitName();
92+
}, [chainId, disputeKitAddress]);
93+
94+
return {
95+
disputeKitName,
96+
isLoading,
97+
error,
98+
};
99+
};
100+
101+
/**
102+
* Hook to get all dispute kit addresses for the current chain
103+
* @returns All dispute kit addresses, loading state, and error state
104+
*/
105+
export const useDisputeKitAddressesAll = (): UseDisputeKitAddressesAllReturn => {
106+
const chainId = useChainId();
107+
const [availableDisputeKits, setAvailableDisputeKits] = useState<Record<string, DisputeKits>>({});
108+
const [isLoading, setIsLoading] = useState(true);
109+
const [error, setError] = useState<string | null>(null);
110+
111+
useEffect(() => {
112+
const loadAllDisputeKitAddresses = async () => {
113+
try {
114+
setIsLoading(true);
115+
setError(null);
116+
117+
// If no chainId, we can't look up from generated contracts
118+
if (!chainId) {
119+
setAvailableDisputeKits({});
120+
setIsLoading(false);
121+
return;
122+
}
123+
124+
// Dynamic import to handle cases where generated contracts might not be available
125+
try {
126+
const generatedContracts = await import("hooks/contracts/generated");
127+
const newAvailableDisputeKits: Record<string, DisputeKits> = {};
128+
129+
// Iterate through all dispute kits and get their addresses
130+
for (const [humanName, contractKey] of Object.entries(DISPUTE_KIT_CONFIG)) {
131+
const addressMapping = generatedContracts[contractKey as keyof typeof generatedContracts];
132+
133+
if (addressMapping && typeof addressMapping === "object" && chainId in addressMapping) {
134+
const contractAddress = addressMapping[chainId as keyof typeof addressMapping] as string;
135+
if (contractAddress && typeof contractAddress === "string") {
136+
newAvailableDisputeKits[contractAddress.toLowerCase()] = humanName as DisputeKits;
137+
}
138+
}
139+
}
140+
141+
setAvailableDisputeKits(newAvailableDisputeKits);
142+
} catch {
143+
// If we can't import generated contracts, return empty object
144+
setAvailableDisputeKits({});
145+
}
146+
} catch (err) {
147+
console.error("Failed to load dispute kit addresses:", err);
148+
setError("Failed to load dispute kit addresses");
149+
setAvailableDisputeKits({});
150+
} finally {
151+
setIsLoading(false);
152+
}
153+
};
154+
155+
loadAllDisputeKitAddresses();
156+
}, [chainId]);
157+
158+
return {
159+
availableDisputeKits,
160+
isLoading,
161+
error,
162+
};
163+
};

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import React from "react";
22
import styled, { css } from "styled-components";
33

4-
import { useToggle } from "react-use";
54
import { useParams } from "react-router-dom";
5+
import { useToggle } from "react-use";
66

7+
import { DisputeKits } from "consts/index";
78
import { Periods } from "consts/periods";
8-
import { getDisputeKitName } from "consts/index";
9+
import { useDisputeKitAddresses } from "hooks/useDisputeKitAddresses";
10+
911
import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
1012

1113
import { landscapeStyle } from "styles/landscapeStyle";
@@ -49,11 +51,10 @@ const Appeal: React.FC<{ currentPeriodIndex: number }> = ({ currentPeriodIndex }
4951
const [isAppealMiniGuideOpen, toggleAppealMiniGuide] = useToggle(false);
5052
const { id } = useParams();
5153
const { data: disputeData } = useDisputeDetailsQuery(id);
52-
const disputeKitId = disputeData?.dispute?.currentRound?.disputeKit?.id;
53-
const disputeKitName = disputeKitId ? getDisputeKitName(Number(disputeKitId))?.toLowerCase() : "";
54-
const isClassicDisputeKit = disputeKitName?.includes("classic") ?? false;
55-
const isShutterDisputeKit = disputeKitName?.includes("shutter") ?? false;
56-
54+
const disputeKitAddress = disputeData?.dispute?.currentRound?.disputeKit?.address;
55+
const { disputeKitName } = useDisputeKitAddresses({ disputeKitAddress });
56+
const isClassicDisputeKit = disputeKitName === DisputeKits.Classic;
57+
const isShutterDisputeKit = disputeKitName === DisputeKits.Shutter;
5758
return (
5859
<Container>
5960
{Periods.appeal === currentPeriodIndex ? (

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { useAccount } from "wagmi";
77

88
import VoteIcon from "svgs/icons/voted.svg";
99

10+
import { DisputeKits } from "consts/index";
1011
import { Periods } from "consts/periods";
12+
import { useDisputeKitAddresses } from "hooks/useDisputeKitAddresses";
1113
import { useLockOverlayScroll } from "hooks/useLockOverlayScroll";
1214
import { useVotingContext } from "hooks/useVotingContext";
1315
import { formatDate } from "utils/date";
@@ -17,8 +19,8 @@ import { isLastRound } from "utils/isLastRound";
1719
import { useAppealCost } from "queries/useAppealCost";
1820
import { DisputeDetailsQuery, useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
1921

20-
import { responsiveSize } from "styles/responsiveSize";
2122
import { landscapeStyle } from "styles/landscapeStyle";
23+
import { responsiveSize } from "styles/responsiveSize";
2224

2325
import { getPeriodEndTimestamp } from "components/DisputeView";
2426
import InfoCard from "components/InfoCard";
@@ -28,8 +30,6 @@ import Classic from "./Classic";
2830
import Shutter from "./Shutter";
2931
import VotingHistory from "./VotingHistory";
3032

31-
import { getDisputeKitName } from "consts/index";
32-
3333
const Container = styled.div`
3434
padding: 20px 16px 16px;
3535
@@ -70,10 +70,10 @@ const Voting: React.FC<IVoting> = ({ arbitrable, currentPeriodIndex, dispute })
7070
const timesPerPeriod = disputeData?.dispute?.court?.timesPerPeriod;
7171
const finalDate = useFinalDate(lastPeriodChange, currentPeriodIndex, timesPerPeriod);
7272

73-
const disputeKitId = disputeData?.dispute?.currentRound?.disputeKit?.id;
74-
const disputeKitName = disputeKitId ? getDisputeKitName(Number(disputeKitId)) : undefined;
75-
const isClassicDisputeKit = disputeKitName?.toLowerCase().includes("classic") ?? false;
76-
const isShutterDisputeKit = disputeKitName?.toLowerCase().includes("shutter") ?? false;
73+
const disputeKitAddress = disputeData?.dispute?.currentRound?.disputeKit?.address;
74+
const { disputeKitName } = useDisputeKitAddresses({ disputeKitAddress });
75+
const isClassicDisputeKit = disputeKitName === DisputeKits.Classic;
76+
const isShutterDisputeKit = disputeKitName === DisputeKits.Shutter;
7777

7878
const isCommitOrVotePeriod = useMemo(
7979
() => [Periods.vote, Periods.commit].includes(currentPeriodIndex),

web/src/pages/Resolver/Briefing/Description.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useRef, useEffect } from "react";
22
import styled, { css } from "styled-components";
33

44
import { Textarea } from "@kleros/ui-components-library";
@@ -17,6 +17,7 @@ const Container = styled.div`
1717
flex-direction: column;
1818
align-items: center;
1919
`;
20+
2021
const StyledTextArea = styled(Textarea)`
2122
width: 84vw;
2223
height: 300px;
@@ -26,15 +27,26 @@ const StyledTextArea = styled(Textarea)`
2627
`
2728
)}
2829
`;
30+
2931
const Description: React.FC = () => {
3032
const { disputeData, setDisputeData } = useNewDisputeContext();
33+
const containerRef = useRef<HTMLDivElement>(null);
3134

3235
const handleWrite = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
3336
setDisputeData({ ...disputeData, description: event.target.value });
3437
};
3538

39+
useEffect(() => {
40+
if (containerRef.current) {
41+
const textareaElement = containerRef.current.querySelector("textarea");
42+
if (textareaElement) {
43+
textareaElement.focus();
44+
}
45+
}
46+
}, []);
47+
3648
return (
37-
<Container>
49+
<Container ref={containerRef}>
3850
<Header text="Describe the case" />
3951
<StyledTextArea
4052
dir="auto"
@@ -46,4 +58,5 @@ const Description: React.FC = () => {
4658
</Container>
4759
);
4860
};
61+
4962
export default Description;

web/src/pages/Resolver/Briefing/Title.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useRef, useEffect } from "react";
22
import styled, { css } from "styled-components";
33

44
import { Field } from "@kleros/ui-components-library";
@@ -33,15 +33,26 @@ const StyledField = styled(Field)`
3333
`
3434
)}
3535
`;
36+
3637
const Title: React.FC = () => {
3738
const { disputeData, setDisputeData } = useNewDisputeContext();
39+
const containerRef = useRef<HTMLDivElement>(null);
3840

3941
const handleWrite = (event: React.ChangeEvent<HTMLInputElement>) => {
4042
setDisputeData({ ...disputeData, title: event.target.value });
4143
};
4244

45+
useEffect(() => {
46+
if (containerRef.current) {
47+
const inputElement = containerRef.current.querySelector("input");
48+
if (inputElement) {
49+
inputElement.focus();
50+
}
51+
}
52+
}, []);
53+
4354
return (
44-
<Container>
55+
<Container ref={containerRef}>
4556
<Header text="Choose a title" />
4657
<StyledField
4758
dir="auto"
@@ -53,4 +64,5 @@ const Title: React.FC = () => {
5364
</Container>
5465
);
5566
};
67+
5668
export default Title;

0 commit comments

Comments
 (0)