Skip to content
12 changes: 8 additions & 4 deletions kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
8 changes: 7 additions & 1 deletion kleros-sdk/src/dataMappings/utils/populateTemplate.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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;
};
11 changes: 0 additions & 11 deletions kleros-sdk/src/utils/getDispute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
13 changes: 11 additions & 2 deletions subgraph/core/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ interface Evidence {
fileTypeExtension: String
}


############
# Entities #
############
Expand Down Expand Up @@ -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!
Expand Down
8 changes: 7 additions & 1 deletion subgraph/core/src/DisputeKitClassic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ensureClassicContributionFromEvent } from "./entities/ClassicContributi
import { createClassicDisputeFromEvent } from "./entities/ClassicDispute";
import {
createClassicRound,
ensureAnswer,
updateChoiceFundingFromContributionEvent,
updateCountsAndGetCurrentRuling,
} from "./entities/ClassicRound";
Expand Down Expand Up @@ -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());
Expand Down
2 changes: 2 additions & 0 deletions subgraph/core/src/entities/ClassicContribution.ts
Original file line number Diff line number Diff line change
@@ -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<T>(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;

Expand Down
57 changes: 30 additions & 27 deletions subgraph/core/src/entities/ClassicRound.ts
Original file line number Diff line number Diff line change
@@ -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<BigInt>(choicesLength.toI32()).fill(ZERO);
classicRound.tied = true;
classicRound.totalVoted = ZERO;
classicRound.totalCommited = ZERO;
classicRound.paidFees = new Array<BigInt>(choicesLength.toI32()).fill(ZERO);
classicRound.feeRewards = ZERO;
classicRound.appealFeesDispersed = false;
classicRound.totalFeeDispersed = ZERO;
Expand All @@ -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 {
Expand All @@ -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 };
}
Expand All @@ -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();
}
2 changes: 1 addition & 1 deletion subgraph/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kleros/kleros-v2-subgraph",
"version": "0.10.3",
"version": "0.11.0",
"drtVersion": "0.11.0",
"license": "MIT",
"scripts": {
Expand Down
8 changes: 3 additions & 5 deletions web/src/components/Verdict/DisputeTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,18 @@ 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<TimelineItems>(
(acc, { winningChoice }, index) => {
const parsedRoundChoice = parseInt(winningChoice);
const isOngoing = index === localRounds.length - 1 && currentPeriodIndex < 3;
const roundTimeline = rounds?.[index].timeline;

const icon = dispute.ruled && !rulingOverride && index === localRounds.length - 1 ? ClosedCaseIcon : "";
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])} / ${
Expand All @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion web/src/hooks/queries/useClassicAppealQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ const classicAppealQuery = graphql(`
localRounds {
... on ClassicRound {
winningChoice
paidFees
answers {
answerId
count
paidFee
funded
}
fundedChoices
appealFeesDispersed
totalFeeDispersed
Expand Down
Loading