Skip to content

feat: shutter support in dispute commiting & appeal #1994

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 18 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5ac64f5
feat: shutter support in dispute commiting
kemuru May 15, 2025
724a949
Merge branch 'feat/shutter-dispute-kit' into feat(web)/shutter-fronte…
kemuru May 15, 2025
8a5b2f4
feat: shutter appeal support
kemuru May 15, 2025
6b240a6
Merge branch 'feat/shutter-dispute-kit' into feat(web)/shutter-fronte…
kemuru May 15, 2025
05711fe
Merge branch 'feat/shutter-dispute-kit' into feat(web)/shutter-fronte…
kemuru May 20, 2025
729777d
feat: postinstall script, use correct commit function
kemuru May 20, 2025
8bf3772
fix: vote hashing during commitment must follow DisputeKitShutter.has…
jaybuidl May 20, 2025
8c74d4a
feat: decryption delay is the remaining of the commit period
kemuru May 22, 2025
47049eb
Merge branch 'feat/shutter-dispute-kit' into feat(web)/shutter-fronte…
jaybuidl May 22, 2025
bae8db4
chore: remove postinstall script, tell vite where to find it
kemuru May 23, 2025
e983fdd
Merge branch 'feat/shutter-dispute-kit' into feat(web)/shutter-fronte…
kemuru May 26, 2025
2e5d2a7
feat: support for revealing shutter commit from the frontend
kemuru May 26, 2025
bf7a3c0
chore: remove console logs, few code smells
kemuru May 26, 2025
c8bf188
Merge branch 'feat/shutter-dispute-kit' into feat(web)/shutter-fronte…
kemuru May 28, 2025
04605db
chore: extra 5 min decryptiondelay
kemuru May 29, 2025
b3817bd
fix: trycatch in case voting fails
kemuru May 29, 2025
f816ca3
Merge branch 'feat/shutter-dispute-kit' into feat(web)/shutter-fronte…
kemuru May 29, 2025
4e2dea9
Merge branch 'feat/shutter-dispute-kit' into feat(web)/shutter-fronte…
kemuru May 29, 2025
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
2 changes: 2 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"typescript": "^5.6.3",
"vite": "^5.4.11",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-static-copy": "^3.0.0",
"vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^4.3.2"
},
Expand All @@ -94,6 +95,7 @@
"@reown/appkit-adapter-wagmi": "^1.7.1",
"@sentry/react": "^7.120.0",
"@sentry/tracing": "^7.120.0",
"@shutter-network/shutter-sdk": "^0.0.1",
"@solana/wallet-adapter-react": "^0.15.36",
"@solana/web3.js": "^1.98.0",
"@tanstack/react-query": "^5.69.0",
Expand Down
3 changes: 3 additions & 0 deletions web/src/hooks/queries/useDisputeDetailsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const disputeDetailsQuery = graphql(`
currentRound {
id
nbVotes
disputeKit {
id
}
}
currentRoundIndex
isCrossChain
Expand Down
147 changes: 147 additions & 0 deletions web/src/pages/Cases/CaseDetails/Appeal/Shutter/Fund.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { useMemo, useState } from "react";
import styled from "styled-components";

import { useParams } from "react-router-dom";
import { useDebounce } from "react-use";
import { useAccount, useBalance, usePublicClient } from "wagmi";
import { Field, Button } from "@kleros/ui-components-library";

import { REFETCH_INTERVAL } from "consts/index";
import { useSimulateDisputeKitShutterFundAppeal, useWriteDisputeKitShutterFundAppeal } from "hooks/contracts/generated";
import { useSelectedOptionContext, useFundingContext, useCountdownContext } from "hooks/useClassicAppealContext";
import { useParsedAmount } from "hooks/useParsedAmount";

import { isUndefined } from "utils/index";
import { wrapWithToast } from "utils/wrapWithToast";

import { EnsureChain } from "components/EnsureChain";
import { ErrorButtonMessage } from "components/ErrorButtonMessage";
import ClosedCircleIcon from "components/StyledIcons/ClosedCircleIcon";

const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
`;

const StyledField = styled(Field)`
width: 100%;
& > input {
text-align: center;
}
&:before {
position: absolute;
content: "ETH";
right: 32px;
top: 50%;
transform: translateY(-50%);
color: ${({ theme }) => theme.primaryText};
}
`;

const StyledButton = styled(Button)`
margin: auto;
margin-top: 4px;
`;

const StyledLabel = styled.label`
align-self: flex-start;
`;

const useNeedFund = () => {
const { loserSideCountdown } = useCountdownContext();
const { fundedChoices, winningChoice } = useFundingContext();
return (
(loserSideCountdown ?? 0) > 0 ||
(!isUndefined(fundedChoices) &&
!isUndefined(winningChoice) &&
fundedChoices.length > 0 &&
!fundedChoices.includes(winningChoice))
);
};

const useFundAppeal = (parsedAmount: bigint, insufficientBalance: boolean) => {
const { id } = useParams();
const { selectedOption } = useSelectedOptionContext();
const {
data: fundAppealConfig,
isLoading,
isError,
} = useSimulateDisputeKitShutterFundAppeal({
query: { enabled: !isUndefined(id) && !isUndefined(selectedOption) && !insufficientBalance },
args: [BigInt(id ?? 0), BigInt(selectedOption?.id ?? 0)],
value: parsedAmount,
});
const { writeContractAsync: fundAppeal } = useWriteDisputeKitShutterFundAppeal();
return { fundAppeal, fundAppealConfig, isLoading, isError };
};

interface IFund {
amount: `${number}`;
setAmount: (val: string) => void;
setIsOpen: (val: boolean) => void;
}

const Fund: React.FC<IFund> = ({ amount, setAmount, setIsOpen }) => {
const needFund = useNeedFund();
const { address, isDisconnected } = useAccount();
const { data: balance } = useBalance({
query: { refetchInterval: REFETCH_INTERVAL },
address,
});
const publicClient = usePublicClient();
const [isSending, setIsSending] = useState(false);
const [debouncedAmount, setDebouncedAmount] = useState<`${number}` | "">("");
useDebounce(() => setDebouncedAmount(amount), 500, [amount]);
const parsedAmount = useParsedAmount(debouncedAmount as `${number}`);
const insufficientBalance = useMemo(() => balance && balance.value < parsedAmount, [balance, parsedAmount]);
const { fundAppealConfig, fundAppeal, isLoading, isError } = useFundAppeal(parsedAmount, insufficientBalance);
const isFundDisabled = useMemo(
() =>
isDisconnected ||
isSending ||
!balance ||
insufficientBalance ||
Number(parsedAmount) <= 0 ||
isError ||
isLoading,
[isDisconnected, isSending, balance, insufficientBalance, parsedAmount, isError, isLoading]
);

return needFund ? (
<Container>
<StyledLabel>How much ETH do you want to contribute?</StyledLabel>
<StyledField
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount to fund"
/>
<EnsureChain>
<div>
<StyledButton
disabled={isFundDisabled}
isLoading={(isSending || isLoading) && !insufficientBalance}
text={isDisconnected ? "Connect to Fund" : "Fund"}
onClick={() => {
if (fundAppeal && fundAppealConfig && publicClient) {
setIsSending(true);
wrapWithToast(async () => await fundAppeal(fundAppealConfig.request), publicClient)
.then((res) => setIsOpen(res.status))
.finally(() => setIsSending(false));
}
}}
/>
{insufficientBalance && (
<ErrorButtonMessage>
<ClosedCircleIcon /> Insufficient balance
</ErrorButtonMessage>
)}
</div>
</EnsureChain>
</Container>
) : null;
};

export default Fund;
49 changes: 49 additions & 0 deletions web/src/pages/Cases/CaseDetails/Appeal/Shutter/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useState } from "react";
import { useSelectedOptionContext } from "hooks/useClassicAppealContext";
import Popup, { PopupType } from "components/Popup";
import AppealIcon from "svgs/icons/appeal.svg";
import HowItWorks from "components/HowItWorks";
import Appeal from "components/Popup/MiniGuides/Appeal";
import { AppealHeader, StyledTitle } from "..";
import Options from "../Classic/Options";
import Fund from "./Fund";

