Skip to content

Support for Gated Dispute Kits and other improvements #2050

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 6 additions & 11 deletions web/src/consts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,9 @@ export const RPC_ERROR = `RPC Error: Unable to fetch dispute data. Please avoid

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

export const getDisputeKitName = (id: number): string | undefined => {
const universityDisputeKits: Record<number, string> = { 1: "Classic Dispute Kit" };
const neoDisputeKits: Record<number, string> = { 1: "Classic Dispute Kit" };
const testnetDisputeKits: Record<number, string> = { 1: "Classic Dispute Kit" };
const devnetDisputeKits: Record<number, string> = { 1: "Classic Dispute Kit", 2: "Shutter Dispute Kit" };

if (isKlerosUniversity()) return universityDisputeKits[id];
if (isKlerosNeo()) return neoDisputeKits[id];
if (isTestnetDeployment()) return testnetDisputeKits[id];
return devnetDisputeKits[id];
};
export enum DisputeKits {
Classic = "Classic Dispute Kit",
Shutter = "Shutter Dispute Kit",
Gated = "Gated Dispute Kit",
GatedShutter = "Gated Shutter Dispute Kit",
}
16 changes: 16 additions & 0 deletions web/src/context/NewDisputeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ interface IDisputeData extends IDisputeTemplate {
arbitrationCost?: string;
aliasesArray?: AliasArray[];
disputeKitId?: number;
disputeKitData?: IDisputeKitData;
}

export type IDisputeKitData = IGatedDisputeData | ISomeFutureDisputeData;

export interface IGatedDisputeData {
type: "gated";
isERC1155: boolean;
tokenGate: string;
tokenId: string;
}

// Placeholder
export interface ISomeFutureDisputeData {
type: "future";
contract: string;
}

interface INewDisputeContext {
Expand Down
1 change: 1 addition & 0 deletions web/src/hooks/queries/useDisputeDetailsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const disputeDetailsQuery = graphql(`
nbVotes
disputeKit {
id
address
}
}
currentRoundIndex
Expand Down
3 changes: 3 additions & 0 deletions web/src/hooks/queries/useSupportedDisputeKits.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useQuery } from "@tanstack/react-query";

import { useGraphqlBatcher } from "context/GraphqlBatcher";

import { graphql } from "src/graphql";
import { SupportedDisputeKitsQuery } from "src/graphql/graphql";

