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..a70668ab7 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 && BigInt(answer.id) !== BigInt(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; }; diff --git a/subgraph/core/schema.graphql b/subgraph/core/schema.graphql index 903c967d1..956d102af 100644 --- a/subgraph/core/schema.graphql +++ b/subgraph/core/schema.graphql @@ -63,6 +63,7 @@ interface Evidence { fileTypeExtension: String } + ############ # Entities # ############ @@ -267,17 +268,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/ClassicContribution.ts b/subgraph/core/src/entities/ClassicContribution.ts index 6c9b59077..c9d11211f 100644 --- a/subgraph/core/src/entities/ClassicContribution.ts +++ b/subgraph/core/src/entities/ClassicContribution.ts @@ -1,12 +1,14 @@ import { ClassicContribution } from "../../generated/schema"; import { Contribution as ContributionEvent, Withdrawal } from "../../generated/DisputeKitClassic/DisputeKitClassic"; import { DISPUTEKIT_ID } from "../DisputeKitClassic"; +import { ensureUser } from "./User"; export function ensureClassicContributionFromEvent(event: T): ClassicContribution | null { if (!(event instanceof ContributionEvent) && !(event instanceof Withdrawal)) return null; const coreDisputeID = event.params._coreDisputeID.toString(); const coreRoundIndex = event.params._coreRoundID.toString(); const roundID = `${DISPUTEKIT_ID}-${coreDisputeID}-${coreRoundIndex}`; + ensureUser(event.params._contributor.toHexString()); const contributor = event.params._contributor.toHexString(); const choice = event.params._choice; diff --git a/subgraph/core/src/entities/ClassicRound.ts b/subgraph/core/src/entities/ClassicRound.ts index 93c3ae00e..86dc57491 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 { ONE, ZERO } from "../utils"; +import { Answer, ClassicRound } from "../../generated/schema"; +import { 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(); } diff --git a/subgraph/package.json b/subgraph/package.json index de6899c48..f655b6e86 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-subgraph", - "version": "0.10.3", + "version": "0.11.0", "drtVersion": "0.11.0", "license": "MIT", "scripts": { diff --git a/web/src/components/Verdict/DisputeTimeline.tsx b/web/src/components/Verdict/DisputeTimeline.tsx index 0412bbb0d..2bbc53879 100644 --- a/web/src/components/Verdict/DisputeTimeline.tsx +++ b/web/src/components/Verdict/DisputeTimeline.tsx @@ -92,12 +92,10 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string const dispute = disputeDetails?.dispute; if (dispute) { const rulingOverride = dispute.overridden; - const parsedDisputeFinalRuling = parseInt(dispute.currentRuling); const currentPeriodIndex = Periods[dispute.period]; return localRounds?.reduce( (acc, { winningChoice }, index) => { - const parsedRoundChoice = parseInt(winningChoice); const isOngoing = index === localRounds.length - 1 && currentPeriodIndex < 3; const roundTimeline = rounds?.[index].timeline; @@ -105,7 +103,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 +122,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 8a5ddf073..e0458d02e 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 4a32f38b7..ea5b66297 100644 --- a/web/src/hooks/useClassicAppealContext.tsx +++ b/web/src/hooks/useClassicAppealContext.tsx @@ -12,19 +12,22 @@ import { isUndefined } from "utils/index"; 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; isLoading?: boolean; } + 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, @@ -34,14 +37,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, @@ -53,17 +55,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