interface IShutter {
isAppealMiniGuideOpen: boolean;
toggleAppealMiniGuide: () => void;
}

const Shutter: React.FC<IShutter> = ({ isAppealMiniGuideOpen, toggleAppealMiniGuide }) => {
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [amount, setAmount] = useState("");
const { selectedOption } = useSelectedOptionContext();

return (
<>
{isPopupOpen && (
<Popup
title="Thanks for Funding the Appeal"
icon={AppealIcon}
popupType={PopupType.APPEAL}
setIsOpen={setIsPopupOpen}
setAmount={setAmount}
option={selectedOption?.title ?? ""}
amount={amount}
/>
)}
<AppealHeader>
<StyledTitle>Appeal crowdfunding</StyledTitle>
<HowItWorks
isMiniGuideOpen={isAppealMiniGuideOpen}
toggleMiniGuide={toggleAppealMiniGuide}
MiniGuideComponent={Appeal}
/>
</AppealHeader>
<label>The jury decision is appealed when two options are fully funded.</label>
<Options setAmount={setAmount} />
<Fund amount={amount as `${number}`} setAmount={setAmount} setIsOpen={setIsPopupOpen} />
</>
);
};

export default Shutter;
20 changes: 18 additions & 2 deletions web/src/pages/Cases/CaseDetails/Appeal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import React from "react";
import styled, { css } from "styled-components";

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

