Skip to content

Commit ef60216

Browse files
authored
Merge pull request #1668 from kleros/feat/arbitrator-dev-buttons
Maintenance buttons at arbitrator-level
2 parents 1044333 + 5d9bf8e commit ef60216

File tree

11 files changed

+391
-26
lines changed

11 files changed

+391
-26
lines changed

subgraph/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kleros/kleros-v2-subgraph",
3-
"version": "0.7.0",
3+
"version": "0.7.2",
44
"license": "MIT",
55
"scripts": {
66
"update:core:arbitrum-sepolia-devnet": "./scripts/update.sh arbitrumSepoliaDevnet arbitrum-sepolia core/subgraph.yaml",
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from "react";
2+
import styled, { css, keyframes } from "styled-components";
3+
4+
import DottedMenu from "svgs/icons/dotted-menu.svg";
5+
6+
const ripple = keyframes`
7+
0% {
8+
opacity: 0;
9+
transform: scale3d(0.5, 0.5, 1);
10+
}
11+
10% {
12+
opacity: 0.5;
13+
transform: scale3d(0.75, 0.75, 1);
14+
}
15+
16+
100% {
17+
opacity: 0;
18+
transform: scale3d(1.75, 1.75, 1);
19+
}
20+
`;
21+
22+
const ring = (duration: string, delay: string) => css`
23+
opacity: 0;
24+
position: absolute;
25+
top: 0;
26+
left: 0;
27+
transform: translate(50%);
28+
content: "";
29+
height: 100%;
30+
width: 100%;
31+
border: 3px solid ${({ theme }) => theme.primaryBlue};
32+
border-radius: 100%;
33+
animation-name: ${ripple};
34+
animation-duration: ${duration};
35+
animation-delay: ${delay};
36+
animation-iteration-count: infinite;
37+
animation-timing-function: cubic-bezier(0.65, 0, 0.34, 1);
38+
z-index: 0;
39+
`;
40+
41+
const Container = styled.div<{ displayRipple: boolean }>`
42+
display: flex;
43+
justify-content: center;
44+
align-items: center;
45+
width: 36px;
46+
height: 36px;
47+
${({ displayRipple }) =>
48+
displayRipple &&
49+
css`
50+
&::after {
51+
${ring("3s", "0s")}
52+
}
53+
&::before {
54+
${ring("3s", "0.5s")}
55+
}
56+
`}
57+
`;
58+
59+
const ButtonContainer = styled.div`
60+
border-radius: 50%;
61+
z-index: 1;
62+
background-color: ${({ theme }) => theme.lightBackground};
63+
`;
64+
65+
const StyledDottedMenu = styled(DottedMenu)`
66+
cursor: pointer;
67+
width: 100%;
68+
height: 100%;
69+
fill: ${({ theme }) => theme.primaryBlue};
70+
`;
71+
72+
interface IMenuButton {
73+
toggle: () => void;
74+
displayRipple: boolean;
75+
className?: string;
76+
}
77+
78+
const DottedMenuButton: React.FC<IMenuButton> = ({ toggle, displayRipple, className }) => {
79+
return (
80+
<Container {...{ displayRipple, className }}>
81+
<ButtonContainer className="button-container">
82+
<StyledDottedMenu onClick={toggle} className="menu-icon" />
83+
</ButtonContainer>
84+
</Container>
85+
);
86+
};
87+
88+
export default DottedMenuButton;

web/src/components/Phase.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import { useSortitionModulePhase } from "hooks/useSortitionModulePhase";
5+
6+
import { isUndefined } from "src/utils";
7+
8+
export enum Phases {
9+
staking,
10+
generating,
11+
drawing,
12+
}
13+
14+
const StyledLabel = styled.label``;
15+
16+
const Phase: React.FC<{ className?: string }> = ({ className }) => {
17+
const { data: phase } = useSortitionModulePhase();
18+
return <>{isUndefined(phase) ? null : <StyledLabel {...{ className }}>Phase: {Phases[phase]}</StyledLabel>}</>;
19+
};
20+
21+
export default Phase;

web/src/layout/Header/navbar/Debug.tsx

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

44
import { GIT_BRANCH, GIT_DIRTY, GIT_HASH, GIT_TAGS, GIT_URL, RELEASE_VERSION } from "consts/index";
5-
import { useSortitionModulePhase } from "hooks/useSortitionModulePhase";
65
import { useToggleTheme } from "hooks/useToggleThemeContext";
76
import { isUndefined } from "utils/index";
87

8+
import Phase from "components/Phase";
9+
910
const Container = styled.div`
1011
display: flex;
1112
flex-direction: column;
@@ -32,6 +33,10 @@ const StyledLabel = styled.label`
3233
padding-left: 8px;
3334
`;
3435

36+
const StyledPhase = styled(Phase)`
37+
padding-left: 8px;
38+
`;
39+
3540
const Version = () => (
3641
<StyledLabel>
3742
v{RELEASE_VERSION}{" "}
@@ -51,23 +56,12 @@ const ServicesStatus = () => {
5156
return <label>{isUndefined(statusUrl) ? null : <StyledIframe src={`${statusUrl + statusUrlParameters}`} />}</label>;
5257
};
5358

54-
enum Phases {
55-
staking,
56-
generating,
57-
drawing,
58-
}
59-
60-
const Phase = () => {
61-
const { data: phase } = useSortitionModulePhase();
62-
return <>{isUndefined(phase) ? null : <StyledLabel>Phase: {Phases[phase as number]}</StyledLabel>}</>;
63-
};
64-
6559
const Debug: React.FC = () => {
6660
return (
6761
<Container>
6862
<ServicesStatus />
6963
<Version />
70-
<Phase />
64+
<StyledPhase />
7165
</Container>
7266
);
7367
};

web/src/pages/Cases/CaseDetails/MaintenanceButtons/PassPeriodButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const PassPeriodButton: React.FC<IPassPeriodButton> = ({ id, setIsOpen, period }
2828
const publicClient = usePublicClient();
2929
const { data: maintenanceData } = useDisputeMaintenanceQuery(id);
3030

31-
const isDrawn = useMemo(() => maintenanceData?.dispute?.currentRound.jurorsDrawn, [maintenanceData]);
31+
const isDrawn = useMemo(() => maintenanceData?.dispute?.currentRound.jurorsDrawn ?? false, [maintenanceData]);
3232

3333
const {
3434
data: passPeriodConfig,

web/src/pages/Cases/CaseDetails/MaintenanceButtons/WithdrawAppealFees.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ const WithdrawAppealFees: React.FC<IWithdrawAppealFees> = ({ id, roundIndex, set
3939
const { data: appealData } = useClassicAppealQuery(id);
4040

4141
const localRounds = useMemo(() => getLocalRounds(appealData?.dispute?.disputeKitDispute), [appealData]);
42-
console.log({ localRounds });
4342

4443
const feeDispersed = useMemo(
4544
() =>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
88
import { Periods } from "src/consts/periods";
99
import { Period } from "src/graphql/graphql";
1010

11+
import DottedMenuButton from "components/DottedMenuButton";
1112
import { EnsureChain } from "components/EnsureChain";
1213
import { Overlay } from "components/Overlay";
1314

1415
import DistributeRewards from "./DistributeRewards";
1516
import DrawButton from "./DrawButton";
1617
import ExecuteRulingButton from "./ExecuteRuling";
17-
import MenuButton from "./MenuButton";
1818
import PassPeriodButton from "./PassPeriodButton";
1919
import WithdrawAppealFees from "./WithdrawAppealFees";
2020

@@ -128,7 +128,7 @@ const MaintenanceButtons: React.FC = () => {
128128
</PopupContainer>
129129
</>
130130
) : null}
131-
<MenuButton {...{ toggle, displayRipple }} />
131+
<DottedMenuButton {...{ toggle, displayRipple }} />
132132
</Container>
133133
);
134134
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, { useMemo, useState } from "react";
2+
import styled from "styled-components";
3+
4+
import { usePublicClient } from "wagmi";
5+
6+
import { Button } from "@kleros/ui-components-library";
7+
8+
import {
9+
useReadSortitionModuleDelayedStakeReadIndex,
10+
useReadSortitionModuleDelayedStakeWriteIndex,
11+
useSimulateSortitionModuleExecuteDelayedStakes,
12+
useWriteSortitionModuleExecuteDelayedStakes,
13+
} from "hooks/contracts/generated";
14+
import { useSortitionModulePhase } from "hooks/useSortitionModulePhase";
15+
import { wrapWithToast } from "utils/wrapWithToast";
16+
17+
import { isUndefined } from "src/utils";
18+
19+
import { Phases } from "components/Phase";
20+
21+
import { IBaseStakeMaintenanceButton } from ".";
22+
23+
const StyledButton = styled(Button)`
24+
width: 100%;
25+
`;
26+
27+
type IExecuteStakeDelayedButton = IBaseStakeMaintenanceButton;
28+
29+
const ExecuteDelayedStakeButton: React.FC<IExecuteStakeDelayedButton> = ({ setIsOpen }) => {
30+
const [isSending, setIsSending] = useState(false);
31+
const publicClient = usePublicClient();
32+
const { data: phase } = useSortitionModulePhase();
33+
const { data: delayedStakeWriteIndex } = useReadSortitionModuleDelayedStakeWriteIndex();
34+
const { data: delayedStakeReadIndex } = useReadSortitionModuleDelayedStakeReadIndex();
35+
36+
const canExecute = useMemo(() => {
37+
if (isUndefined(phase) || isUndefined(delayedStakeReadIndex) || isUndefined(delayedStakeWriteIndex)) return false;
38+
return phase === Phases.staking && delayedStakeWriteIndex >= delayedStakeReadIndex;
39+
}, [phase, delayedStakeReadIndex, delayedStakeWriteIndex]);
40+
41+
const {
42+
data: executeDelayedStakeConfig,
43+
isLoading: isLoadingConfig,
44+
isError,
45+
} = useSimulateSortitionModuleExecuteDelayedStakes({
46+
query: {
47+
enabled: canExecute,
48+
},
49+
args: [1n + (delayedStakeWriteIndex ?? 0n) - (delayedStakeReadIndex ?? 0n)],
50+
});
51+
52+
const { writeContractAsync: executeDelayedStake } = useWriteSortitionModuleExecuteDelayedStakes();
53+
54+
const isLoading = useMemo(() => isLoadingConfig || isSending, [isLoadingConfig, isSending]);
55+
const isDisabled = useMemo(() => isError || isLoading || !canExecute, [isError, isLoading, canExecute]);
56+
const handleClick = () => {
57+
if (!executeDelayedStakeConfig || !publicClient || !executeDelayedStake) return;
58+
59+
setIsSending(true);
60+
61+
wrapWithToast(async () => await executeDelayedStake(executeDelayedStakeConfig.request), publicClient).finally(
62+
() => {
63+
setIsSending(false);
64+
setIsOpen(false);
65+
}
66+
);
67+
};
68+
return (
69+
<StyledButton
70+
text="Execute Delayed Stakes"
71+
small
72+
isLoading={isLoading}
73+
disabled={isDisabled}
74+
onClick={handleClick}
75+
/>
76+
);
77+
};
78+
79+
export default ExecuteDelayedStakeButton;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { useMemo, useState } from "react";
2+
import styled from "styled-components";
3+
4+
import { usePublicClient } from "wagmi";
5+
6+
import { Button } from "@kleros/ui-components-library";
7+
8+
import {
9+
useReadSortitionModuleDisputesWithoutJurors,
10+
useReadSortitionModuleLastPhaseChange,
11+
useReadSortitionModuleMaxDrawingTime,
12+
useReadSortitionModuleMinStakingTime,
13+
useSimulateSortitionModulePassPhase,
14+
useWriteSortitionModulePassPhase,
15+
} from "hooks/contracts/generated";
16+
import { useSortitionModulePhase } from "hooks/useSortitionModulePhase";
17+
import { wrapWithToast } from "utils/wrapWithToast";
18+
19+
import { isUndefined } from "src/utils";
20+
21+
import { Phases } from "components/Phase";
22+
23+
import { IBaseStakeMaintenanceButton } from ".";
24+
25+
const StyledButton = styled(Button)`
26+
width: 100%;
27+
`;
28+
29+
type IPassPhaseButton = IBaseStakeMaintenanceButton;
30+
31+
const PassPhaseButton: React.FC<IPassPhaseButton> = ({ setIsOpen }) => {
32+
const [isSending, setIsSending] = useState(false);
33+
const publicClient = usePublicClient();
34+
const { data: phase } = useSortitionModulePhase();
35+
const { data: lastPhaseChange } = useReadSortitionModuleLastPhaseChange();
36+
const { data: minStakingTime } = useReadSortitionModuleMinStakingTime();
37+
const { data: maxDrawingTime } = useReadSortitionModuleMaxDrawingTime();
38+
const { data: disputeWithoutJurors } = useReadSortitionModuleDisputesWithoutJurors();
39+
40+
const canChangePhase = useMemo(() => {
41+
if (
42+
isUndefined(phase) ||
43+
isUndefined(lastPhaseChange) ||
44+
isUndefined(minStakingTime) ||
45+
isUndefined(maxDrawingTime) ||
46+
isUndefined(disputeWithoutJurors)
47+
)
48+
return false;
49+
50+
const now = Math.floor(Date.now() / 1000);
51+
switch (phase) {
52+
case Phases.staking:
53+
return BigInt(now) - lastPhaseChange >= minStakingTime;
54+
case Phases.drawing:
55+
return disputeWithoutJurors === 0n || BigInt(now) - lastPhaseChange >= maxDrawingTime;
56+
default:
57+
return true;
58+
}
59+
}, [phase, lastPhaseChange, minStakingTime, maxDrawingTime, disputeWithoutJurors]);
60+
61+
const {
62+
data: passPhaseConfig,
63+
isLoading: isLoadingConfig,
64+
isError,
65+
} = useSimulateSortitionModulePassPhase({
66+
query: {
67+
enabled: canChangePhase,
68+
},
69+
});
70+
71+
const { writeContractAsync: passPhase } = useWriteSortitionModulePassPhase();
72+
73+
const isLoading = useMemo(() => isLoadingConfig || isSending, [isLoadingConfig, isSending]);
74+
const isDisabled = useMemo(() => isError || isLoading || !canChangePhase, [isError, isLoading, canChangePhase]);
75+
const handleClick = () => {
76+
if (!passPhaseConfig || !publicClient || !passPhase) return;
77+
78+
setIsSending(true);
79+
80+
wrapWithToast(async () => await passPhase(passPhaseConfig.request), publicClient).finally(() => {
81+
setIsSending(false);
82+
setIsOpen(false);
83+
});
84+
};
85+
return <StyledButton text="Pass Phase" small isLoading={isLoading} disabled={isDisabled} onClick={handleClick} />;
86+
};
87+
88+
export default PassPhaseButton;

0 commit comments

Comments
 (0)