diff --git a/subgraph/schema.graphql b/subgraph/schema.graphql index b904baccc..ad646bee1 100644 --- a/subgraph/schema.graphql +++ b/subgraph/schema.graphql @@ -66,6 +66,7 @@ type User @entity { shifts: [TokenAndETHShift!]! @derivedFrom(field: "juror") draws: [Draw!]! @derivedFrom(field: "juror") activeDisputes: BigInt! + rounds: [Round!]! disputes: [Dispute!]! resolvedDisputes: [Dispute!]! totalResolvedDisputes: BigInt! @@ -142,6 +143,7 @@ type Court @entity { type Dispute @entity { id: ID! + disputeID: BigInt! court: Court! arbitrated: Arbitrable! period: Period! @@ -149,15 +151,21 @@ type Dispute @entity { currentRuling: BigInt! tied: Boolean! overridden: Boolean! + periodDeadline: BigInt! + periodNotificationIndex: BigInt! lastPeriodChange: BigInt! lastPeriodChangeBlockNumber: BigInt! - periodDeadline: BigInt! rounds: [Round!]! @derivedFrom(field: "dispute") currentRound: Round! currentRoundIndex: BigInt! jurors: [User!]! @derivedFrom(field: "disputes") shifts: [TokenAndETHShift!]! @derivedFrom(field: "dispute") - disputeKitDispute: DisputeKitDispute @derivedFrom(field: "coreDispute") + disputeKitDispute: [DisputeKitDispute!]! @derivedFrom(field: "coreDispute") +} + +type PeriodIndexCounter @entity { + id: String! + counter: BigInt! } type Round @entity { @@ -166,6 +174,7 @@ type Round @entity { tokensAtStakePerJuror: BigInt! totalFeesForJurors: BigInt! nbVotes: BigInt! + isCurrentRound: Boolean! repartitions: BigInt! penalties: BigInt! drawnJurors: [Draw!]! @derivedFrom(field: "round") @@ -179,8 +188,9 @@ type Draw @entity(immutable: true) { dispute: Dispute! round: Round! juror: User! - voteID: BigInt! + voteIDNum: BigInt! vote: Vote @derivedFrom(field: "draw") + drawNotificationIndex: BigInt } type DisputeKit @entity { @@ -228,7 +238,6 @@ type ClassicDispute implements DisputeKitDispute @entity { currentLocalRoundIndex: BigInt! numberOfChoices: BigInt! - jumped: Boolean! extraData: Bytes! } diff --git a/subgraph/src/KlerosCore.ts b/subgraph/src/KlerosCore.ts index 3c0efc480..deb9fbba8 100644 --- a/subgraph/src/KlerosCore.ts +++ b/subgraph/src/KlerosCore.ts @@ -10,6 +10,7 @@ import { NewPeriod, StakeSet, TokenAndETHShift as TokenAndETHShiftEvent, + CourtJump, Ruling, StakeDelayed, AcceptedFeeToken, @@ -29,6 +30,7 @@ import { Court, Dispute, User } from "../generated/schema"; import { BigInt } from "@graphprotocol/graph-ts"; import { updatePenalty } from "./entities/Penalty"; import { ensureFeeToken } from "./entities/FeeToken"; +import { getAndIncrementPeriodCounter } from "./entities/PeriodIndexCounter"; function getPeriodName(index: i32): string { const periodArray = ["evidence", "commit", "vote", "appeal", "execution"]; @@ -128,6 +130,7 @@ export function handleNewPeriod(event: NewPeriod): void { dispute.period = newPeriod; dispute.lastPeriodChange = event.block.timestamp; dispute.lastPeriodChangeBlockNumber = event.block.number; + dispute.periodNotificationIndex = getAndIncrementPeriodCounter(newPeriod); if (newPeriod !== "execution") { dispute.periodDeadline = event.block.timestamp.plus(court.timesPerPeriod[event.params._period]); } else { @@ -164,6 +167,13 @@ export function handleAppealDecision(event: AppealDecision): void { createRoundFromRoundInfo(disputeID, newRoundIndex, roundInfo); } +export function handleCourtJump(event: CourtJump): void { + const dispute = Dispute.load(event.params._disputeID.toString()); + if (!dispute) return; + dispute.court = event.params._toCourtID.toString(); + dispute.save(); +} + export function handleDraw(event: DrawEvent): void { createDrawFromEvent(event); const disputeID = event.params._disputeID.toString(); diff --git a/subgraph/src/entities/ClassicDispute.ts b/subgraph/src/entities/ClassicDispute.ts index 30d49bf60..9d49c7210 100644 --- a/subgraph/src/entities/ClassicDispute.ts +++ b/subgraph/src/entities/ClassicDispute.ts @@ -8,7 +8,6 @@ export function createClassicDisputeFromEvent(event: DisputeCreation): void { classicDispute.coreDispute = coreDisputeID; classicDispute.currentLocalRoundIndex = ZERO; classicDispute.numberOfChoices = event.params._numberOfChoices; - classicDispute.jumped = false; classicDispute.extraData = event.params._extraData; classicDispute.save(); } diff --git a/subgraph/src/entities/Dispute.ts b/subgraph/src/entities/Dispute.ts index 5fac58f69..3cbcbcbb6 100644 --- a/subgraph/src/entities/Dispute.ts +++ b/subgraph/src/entities/Dispute.ts @@ -1,14 +1,15 @@ import { KlerosCore, DisputeCreation } from "../../generated/KlerosCore/KlerosCore"; import { Court, Dispute } from "../../generated/schema"; import { ZERO } from "../utils"; +import { getAndIncrementPeriodCounter } from "./PeriodIndexCounter"; export function createDisputeFromEvent(event: DisputeCreation): void { - const contract = KlerosCore.bind(event.address); const disputeID = event.params._disputeID; - const disputeContractState = contract.disputes(disputeID); + const disputeContractState = KlerosCore.bind(event.address).disputes(disputeID); const dispute = new Dispute(disputeID.toString()); const courtID = disputeContractState.value0.toString(); dispute.court = courtID; + dispute.disputeID = disputeID; dispute.arbitrated = event.params._arbitrable.toHexString(); dispute.period = "evidence"; dispute.ruled = false; @@ -17,6 +18,7 @@ export function createDisputeFromEvent(event: DisputeCreation): void { dispute.overridden = false; dispute.lastPeriodChange = event.block.timestamp; dispute.lastPeriodChangeBlockNumber = event.block.number; + dispute.periodNotificationIndex = getAndIncrementPeriodCounter(dispute.period); const court = Court.load(courtID); if (!court) return; dispute.periodDeadline = event.block.timestamp.plus(court.timesPerPeriod[0]); diff --git a/subgraph/src/entities/Draw.ts b/subgraph/src/entities/Draw.ts index d5fd35946..a62567b32 100644 --- a/subgraph/src/entities/Draw.ts +++ b/subgraph/src/entities/Draw.ts @@ -1,5 +1,6 @@ import { Draw as DrawEvent } from "../../generated/KlerosCore/KlerosCore"; -import { Draw } from "../../generated/schema"; +import { Draw, User } from "../../generated/schema"; +import { getAndIncrementPeriodCounter } from "./PeriodIndexCounter"; export function createDrawFromEvent(event: DrawEvent): void { const disputeID = event.params._disputeID.toString(); @@ -9,9 +10,13 @@ export function createDrawFromEvent(event: DrawEvent): void { const drawID = `${disputeID}-${roundIndex.toString()}-${voteID.toString()}`; const draw = new Draw(drawID); draw.blockNumber = event.block.number; + const user = User.load(event.params._address.toHexString()); + if (user && !user.disputes.includes(disputeID)) { + draw.drawNotificationIndex = getAndIncrementPeriodCounter("draw"); + } draw.dispute = disputeID; draw.round = roundID; draw.juror = event.params._address.toHexString(); - draw.voteID = voteID; + draw.voteIDNum = voteID; draw.save(); } diff --git a/subgraph/src/entities/PeriodIndexCounter.ts b/subgraph/src/entities/PeriodIndexCounter.ts new file mode 100644 index 000000000..c685f8904 --- /dev/null +++ b/subgraph/src/entities/PeriodIndexCounter.ts @@ -0,0 +1,14 @@ +import { PeriodIndexCounter } from "../../generated/schema"; +import { BigInt } from "@graphprotocol/graph-ts"; + +export function getAndIncrementPeriodCounter(id: string): BigInt { + let counter = PeriodIndexCounter.load(id); + if (!counter) { + counter = new PeriodIndexCounter(id); + counter.counter = BigInt.fromI32(0); + } + const counterOld = counter.counter; + counter.counter = counter.counter.plus(BigInt.fromI32(1)); + counter.save(); + return counterOld; +} diff --git a/subgraph/src/entities/Round.ts b/subgraph/src/entities/Round.ts index 8c67b1082..5ffb518e6 100644 --- a/subgraph/src/entities/Round.ts +++ b/subgraph/src/entities/Round.ts @@ -9,6 +9,7 @@ export function createRoundFromRoundInfo( ): void { const roundID = `${disputeID.toString()}-${roundIndex.toString()}`; const round = new Round(roundID); + round.isCurrentRound = true; const feeToken = roundInfo.feeToken.toHexString(); round.feeToken = feeToken === "0x0000000000000000000000000000000000000000" ? null : feeToken; round.disputeKit = roundInfo.disputeKitID.toString(); diff --git a/subgraph/src/entities/User.ts b/subgraph/src/entities/User.ts index a1b0ed340..918878bcb 100644 --- a/subgraph/src/entities/User.ts +++ b/subgraph/src/entities/User.ts @@ -18,6 +18,7 @@ export function createUserFromAddress(id: string): User { user.totalDelayed = ZERO; user.activeDisputes = ZERO; user.disputes = []; + user.rounds = []; user.resolvedDisputes = []; user.totalResolvedDisputes = ZERO; user.totalAppealingDisputes = ZERO; diff --git a/subgraph/subgraph.yaml b/subgraph/subgraph.yaml index a351d26e3..2bd08d6da 100644 --- a/subgraph/subgraph.yaml +++ b/subgraph/subgraph.yaml @@ -56,6 +56,8 @@ dataSources: handler: handleRuling - event: AcceptedFeeToken(indexed address,indexed bool) handler: handleAcceptedFeeToken + - event: CourtJump(indexed uint256,indexed uint256,indexed uint96,uint96) + handler: handleCourtJump file: ./src/KlerosCore.ts - kind: ethereum name: PolicyRegistry diff --git a/web/src/components/DisputeCard/index.tsx b/web/src/components/DisputeCard/index.tsx index b9eaf5d7a..39895673c 100644 --- a/web/src/components/DisputeCard/index.tsx +++ b/web/src/components/DisputeCard/index.tsx @@ -14,6 +14,7 @@ import { useVotingHistory } from "queries/useVotingHistory"; import DisputeInfo from "./DisputeInfo"; import PeriodBanner from "./PeriodBanner"; import { isUndefined } from "utils/index"; +import { getLocalRounds } from "utils/getLocalRounds"; const StyledCard = styled(Card)` width: 100%; @@ -104,7 +105,7 @@ const DisputeCard: React.FC = ({ id, arbitrated, period, lastPerio const courtName = courtPolicy?.name; const category = disputeTemplate ? disputeTemplate.category : undefined; const { data: votingHistory } = useVotingHistory(id); - const localRounds = votingHistory?.dispute?.disputeKitDispute?.localRounds; + const localRounds = getLocalRounds(votingHistory?.dispute?.disputeKitDispute); const navigate = useNavigate(); return ( <> diff --git a/web/src/components/Verdict/DisputeTimeline.tsx b/web/src/components/Verdict/DisputeTimeline.tsx index 2daad4db0..05a24b52e 100644 --- a/web/src/components/Verdict/DisputeTimeline.tsx +++ b/web/src/components/Verdict/DisputeTimeline.tsx @@ -2,15 +2,16 @@ import React, { useMemo } from "react"; import { useParams } from "react-router-dom"; import styled, { useTheme } from "styled-components"; import { _TimelineItem1, CustomTimeline } from "@kleros/ui-components-library"; +import CalendarIcon from "assets/svgs/icons/calendar.svg"; +import ClosedCaseIcon from "assets/svgs/icons/check-circle-outline.svg"; +import AppealedCaseIcon from "assets/svgs/icons/close-circle.svg"; import { Periods } from "consts/periods"; import { ClassicRound } from "src/graphql/graphql"; -import { getVoteChoice } from "pages/Cases/CaseDetails/Voting/VotingHistory"; import { DisputeDetailsQuery, useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery"; import { useDisputeTemplate } from "queries/useDisputeTemplate"; import { useVotingHistory } from "queries/useVotingHistory"; -import CalendarIcon from "assets/svgs/icons/calendar.svg"; -import ClosedCaseIcon from "assets/svgs/icons/check-circle-outline.svg"; -import AppealedCaseIcon from "assets/svgs/icons/close-circle.svg"; +import { getVoteChoice } from "pages/Cases/CaseDetails/Voting/VotingHistory"; +import { getLocalRounds } from "utils/getLocalRounds"; const Container = styled.div` display: flex; @@ -63,7 +64,7 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string const { id } = useParams(); const { data: votingHistory } = useVotingHistory(id); const { data: disputeTemplate } = useDisputeTemplate(id, arbitrable); - const localRounds: ClassicRound[] = votingHistory?.dispute?.disputeKitDispute?.localRounds as ClassicRound[]; + const localRounds: ClassicRound[] = getLocalRounds(votingHistory?.dispute?.disputeKitDispute) as ClassicRound[]; const theme = useTheme(); diff --git a/web/src/hooks/queries/useClassicAppealQuery.ts b/web/src/hooks/queries/useClassicAppealQuery.ts index ebf833961..bcdce6ebb 100644 --- a/web/src/hooks/queries/useClassicAppealQuery.ts +++ b/web/src/hooks/queries/useClassicAppealQuery.ts @@ -34,7 +34,7 @@ const classicAppealQuery = graphql(` export const useClassicAppealQuery = (id?: string | number) => { const isEnabled = id !== undefined; - return useQuery({ + return useQuery({ queryKey: ["refetchOnBlock", `classicAppealQuery${id}`], enabled: isEnabled, queryFn: async () => await graphqlQueryFnHelper(classicAppealQuery, { disputeID: id?.toString() }), diff --git a/web/src/hooks/queries/useDrawQuery.ts b/web/src/hooks/queries/useDrawQuery.ts index b152b5070..b94802abc 100644 --- a/web/src/hooks/queries/useDrawQuery.ts +++ b/web/src/hooks/queries/useDrawQuery.ts @@ -7,14 +7,14 @@ export type { DrawQuery }; const drawQuery = graphql(` query Draw($address: String, $disputeID: String, $roundID: String) { draws(where: { dispute: $disputeID, juror: $address, round: $roundID }) { - voteID + voteIDNum } } `); export const useDrawQuery = (address?: string | null, disputeID?: string, roundID?: string) => { const isEnabled = !!(address && disputeID && roundID); - return useQuery({ + return useQuery({ queryKey: [`drawQuery${[address, disputeID, roundID]}`], enabled: isEnabled, queryFn: async () => await graphqlQueryFnHelper(drawQuery, { address, disputeID, roundID }), diff --git a/web/src/hooks/useClassicAppealContext.tsx b/web/src/hooks/useClassicAppealContext.tsx index 1a549fee4..2e1c22cf8 100644 --- a/web/src/hooks/useClassicAppealContext.tsx +++ b/web/src/hooks/useClassicAppealContext.tsx @@ -7,6 +7,7 @@ import { useAppealCost } from "queries/useAppealCost"; import { useDisputeKitClassicMultipliers } from "queries/useDisputeKitClassicMultipliers"; import { useClassicAppealQuery, ClassicAppealQuery } from "queries/useClassicAppealQuery"; import { useCountdown } from "hooks/useCountdown"; +import { getLocalRounds } from "utils/getLocalRounds"; const LoserSideCountdownContext = createContext(undefined); @@ -100,7 +101,7 @@ export const useOptionsContext = () => useContext(OptionsContext); const getCurrentLocalRound = (dispute?: ClassicAppealQuery["dispute"]) => { const period = dispute?.period; const currentLocalRoundIndex = dispute?.disputeKitDispute?.currentLocalRoundIndex; - return dispute?.disputeKitDispute?.localRounds[ + return getLocalRounds(dispute?.disputeKitDispute)[ ["appeal", "execution"].includes(period ?? "") ? currentLocalRoundIndex : currentLocalRoundIndex - 1 ]; }; diff --git a/web/src/pages/Cases/CaseDetails/Overview.tsx b/web/src/pages/Cases/CaseDetails/Overview.tsx index 470c328cf..e0a80d2bc 100644 --- a/web/src/pages/Cases/CaseDetails/Overview.tsx +++ b/web/src/pages/Cases/CaseDetails/Overview.tsx @@ -15,6 +15,7 @@ import { StyledSkeleton } from "components/StyledSkeleton"; import DisputeInfo from "components/DisputeCard/DisputeInfo"; import Verdict from "components/Verdict/index"; import { useVotingHistory } from "hooks/queries/useVotingHistory"; +import { getLocalRounds } from "utils/getLocalRounds"; const Container = styled.div` width: 100%; @@ -117,7 +118,7 @@ const Overview: React.FC = ({ arbitrable, courtID, currentPeriodIndex const { data: disputeDetails } = useDisputeDetailsQuery(id); const { data: courtPolicy } = useCourtPolicy(courtID); const { data: votingHistory } = useVotingHistory(id); - const localRounds = votingHistory?.dispute?.disputeKitDispute?.localRounds; + const localRounds = getLocalRounds(votingHistory?.dispute?.disputeKitDispute); const courtName = courtPolicy?.name; const court = disputeDetails?.dispute?.court; const rewards = court ? `≥ ${formatEther(court.feeForJuror)} ETH` : undefined; diff --git a/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx b/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx index f53602a23..9aed8c3c4 100644 --- a/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx +++ b/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx @@ -9,6 +9,7 @@ import { useVotingHistory } from "queries/useVotingHistory"; import { useDisputeTemplate } from "queries/useDisputeTemplate"; import { shortenAddress } from "utils/shortenAddress"; import { isUndefined } from "utils/index"; +import { getLocalRounds } from "utils/getLocalRounds"; const Container = styled.div``; @@ -87,7 +88,7 @@ const AccordionContent: React.FC<{ ); }; -export const getVoteChoice = (vote, answers) => { +export const getVoteChoice = (vote: number, answers: { title: string }[]) => { const selectedAnswer = answers?.[vote - 1]?.title; if (vote === 0) { return "Refuse to arbitrate"; @@ -104,7 +105,7 @@ const VotingHistory: React.FC<{ arbitrable?: `0x${string}`; isQuestion: boolean const [currentTab, setCurrentTab] = useState(0); const { data: disputeTemplate } = useDisputeTemplate(id, arbitrable); const rounds = votingHistory?.dispute?.rounds; - const localRounds = votingHistory?.dispute?.disputeKitDispute?.localRounds; + const localRounds = getLocalRounds(votingHistory?.dispute?.disputeKitDispute); const answers = disputeTemplate?.answers; return ( @@ -115,7 +116,7 @@ const VotingHistory: React.FC<{ arbitrable?: `0x${string}`; isQuestion: boolean {isQuestion && disputeTemplate.question ? ( {disputeTemplate.question} ) : ( - The dispute's template is not correct please vote refuse to arbitrate + {"The dispute's template is not correct please vote refuse to arbitrate"} )} = ({ arbitrable, currentPeriodIndex }) => { const { data: appealCost } = useAppealCost(id); const { data: drawData } = useDrawQuery(address?.toLowerCase(), id, disputeData?.dispute?.currentRound.id); const roundId = disputeData?.dispute?.currentRoundIndex; - const voteId = drawData?.draws?.[0]?.voteID; + const voteId = drawData?.draws?.[0]?.voteIDNum; const { data: voted } = useDisputeKitClassicIsVoteActive({ enabled: !isUndefined(roundId) && !isUndefined(voteId), args: [BigInt(id ?? 0), roundId, voteId], @@ -101,7 +101,11 @@ const Voting: React.FC = ({ arbitrable, currentPeriodIndex }) => { !voted ? ( <> - draw.voteID)} /> + draw.voteIDNum)} + /> ) : ( diff --git a/web/src/utils/getLocalRounds.ts b/web/src/utils/getLocalRounds.ts new file mode 100644 index 000000000..547d7cc80 --- /dev/null +++ b/web/src/utils/getLocalRounds.ts @@ -0,0 +1,20 @@ +import { VotingHistoryQuery } from "queries/useVotingHistory"; +import { ClassicAppealQuery } from "queries/useClassicAppealQuery"; + +type IVotingHistoryLocalRounds = NonNullable< + NonNullable["disputeKitDispute"] +>["localRounds"]; + +type IClassicAppealQueryLocalRounds = NonNullable< + NonNullable["disputeKitDispute"] +>["localRounds"]; + +type ILocalRounds = IClassicAppealQueryLocalRounds | IVotingHistoryLocalRounds; + +interface IDisputeKitDisputes { + localRounds: ILocalRounds; +} + +export const getLocalRounds = (disputeKitDisputes: IDisputeKitDisputes | undefined | null) => { + return disputeKitDisputes?.reduce((acc: ILocalRounds, { localRounds }) => acc.concat(localRounds), []); +};