Skip to content

Commit 469039a

Browse files
authored
Merge pull request #1994 from kleros/feat(web)/shutter-frontend-rendering
feat: shutter support in dispute commiting & appeal
2 parents fe7eb85 + 4e2dea9 commit 469039a

File tree

19 files changed

+781
-15
lines changed

19 files changed

+781
-15
lines changed

web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"typescript": "^5.6.3",
7676
"vite": "^5.4.11",
7777
"vite-plugin-node-polyfills": "^0.23.0",
78+
"vite-plugin-static-copy": "^3.0.0",
7879
"vite-plugin-svgr": "^4.3.0",
7980
"vite-tsconfig-paths": "^4.3.2"
8081
},
@@ -94,6 +95,7 @@
9495
"@reown/appkit-adapter-wagmi": "^1.7.1",
9596
"@sentry/react": "^7.120.0",
9697
"@sentry/tracing": "^7.120.0",
98+
"@shutter-network/shutter-sdk": "^0.0.1",
9799
"@solana/wallet-adapter-react": "^0.15.36",
98100
"@solana/web3.js": "^1.98.0",
99101
"@tanstack/react-query": "^5.69.0",

web/src/hooks/queries/useDisputeDetailsQuery.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ const disputeDetailsQuery = graphql(`
2828
currentRound {
2929
id
3030
nbVotes
31+
disputeKit {
32+
id
33+
}
3134
}
3235
currentRoundIndex
3336
isCrossChain
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { useMemo, useState } from "react";
2+
import styled from "styled-components";
3+
4+
import { useParams } from "react-router-dom";
5+
import { useDebounce } from "react-use";
6+
import { useAccount, useBalance, usePublicClient } from "wagmi";
7+
import { Field, Button } from "@kleros/ui-components-library";
8+
9+
import { REFETCH_INTERVAL } from "consts/index";
10+
import { useSimulateDisputeKitShutterFundAppeal, useWriteDisputeKitShutterFundAppeal } from "hooks/contracts/generated";
11+
import { useSelectedOptionContext, useFundingContext, useCountdownContext } from "hooks/useClassicAppealContext";
12+
import { useParsedAmount } from "hooks/useParsedAmount";
13+
14+
import { isUndefined } from "utils/index";
15+
import { wrapWithToast } from "utils/wrapWithToast";
16+
17+
import { EnsureChain } from "components/EnsureChain";
18+
import { ErrorButtonMessage } from "components/ErrorButtonMessage";
19+
import ClosedCircleIcon from "components/StyledIcons/ClosedCircleIcon";
20+
21+
const Container = styled.div`
22+
display: flex;
23+
flex-direction: column;
24+
align-items: center;
25+
gap: 8px;
26+
`;
27+
28+
const StyledField = styled(Field)`
29+
width: 100%;
30+
& > input {
31+
text-align: center;
32+
}
33+
&:before {
34+
position: absolute;
35+
content: "ETH";
36+
right: 32px;
37+
top: 50%;
38+
transform: translateY(-50%);
39+
color: ${({ theme }) => theme.primaryText};
40+
}
41+
`;
42+
43+
const StyledButton = styled(Button)`
44+
margin: auto;
45+
margin-top: 4px;
46+
`;
47+
48+
const StyledLabel = styled.label`
49+
align-self: flex-start;
50+
`;
51+
52+
const useNeedFund = () => {
53+
const { loserSideCountdown } = useCountdownContext();
54+
const { fundedChoices, winningChoice } = useFundingContext();
55+
return (
56+
(loserSideCountdown ?? 0) > 0 ||
57+
(!isUndefined(fundedChoices) &&
58+
!isUndefined(winningChoice) &&
59+
fundedChoices.length > 0 &&
60+
!fundedChoices.includes(winningChoice))
61+
);
62+
};
63+
64+
const useFundAppeal = (parsedAmount: bigint, insufficientBalance: boolean) => {
65+
const { id } = useParams();
66+
const { selectedOption } = useSelectedOptionContext();
67+
const {
68+
data: fundAppealConfig,
69+
isLoading,
70+
isError,
71+
} = useSimulateDisputeKitShutterFundAppeal({
72+
query: { enabled: !isUndefined(id) && !isUndefined(selectedOption) && !insufficientBalance },
73+
args: [BigInt(id ?? 0), BigInt(selectedOption?.id ?? 0)],
74+
value: parsedAmount,
75+
});
76+
const { writeContractAsync: fundAppeal } = useWriteDisputeKitShutterFundAppeal();
77+
return { fundAppeal, fundAppealConfig, isLoading, isError };
78+
};
79+
80+
interface IFund {
81+
amount: `${number}`;
82+
setAmount: (val: string) => void;
83+
setIsOpen: (val: boolean) => void;
84+
}
85+
86+
const Fund: React.FC<IFund> = ({ amount, setAmount, setIsOpen }) => {
87+
const needFund = useNeedFund();
88+
const { address, isDisconnected } = useAccount();
89+
const { data: balance } = useBalance({
90+
query: { refetchInterval: REFETCH_INTERVAL },
91+
address,
92+
});
93+
const publicClient = usePublicClient();
94+
const [isSending, setIsSending] = useState(false);
95+
const [debouncedAmount, setDebouncedAmount] = useState<`${number}` | "">("");
96+
useDebounce(() => setDebouncedAmount(amount), 500, [amount]);
97+
const parsedAmount = useParsedAmount(debouncedAmount as `${number}`);
98+
const insufficientBalance = useMemo(() => balance && balance.value < parsedAmount, [balance, parsedAmount]);
99+
const { fundAppealConfig, fundAppeal, isLoading, isError } = useFundAppeal(parsedAmount, insufficientBalance);
100+
const isFundDisabled = useMemo(
101+
() =>
102+
isDisconnected ||
103+
isSending ||
104+
!balance ||
105+
insufficientBalance ||
106+
Number(parsedAmount) <= 0 ||
107+
isError ||
108+
isLoading,
109+
[isDisconnected, isSending, balance, insufficientBalance, parsedAmount, isError, isLoading]
110+
);
111+
112+
return needFund ? (
113+
<Container>
114+
<StyledLabel>How much ETH do you want to contribute?</StyledLabel>
115+
<StyledField
116+
type="number"
117+
value={amount}
118+
onChange={(e) => setAmount(e.target.value)}
119+
placeholder="Amount to fund"
120+
/>
121+
<EnsureChain>
122+
<div>
123+
<StyledButton
124+
disabled={isFundDisabled}
125+
isLoading={(isSending || isLoading) && !insufficientBalance}
126+
text={isDisconnected ? "Connect to Fund" : "Fund"}
127+
onClick={() => {
128+
if (fundAppeal && fundAppealConfig && publicClient) {
129+
setIsSending(true);
130+
wrapWithToast(async () => await fundAppeal(fundAppealConfig.request), publicClient)
131+
.then((res) => setIsOpen(res.status))
132+
.finally(() => setIsSending(false));
133+
}
134+
}}
135+
/>
136+
{insufficientBalance && (
137+
<ErrorButtonMessage>
138+
<ClosedCircleIcon /> Insufficient balance
139+
</ErrorButtonMessage>
140+
)}
141+
</div>
142+
</EnsureChain>
143+
</Container>
144+
) : null;
145+
};
146+
147+
export default Fund;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useState } from "react";
2+
import { useSelectedOptionContext } from "hooks/useClassicAppealContext";
3+
import Popup, { PopupType } from "components/Popup";
4+
import AppealIcon from "svgs/icons/appeal.svg";
5+
import HowItWorks from "components/HowItWorks";
6+
import Appeal from "components/Popup/MiniGuides/Appeal";
7+
import { AppealHeader, StyledTitle } from "..";
8+
import Options from "../Classic/Options";
9+
import Fund from "./Fund";
10+
11+
interface IShutter {
12+
isAppealMiniGuideOpen: boolean;
13+
toggleAppealMiniGuide: () => void;
14+
}
15+
16+
const Shutter: React.FC<IShutter> = ({ isAppealMiniGuideOpen, toggleAppealMiniGuide }) => {
17+
const [isPopupOpen, setIsPopupOpen] = useState(false);
18+
const [amount, setAmount] = useState("");
19+
const { selectedOption } = useSelectedOptionContext();
20+
21+
return (
22+
<>
23+
{isPopupOpen && (
24+
<Popup
25+
title="Thanks for Funding the Appeal"
26+
icon={AppealIcon}
27+
popupType={PopupType.APPEAL}
28+
setIsOpen={setIsPopupOpen}
29+
setAmount={setAmount}
30+
option={selectedOption?.title ?? ""}
31+
amount={amount}
32+
/>
33+
)}
34+
<AppealHeader>
35+
<StyledTitle>Appeal crowdfunding</StyledTitle>
36+
<HowItWorks
37+
isMiniGuideOpen={isAppealMiniGuideOpen}
38+
toggleMiniGuide={toggleAppealMiniGuide}
39+
MiniGuideComponent={Appeal}
40+
/>
41+
</AppealHeader>
42+
<label>The jury decision is appealed when two options are fully funded.</label>
43+
<Options setAmount={setAmount} />
44+
<Fund amount={amount as `${number}`} setAmount={setAmount} setIsOpen={setIsPopupOpen} />
45+
</>
46+
);
47+
};
48+
49+
export default Shutter;

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

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

44
import { useToggle } from "react-use";
5+
import { useParams } from "react-router-dom";
56

67
import { Periods } from "consts/periods";
7-
import { ClassicAppealProvider } from "hooks/useClassicAppealContext";
8+
import { getDisputeKitName } from "consts/index";
9+
import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
810

911
import { landscapeStyle } from "styles/landscapeStyle";
1012
import { responsiveSize } from "styles/responsiveSize";
1113

1214
import AppealHistory from "./AppealHistory";
1315
import Classic from "./Classic";
16+
import Shutter from "./Shutter";
1417

1518
const Container = styled.div`
1619
padding: 16px;
@@ -44,11 +47,24 @@ export const StyledTitle = styled.h1`
4447

4548
const Appeal: React.FC<{ currentPeriodIndex: number }> = ({ currentPeriodIndex }) => {
4649
const [isAppealMiniGuideOpen, toggleAppealMiniGuide] = useToggle(false);
50+
const { id } = useParams();
51+
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;
4756

4857
return (
4958
<Container>
5059
{Periods.appeal === currentPeriodIndex ? (
51-
<Classic isAppealMiniGuideOpen={isAppealMiniGuideOpen} toggleAppealMiniGuide={toggleAppealMiniGuide} />
60+
<>
61+
{isClassicDisputeKit && (
62+
<Classic isAppealMiniGuideOpen={isAppealMiniGuideOpen} toggleAppealMiniGuide={toggleAppealMiniGuide} />
63+
)}
64+
{isShutterDisputeKit && (
65+
<Shutter isAppealMiniGuideOpen={isAppealMiniGuideOpen} toggleAppealMiniGuide={toggleAppealMiniGuide} />
66+
)}
67+
</>
5268
) : (
5369
<AppealHistory isAppealMiniGuideOpen={isAppealMiniGuideOpen} toggleAppealMiniGuide={toggleAppealMiniGuide} />
5470
)}

web/src/pages/Cases/CaseDetails/Timeline.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ const useTimeline = (dispute: DisputeDetailsQuery["dispute"], currentItemIndex:
140140
}));
141141
};
142142

143-
const getDeadline = (
143+
export const getDeadline = (
144144
currentPeriodIndex: number,
145145
lastPeriodChange?: string,
146146
timesPerPeriod?: string[]

web/src/pages/Cases/CaseDetails/Voting/Classic/Commit.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { wrapWithToast } from "utils/wrapWithToast";
1313

1414
import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
1515

16-
import OptionsContainer from "./OptionsContainer";
16+
import OptionsContainer from "../OptionsContainer";
1717

1818
const Container = styled.div`
1919
width: 100%;

web/src/pages/Cases/CaseDetails/Voting/Classic/Reveal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
1919

2020
import InfoCard from "components/InfoCard";
2121

22-
import JustificationArea from "./JustificationArea";
22+
import JustificationArea from "../JustificationArea";
2323
import { Answer } from "@kleros/kleros-sdk";
2424
import { EnsureChain } from "components/EnsureChain";
2525

web/src/pages/Cases/CaseDetails/Voting/Classic/Vote.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { wrapWithToast } from "utils/wrapWithToast";
99

1010
import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
1111

12-
import OptionsContainer from "./OptionsContainer";
12+
import OptionsContainer from "../OptionsContainer";
1313

1414
const Container = styled.div`
1515
width: 100%;

web/src/pages/Cases/CaseDetails/Voting/Classic/OptionsContainer.tsx renamed to web/src/pages/Cases/CaseDetails/Voting/OptionsContainer.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const RefuseToArbitrateContainer = styled.div`
4646
const StyledEnsureChain = styled(EnsureChain)`
4747
align-self: center;
4848
`;
49+
4950
interface IOptions {
5051
arbitrable: `0x${string}`;
5152
handleSelection: (arg0: bigint) => Promise<void>;
@@ -69,11 +70,16 @@ const Options: React.FC<IOptions> = ({ arbitrable, handleSelection, justificatio
6970
async (id: bigint) => {
7071
setIsSending(true);
7172
setChosenOption(id);
72-
await handleSelection(id);
73-
setChosenOption(BigInt(-1));
74-
setIsSending(false);
73+
try {
74+
await handleSelection(id);
75+
} catch (error) {
76+
console.error(error);
77+
} finally {
78+
setChosenOption(BigInt(-1));
79+
setIsSending(false);
80+
}
7581
},
76-
[handleSelection, setChosenOption, setIsSending]
82+
[handleSelection]
7783
);
7884

7985
return id ? (

0 commit comments

Comments
 (0)