import { Periods } from "consts/periods";
import { ClassicAppealProvider } from "hooks/useClassicAppealContext";
import { getDisputeKitName } from "consts/index";
import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";

import { landscapeStyle } from "styles/landscapeStyle";
import { responsiveSize } from "styles/responsiveSize";

import AppealHistory from "./AppealHistory";
import Classic from "./Classic";
import Shutter from "./Shutter";

const Container = styled.div`
padding: 16px;
Expand Down Expand Up @@ -44,11 +47,24 @@ export const StyledTitle = styled.h1`

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;

return (
<Container>
{Periods.appeal === currentPeriodIndex ? (
<Classic isAppealMiniGuideOpen={isAppealMiniGuideOpen} toggleAppealMiniGuide={toggleAppealMiniGuide} />
<>
{isClassicDisputeKit && (
<Classic isAppealMiniGuideOpen={isAppealMiniGuideOpen} toggleAppealMiniGuide={toggleAppealMiniGuide} />
)}
{isShutterDisputeKit && (
<Shutter isAppealMiniGuideOpen={isAppealMiniGuideOpen} toggleAppealMiniGuide={toggleAppealMiniGuide} />
)}
</>
) : (
<AppealHistory isAppealMiniGuideOpen={isAppealMiniGuideOpen} toggleAppealMiniGuide={toggleAppealMiniGuide} />
)}
Expand Down
2 changes: 1 addition & 1 deletion web/src/pages/Cases/CaseDetails/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ const useTimeline = (dispute: DisputeDetailsQuery["dispute"], currentItemIndex:
}));
};

const getDeadline = (
export const getDeadline = (
currentPeriodIndex: number,
lastPeriodChange?: string,
timesPerPeriod?: string[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { wrapWithToast } from "utils/wrapWithToast";

import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";

import OptionsContainer from "./OptionsContainer";
import OptionsContainer from "../OptionsContainer";

const Container = styled.div`
width: 100%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";

import InfoCard from "components/InfoCard";

import JustificationArea from "./JustificationArea";
import JustificationArea from "../JustificationArea";
import { Answer } from "@kleros/kleros-sdk";
import { EnsureChain } from "components/EnsureChain";

Expand Down
2 changes: 1 addition & 1 deletion web/src/pages/Cases/CaseDetails/Voting/Classic/Vote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { wrapWithToast } from "utils/wrapWithToast";

import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";

import OptionsContainer from "./OptionsContainer";
import OptionsContainer from "../OptionsContainer";

const Container = styled.div`
width: 100%;
Expand Down
Loading