From e0e5fca7f33b92d5221b6004330929309904735b Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Thu, 16 Jan 2025 21:04:24 +0530 Subject: [PATCH 1/8] refactor(sdk): make-answer-id-required-in-schema --- .../src/dataMappings/utils/disputeDetailsSchema.ts | 12 ++++++++---- .../src/dataMappings/utils/populateTemplate.ts | 8 +++++++- kleros-sdk/src/utils/getDispute.ts | 11 ----------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts b/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts index d9948ea70..78c4697a7 100644 --- a/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts +++ b/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts @@ -33,15 +33,19 @@ export enum QuestionType { export const QuestionTypeSchema = z.nativeEnum(QuestionType); export const AnswerSchema = z.object({ - id: z - .string() - .regex(/^0x[0-9a-fA-F]+$/) - .optional(), + id: z.string().regex(/^0x[0-9a-fA-F]+$/), title: z.string(), description: z.string(), reserved: z.boolean().optional(), }); +export const RefuseToArbitrateAnswer = { + id: "0x0", + title: "Refuse to Arbitrate / Invalid", + description: "Refuse to Arbitrate / Invalid", + reserved: true, +}; + export const AttachmentSchema = z.object({ label: z.string(), uri: z.string(), diff --git a/kleros-sdk/src/dataMappings/utils/populateTemplate.ts b/kleros-sdk/src/dataMappings/utils/populateTemplate.ts index 783e30196..064393e89 100644 --- a/kleros-sdk/src/dataMappings/utils/populateTemplate.ts +++ b/kleros-sdk/src/dataMappings/utils/populateTemplate.ts @@ -1,6 +1,6 @@ import mustache from "mustache"; import { DisputeDetails } from "./disputeDetailsTypes"; -import DisputeDetailsSchema from "./disputeDetailsSchema"; +import DisputeDetailsSchema, { RefuseToArbitrateAnswer } from "./disputeDetailsSchema"; export const populateTemplate = (mustacheTemplate: string, data: any): DisputeDetails => { const render = mustache.render(mustacheTemplate, data); @@ -11,5 +11,11 @@ export const populateTemplate = (mustacheTemplate: string, data: any): DisputeDe throw validation.error; } + // Filter out any existing answer with id 0 and add our standard Refuse to Arbitrate option + (dispute as DisputeDetails).answers = [ + RefuseToArbitrateAnswer, + ...((dispute as DisputeDetails).answers.filter((answer) => answer.id && Number(answer.id) !== 0) || []), + ]; + return dispute; }; diff --git a/kleros-sdk/src/utils/getDispute.ts b/kleros-sdk/src/utils/getDispute.ts index ca7a500fe..c73fa1f24 100644 --- a/kleros-sdk/src/utils/getDispute.ts +++ b/kleros-sdk/src/utils/getDispute.ts @@ -56,16 +56,5 @@ export const getDispute = async (disputeParameters: GetDisputeParameters): Promi const populatedTemplate = populateTemplate(templateData, data); - // Filter out any existing answer with id 0 and add our standard Refuse to Arbitrate option - populatedTemplate.answers = [ - { - id: "0x0", - title: "Refuse to Arbitrate / Invalid", - description: "Refuse to Arbitrate / Invalid", - reserved: true, - }, - ...(populatedTemplate.answers?.filter((answer) => answer.id && Number(answer.id) !== 0) || []), - ]; - return populatedTemplate; }; From 3ef595d8cef6deabe8b180a17c902ea789c94bf2 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Fri, 17 Jan 2025 15:46:50 +0530 Subject: [PATCH 2/8] refactor(subgraph): abstract-answer-fields-to-answer-entity --- subgraph/core/schema.graphql | 13 ++++- subgraph/core/src/DisputeKitClassic.ts | 8 +++- subgraph/core/src/entities/ClassicRound.ts | 55 ++++++++++++---------- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/subgraph/core/schema.graphql b/subgraph/core/schema.graphql index dc34b33b3..fcdf6f32a 100644 --- a/subgraph/core/schema.graphql +++ b/subgraph/core/schema.graphql @@ -63,6 +63,7 @@ interface Evidence { fileTypeExtension: String } + ############ # Entities # ############ @@ -265,17 +266,25 @@ type ClassicDispute implements DisputeKitDispute @entity { extraData: Bytes! } +type Answer @entity { + id: ID! # classicRound.id-answerId + answerId: BigInt! + count: BigInt! + paidFee: BigInt! + funded: Boolean! + localRound: ClassicRound! +} + type ClassicRound implements DisputeKitRound @entity { id: ID! # disputeKit.id-coreDispute-dispute.rounds.length localDispute: DisputeKitDispute! votes: [Vote!]! @derivedFrom(field: "localRound") + answers: [Answer!]! @derivedFrom(field: "localRound") winningChoice: BigInt! - counts: [BigInt!]! tied: Boolean! totalVoted: BigInt! totalCommited: BigInt! - paidFees: [BigInt!]! contributions: [ClassicContribution!]! @derivedFrom(field: "localRound") feeRewards: BigInt! totalFeeDispersed: BigInt! diff --git a/subgraph/core/src/DisputeKitClassic.ts b/subgraph/core/src/DisputeKitClassic.ts index a8b9b3361..c58147bf1 100644 --- a/subgraph/core/src/DisputeKitClassic.ts +++ b/subgraph/core/src/DisputeKitClassic.ts @@ -14,6 +14,7 @@ import { ensureClassicContributionFromEvent } from "./entities/ClassicContributi import { createClassicDisputeFromEvent } from "./entities/ClassicDispute"; import { createClassicRound, + ensureAnswer, updateChoiceFundingFromContributionEvent, updateCountsAndGetCurrentRuling, } from "./entities/ClassicRound"; @@ -101,11 +102,16 @@ export function handleChoiceFunded(event: ChoiceFunded): void { const localRound = ClassicRound.load(roundID); if (!localRound) return; + const answer = ensureAnswer(roundID, choice); + const currentFeeRewards = localRound.feeRewards; - const deltaFeeRewards = localRound.paidFees[choice.toI32()]; + const deltaFeeRewards = answer.paidFee; localRound.feeRewards = currentFeeRewards.plus(deltaFeeRewards); localRound.fundedChoices = localRound.fundedChoices.concat([choice]); + answer.funded = true; + answer.save(); + if (localRound.fundedChoices.length > 1) { const disputeKitClassic = DisputeKitClassic.bind(event.address); const klerosCore = KlerosCore.bind(disputeKitClassic.core()); diff --git a/subgraph/core/src/entities/ClassicRound.ts b/subgraph/core/src/entities/ClassicRound.ts index 93c3ae00e..0f549bd6e 100644 --- a/subgraph/core/src/entities/ClassicRound.ts +++ b/subgraph/core/src/entities/ClassicRound.ts @@ -1,20 +1,17 @@ import { BigInt } from "@graphprotocol/graph-ts"; import { Contribution } from "../../generated/DisputeKitClassic/DisputeKitClassic"; -import { ClassicRound } from "../../generated/schema"; +import { Answer, ClassicRound } from "../../generated/schema"; import { ONE, ZERO } from "../utils"; export function createClassicRound(disputeID: string, numberOfChoices: BigInt, roundIndex: BigInt): void { - const choicesLength = numberOfChoices.plus(ONE); const localDisputeID = `1-${disputeID}`; const id = `${localDisputeID}-${roundIndex.toString()}`; const classicRound = new ClassicRound(id); classicRound.localDispute = localDisputeID; classicRound.winningChoice = ZERO; - classicRound.counts = new Array(choicesLength.toI32()).fill(ZERO); classicRound.tied = true; classicRound.totalVoted = ZERO; classicRound.totalCommited = ZERO; - classicRound.paidFees = new Array(choicesLength.toI32()).fill(ZERO); classicRound.feeRewards = ZERO; classicRound.appealFeesDispersed = false; classicRound.totalFeeDispersed = ZERO; @@ -27,21 +24,31 @@ class CurrentRulingInfo { tied: boolean; } +export function ensureAnswer(localRoundId: string, answerId: BigInt): Answer { + const id = `${localRoundId}-${answerId}`; + let answer = Answer.load(id); + if (answer) return answer; + answer = new Answer(id); + answer.answerId = answerId; + answer.count = ZERO; + answer.paidFee = ZERO; + answer.funded = false; + answer.localRound = localRoundId; + return answer; +} + export function updateCountsAndGetCurrentRuling(id: string, choice: BigInt, delta: BigInt): CurrentRulingInfo { const round = ClassicRound.load(id); if (!round) return { ruling: ZERO, tied: false }; - const choiceNum = choice.toI32(); - const newChoiceCount = round.counts[choiceNum].plus(delta); - let newCounts: BigInt[] = []; - for (let i = 0; i < round.counts.length; i++) { - if (BigInt.fromI32(i).equals(choice)) { - newCounts.push(newChoiceCount); - } else { - newCounts.push(round.counts[i]); - } - } - round.counts = newCounts; - const currentWinningCount = round.counts[round.winningChoice.toI32()]; + const answer = ensureAnswer(id, choice); + + answer.count = answer.count.plus(delta); + + const newChoiceCount = answer.count; + + const winningAnswer = ensureAnswer(id, round.winningChoice); + const currentWinningCount = winningAnswer.count; + if (choice.equals(round.winningChoice)) { if (round.tied) round.tied = false; } else { @@ -53,6 +60,8 @@ export function updateCountsAndGetCurrentRuling(id: string, choice: BigInt, delt } } round.totalVoted = round.totalVoted.plus(delta); + + answer.save(); round.save(); return { ruling: round.winningChoice, tied: round.tied }; } @@ -68,15 +77,9 @@ export function updateChoiceFundingFromContributionEvent(event: Contribution): v const choice = event.params._choice; const amount = event.params._amount; - const currentPaidFees = classicRound.paidFees[choice.toI32()]; - let newPaidFees: BigInt[] = []; - for (let i = 0; i < classicRound.paidFees.length; i++) { - if (BigInt.fromI32(i).equals(choice)) { - newPaidFees.push(currentPaidFees.plus(amount)); - } else { - newPaidFees.push(classicRound.paidFees[i]); - } - } - classicRound.paidFees = newPaidFees; + const answer = ensureAnswer(roundID, choice); + answer.paidFee = answer.paidFee.plus(amount); + + answer.save(); classicRound.save(); } From 8c5e57cfac5d288bbe6b57287c28778b009b86e6 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Fri, 17 Jan 2025 15:57:30 +0530 Subject: [PATCH 3/8] fix(web): answer-id-in-dispute-flow --- .../components/Verdict/DisputeTimeline.tsx | 6 +-- .../hooks/queries/useClassicAppealQuery.ts | 7 ++- web/src/hooks/useClassicAppealContext.tsx | 48 ++++++++++++------- .../CaseDetails/Appeal/AppealHistory.tsx | 14 +++--- .../Cases/CaseDetails/Appeal/Classic/Fund.tsx | 2 +- .../Appeal/Classic/Options/StageOne.tsx | 23 +++++---- .../Appeal/Classic/Options/StageTwo.tsx | 22 +++++---- .../Appeal/Classic/StageExplainer.tsx | 15 ++---- .../CaseDetails/Appeal/Classic/index.tsx | 7 ++- .../CaseDetails/Voting/Classic/Commit.tsx | 6 +-- .../Voting/Classic/OptionsContainer.tsx | 21 ++++---- .../CaseDetails/Voting/Classic/Reveal.tsx | 20 ++++---- .../Voting/VotesDetails/AccordionTitle.tsx | 2 +- .../CaseDetails/Voting/VotesDetails/index.tsx | 2 +- web/src/utils/getVoteChoice.ts | 13 ++--- 15 files changed, 109 insertions(+), 99 deletions(-) diff --git a/web/src/components/Verdict/DisputeTimeline.tsx b/web/src/components/Verdict/DisputeTimeline.tsx index 0412bbb0d..19d2b09e5 100644 --- a/web/src/components/Verdict/DisputeTimeline.tsx +++ b/web/src/components/Verdict/DisputeTimeline.tsx @@ -105,7 +105,7 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string const answers = disputeData?.answers; acc.push({ title: `Jury Decision - Round ${index + 1}`, - party: isOngoing ? "Voting is ongoing" : getVoteChoice(parsedRoundChoice, answers), + party: isOngoing ? "Voting is ongoing" : getVoteChoice(winningChoice, answers), subtitle: isOngoing ? "" : `${formatDate(roundTimeline?.[Periods.vote])} / ${ @@ -124,10 +124,10 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string rightSided: true, Icon: StyledClosedCircle, }); - } else if (rulingOverride && parsedDisputeFinalRuling !== parsedRoundChoice) { + } else if (rulingOverride && dispute.currentRuling !== winningChoice) { acc.push({ title: "Won by Appeal", - party: getVoteChoice(parsedDisputeFinalRuling, answers), + party: getVoteChoice(dispute.currentRuling, answers), subtitle: formatDate(roundTimeline?.[Periods.appeal]), rightSided: true, Icon: ClosedCaseIcon, diff --git a/web/src/hooks/queries/useClassicAppealQuery.ts b/web/src/hooks/queries/useClassicAppealQuery.ts index d12a76238..f430f197b 100644 --- a/web/src/hooks/queries/useClassicAppealQuery.ts +++ b/web/src/hooks/queries/useClassicAppealQuery.ts @@ -25,7 +25,12 @@ const classicAppealQuery = graphql(` localRounds { ... on ClassicRound { winningChoice - paidFees + answers { + answerId + count + paidFee + funded + } fundedChoices appealFeesDispersed totalFeeDispersed diff --git a/web/src/hooks/useClassicAppealContext.tsx b/web/src/hooks/useClassicAppealContext.tsx index 687fc184c..8ef6c528f 100644 --- a/web/src/hooks/useClassicAppealContext.tsx +++ b/web/src/hooks/useClassicAppealContext.tsx @@ -11,18 +11,21 @@ import { getLocalRounds } from "utils/getLocalRounds"; import { useAppealCost } from "queries/useAppealCost"; import { useClassicAppealQuery, ClassicAppealQuery } from "queries/useClassicAppealQuery"; import { useDisputeKitClassicMultipliers } from "queries/useDisputeKitClassicMultipliers"; +import { Answer, DisputeDetails } from "@kleros/kleros-sdk"; +type Option = Answer & { paidFee?: string; funded?: boolean }; interface ICountdownContext { loserSideCountdown?: number; winnerSideCountdown?: number; } + const CountdownContext = createContext({}); -const OptionsContext = createContext(undefined); +const OptionsContext = createContext(undefined); interface ISelectedOptionContext { - selectedOption: number | undefined; - setSelectedOption: (arg0: number) => void; + selectedOption: Option | undefined; + setSelectedOption: (arg0: Option) => void; } const SelectedOptionContext = createContext({ selectedOption: undefined, @@ -32,14 +35,13 @@ const SelectedOptionContext = createContext({ interface IFundingContext { winningChoice: string | undefined; - paidFees: bigint[] | undefined; loserRequiredFunding: bigint | undefined; winnerRequiredFunding: bigint | undefined; fundedChoices: string[] | undefined; } + const FundingContext = createContext({ winningChoice: undefined, - paidFees: undefined, loserRequiredFunding: undefined, winnerRequiredFunding: undefined, fundedChoices: undefined, @@ -51,17 +53,16 @@ export const ClassicAppealProvider: React.FC<{ const { id } = useParams(); const { data } = useClassicAppealQuery(id); const dispute = data?.dispute; - const paidFees = getPaidFees(data?.dispute); const winningChoice = getWinningChoice(data?.dispute); const { data: appealCost } = useAppealCost(id); const arbitrable = data?.dispute?.arbitrated.id; - const { data: disputeDetails } = usePopulatedDisputeData(id, arbitrable); + const { data: disputeDetails } = usePopulatedDisputeData(id, arbitrable as `0x${string}`); const { data: multipliers } = useDisputeKitClassicMultipliers(); - const options = ["Refuse to Arbitrate"].concat( - disputeDetails?.answers?.map((answer: { title: string; description: string }) => { - return answer.title; - }) - ); + + const [selectedOption, setSelectedOption] = useState