Expand All @@ -8,6 +10,7 @@ const supportedDisputeKitsQuery = graphql(`
court(id: $id) {
supportedDisputeKits {
id
address
}
}
}
Expand Down
163 changes: 163 additions & 0 deletions web/src/hooks/useDisputeKitAddresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { useEffect, useState } from "react";

import { useChainId } from "wagmi";

import { DisputeKits } from "consts/index";

interface UseDisputeKitAddressesParams {
disputeKitAddress?: string;
}

interface UseDisputeKitAddressesAllReturn {
availableDisputeKits: Record<string, DisputeKits>;
isLoading: boolean;
error: string | null;
}

const DISPUTE_KIT_CONFIG = {
[DisputeKits.Classic]: "disputeKitClassicAddress",
[DisputeKits.Shutter]: "disputeKitShutterAddress",
[DisputeKits.Gated]: "disputeKitGatedAddress",
[DisputeKits.GatedShutter]: "disputeKitGatedShutterAddress",
} as const;

/**
* Hook to get dispute kit name based on address
* @param disputeKitAddress - Optional specific dispute kit address to identify
* @returns The human-readable name of the dispute kit and loading state
*/
export const useDisputeKitAddresses = ({ disputeKitAddress }: UseDisputeKitAddressesParams = {}) => {
const chainId = useChainId();
const [disputeKitName, setDisputeKitName] = useState<DisputeKits | undefined>(undefined);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const loadDisputeKitName = async () => {
try {
setIsLoading(true);
setError(null);

// If no dispute kit address is provided, we can't determine the type
if (!disputeKitAddress) {
setDisputeKitName(undefined);
setIsLoading(false);
return;
}

// If no chainId, we can't look up from generated contracts
if (!chainId) {
setDisputeKitName(undefined);
setIsLoading(false);
return;
}

// Dynamic import to handle cases where generated contracts might not be available
try {
const generatedContracts = await import("hooks/contracts/generated");

// Check each dispute kit to see if the address matches
for (const [humanName, contractKey] of Object.entries(DISPUTE_KIT_CONFIG)) {
const addressMapping = generatedContracts[contractKey as keyof typeof generatedContracts];

if (addressMapping && typeof addressMapping === "object" && chainId in addressMapping) {
const contractAddress = addressMapping[chainId as keyof typeof addressMapping] as string;
if (
contractAddress &&
typeof contractAddress === "string" &&
contractAddress.toLowerCase() === disputeKitAddress.toLowerCase()
) {
setDisputeKitName(humanName as DisputeKits);
return;
}
}
}

// If no address matches, return undefined
setDisputeKitName(undefined);
} catch {
// If we can't import generated contracts, return undefined
setDisputeKitName(undefined);
}
Comment on lines +78 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Empty catch block suppresses errors silently

The empty catch block could hide important errors during development.

Log the error for debugging purposes:

-        } catch {
+        } catch (importError) {
+          console.warn('Failed to import generated contracts:', importError);
           // If we can't import generated contracts, return undefined
           setDisputeKitName(undefined);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch {
// If we can't import generated contracts, return undefined
setDisputeKitName(undefined);
}
} catch (importError) {
console.warn('Failed to import generated contracts:', importError);
// If we can't import generated contracts, return undefined
setDisputeKitName(undefined);
}
🤖 Prompt for AI Agents
In web/src/hooks/useDisputeKitAddresses.ts around lines 78 to 81, the catch
block is empty and silently suppresses errors. Modify the catch block to accept
the error object and log it using console.error or a similar logging method to
ensure errors are visible during development and debugging.

} catch (err) {
console.error("Failed to determine dispute kit name:", err);
setError("Failed to determine dispute kit type");
setDisputeKitName(undefined);
} finally {
setIsLoading(false);
}
};

loadDisputeKitName();
}, [chainId, disputeKitAddress]);

return {
disputeKitName,
isLoading,
error,
};
};

/**
* Hook to get all dispute kit addresses for the current chain
* @returns All dispute kit addresses, loading state, and error state
*/
export const useDisputeKitAddressesAll = (): UseDisputeKitAddressesAllReturn => {
const chainId = useChainId();
const [availableDisputeKits, setAvailableDisputeKits] = useState<Record<string, DisputeKits>>({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const loadAllDisputeKitAddresses = async () => {
try {
setIsLoading(true);
setError(null);

// If no chainId, we can't look up from generated contracts
if (!chainId) {
setAvailableDisputeKits({});
setIsLoading(false);
return;
}

// Dynamic import to handle cases where generated contracts might not be available
try {
const generatedContracts = await import("hooks/contracts/generated");
const newAvailableDisputeKits: Record<string, DisputeKits> = {};

// Iterate through all dispute kits and get their addresses
for (const [humanName, contractKey] of Object.entries(DISPUTE_KIT_CONFIG)) {
const addressMapping = generatedContracts[contractKey as keyof typeof generatedContracts];

if (addressMapping && typeof addressMapping === "object" && chainId in addressMapping) {
const contractAddress = addressMapping[chainId as keyof typeof addressMapping] as string;
if (contractAddress && typeof contractAddress === "string") {
newAvailableDisputeKits[contractAddress.toLowerCase()] = humanName as DisputeKits;
}
}
}

setAvailableDisputeKits(newAvailableDisputeKits);
} catch {
// If we can't import generated contracts, return empty object
setAvailableDisputeKits({});
}
Comment on lines +142 to +145
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Empty catch block suppresses errors silently

The empty catch block could hide important errors during development.

Log the error for debugging purposes:

-        } catch {
+        } catch (importError) {
+          console.warn('Failed to import generated contracts:', importError);
           // If we can't import generated contracts, return empty object
           setAvailableDisputeKits({});
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch {
// If we can't import generated contracts, return empty object
setAvailableDisputeKits({});
}
} catch (importError) {
console.warn('Failed to import generated contracts:', importError);
// If we can't import generated contracts, return empty object
setAvailableDisputeKits({});
}
🤖 Prompt for AI Agents
In web/src/hooks/useDisputeKitAddresses.ts around lines 142 to 145, the catch
block is empty and silently suppresses errors, which can hide important issues
during development. Modify the catch block to log the caught error before
setting available dispute kits to an empty object. This will help in debugging
by making the error visible in the console or logs.

} catch (err) {
console.error("Failed to load dispute kit addresses:", err);
setError("Failed to load dispute kit addresses");
setAvailableDisputeKits({});
} finally {
setIsLoading(false);
}
};

loadAllDisputeKitAddresses();
}, [chainId]);

return {
availableDisputeKits,
isLoading,
error,
};
};
Comment on lines +1 to +163
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding unit tests for the new hooks

These hooks contain critical logic for dispute kit identification and would benefit from comprehensive unit tests to ensure reliability.

Would you like me to generate unit tests for these hooks or open an issue to track this task?

🤖 Prompt for AI Agents
In web/src/hooks/useDisputeKitAddresses.ts from lines 1 to 163, the new hooks
useDisputeKitAddresses and useDisputeKitAddressesAll lack unit tests. To fix
this, create comprehensive unit tests covering various scenarios such as valid
and invalid dispute kit addresses, missing chainId, and error handling during
dynamic imports. Use a testing framework like Jest with React Testing Library to
mock dependencies like useChainId and the dynamic import of generated contracts,
ensuring the hooks behave correctly and handle loading and error states as
expected.

15 changes: 8 additions & 7 deletions web/src/pages/Cases/CaseDetails/Appeal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from "react";
import styled, { css } from "styled-components";

import { useToggle } from "react-use";
import { useParams } from "react-router-dom";
import { useToggle } from "react-use";

import { DisputeKits } from "consts/index";
import { Periods } from "consts/periods";
import { getDisputeKitName } from "consts/index";
import { useDisputeKitAddresses } from "hooks/useDisputeKitAddresses";

import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";

import { landscapeStyle } from "styles/landscapeStyle";
Expand Down Expand Up @@ -49,11 +51,10 @@ const Appeal: React.FC<{ currentPeriodIndex: number }> = ({ currentPeriodIndex }
const [isAppealMiniGuideOpen, toggleAppealMiniGuide] = useToggle(false);
const { id } = useParams();
const { data: disputeData } = useDisputeDetailsQuery(id);
const disputeKitId = disputeData?.dispute?.currentRound?.disputeKit?.id;
const disputeKitName = disputeKitId ? getDisputeKitName(Number(disputeKitId))?.toLowerCase() : "";
const isClassicDisputeKit = disputeKitName?.includes("classic") ?? false;
const isShutterDisputeKit = disputeKitName?.includes("shutter") ?? false;

const disputeKitAddress = disputeData?.dispute?.currentRound?.disputeKit?.address;
const { disputeKitName } = useDisputeKitAddresses({ disputeKitAddress });
const isClassicDisputeKit = disputeKitName === DisputeKits.Classic;
const isShutterDisputeKit = disputeKitName === DisputeKits.Shutter;
return (
<Container>
{Periods.appeal === currentPeriodIndex ? (
Expand Down
14 changes: 7 additions & 7 deletions web/src/pages/Cases/CaseDetails/Voting/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { useAccount } from "wagmi";

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

import { DisputeKits } from "consts/index";
import { Periods } from "consts/periods";
import { useDisputeKitAddresses } from "hooks/useDisputeKitAddresses";
import { useLockOverlayScroll } from "hooks/useLockOverlayScroll";
import { useVotingContext } from "hooks/useVotingContext";
import { formatDate } from "utils/date";
Expand All @@ -17,8 +19,8 @@ import { isLastRound } from "utils/isLastRound";
import { useAppealCost } from "queries/useAppealCost";
import { DisputeDetailsQuery, useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";

import { responsiveSize } from "styles/responsiveSize";
import { landscapeStyle } from "styles/landscapeStyle";
import { responsiveSize } from "styles/responsiveSize";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Unused import detected

The responsiveSize import is not used anywhere in this file.

Remove the unused import:

-import { responsiveSize } from "styles/responsiveSize";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { responsiveSize } from "styles/responsiveSize";
🤖 Prompt for AI Agents
In web/src/pages/Cases/CaseDetails/Voting/index.tsx at line 23, the import
statement for responsiveSize is unused. Remove the entire line importing
responsiveSize to clean up the code and avoid unnecessary imports.


import { getPeriodEndTimestamp } from "components/DisputeView";
import InfoCard from "components/InfoCard";
Expand All @@ -28,8 +30,6 @@ import Classic from "./Classic";
import Shutter from "./Shutter";
import VotingHistory from "./VotingHistory";

import { getDisputeKitName } from "consts/index";

const Container = styled.div`
padding: 20px 16px 16px;

Expand Down Expand Up @@ -70,10 +70,10 @@ const Voting: React.FC<IVoting> = ({ arbitrable, currentPeriodIndex, dispute })
const timesPerPeriod = disputeData?.dispute?.court?.timesPerPeriod;
const finalDate = useFinalDate(lastPeriodChange, currentPeriodIndex, timesPerPeriod);

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

const isCommitOrVotePeriod = useMemo(
() => [Periods.vote, Periods.commit].includes(currentPeriodIndex),
Expand Down
17 changes: 15 additions & 2 deletions web/src/pages/Resolver/Briefing/Description.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useRef, useEffect } from "react";
import styled, { css } from "styled-components";

import { Textarea } from "@kleros/ui-components-library";
Expand All @@ -17,6 +17,7 @@ const Container = styled.div`
flex-direction: column;
align-items: center;
`;

const StyledTextArea = styled(Textarea)`
width: 84vw;
height: 300px;
Expand All @@ -26,15 +27,26 @@ const StyledTextArea = styled(Textarea)`
`
)}
`;

const Description: React.FC = () => {
const { disputeData, setDisputeData } = useNewDisputeContext();
const containerRef = useRef<HTMLDivElement>(null);

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

useEffect(() => {
if (containerRef.current) {
const textareaElement = containerRef.current.querySelector("textarea");
if (textareaElement) {
textareaElement.focus();
}
}
}, []);

return (
<Container>
<Container ref={containerRef}>
<Header text="Describe the case" />
<StyledTextArea
dir="auto"
Expand All @@ -46,4 +58,5 @@ const Description: React.FC = () => {
</Container>
);
};

export default Description;
16 changes: 14 additions & 2 deletions web/src/pages/Resolver/Briefing/Title.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useRef, useEffect } from "react";
import styled, { css } from "styled-components";

import { Field } from "@kleros/ui-components-library";
Expand Down Expand Up @@ -33,15 +33,26 @@ const StyledField = styled(Field)`
`
)}
`;

const Title: React.FC = () => {
const { disputeData, setDisputeData } = useNewDisputeContext();
const containerRef = useRef<HTMLDivElement>(null);

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

useEffect(() => {
if (containerRef.current) {
const inputElement = containerRef.current.querySelector("input");
if (inputElement) {
inputElement.focus();
}
}
}, []);

return (
<Container>
<Container ref={containerRef}>
<Header text="Choose a title" />
<StyledField
dir="auto"
Expand All @@ -53,4 +64,5 @@ const Title: React.FC = () => {
</Container>
);
};

export default Title;
Loading
Loading