Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/use answer id in voting #1839

Merged
merged 12 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 && Number(answer.id) !== 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 @@ -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!
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
55 changes: 29 additions & 26 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 { 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<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();
}
6 changes: 3 additions & 3 deletions web/src/components/Verdict/DisputeTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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])} / ${
Expand All @@ -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,
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
48 changes: 30 additions & 18 deletions web/src/hooks/useClassicAppealContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ICountdownContext>({});

const OptionsContext = createContext<string[] | undefined>(undefined);
const OptionsContext = createContext<Option[] | undefined>(undefined);

interface ISelectedOptionContext {
selectedOption: number | undefined;
setSelectedOption: (arg0: number) => void;
selectedOption: Option | undefined;
setSelectedOption: (arg0: Option) => void;
}
const SelectedOptionContext = createContext<ISelectedOptionContext>({
selectedOption: undefined,
Expand All @@ -32,14 +35,13 @@ const SelectedOptionContext = createContext<ISelectedOptionContext>({

interface IFundingContext {
winningChoice: string | undefined;
paidFees: bigint[] | undefined;
loserRequiredFunding: bigint | undefined;
winnerRequiredFunding: bigint | undefined;
fundedChoices: string[] | undefined;
}

const FundingContext = createContext<IFundingContext>({
winningChoice: undefined,
paidFees: undefined,
loserRequiredFunding: undefined,
winnerRequiredFunding: undefined,
fundedChoices: undefined,
Expand All @@ -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<Option>();

const options = useMemo(() => getOptions(disputeDetails, data?.dispute), [disputeDetails, data]);

const loserSideCountdown = useLoserSideCountdown(
dispute?.lastPeriodChange,
dispute?.court.timesPerPeriod[Periods.appeal],
Expand All @@ -81,7 +82,6 @@ export const ClassicAppealProvider: React.FC<{
[appealCost, multipliers]
);
const fundedChoices = getFundedChoices(data?.dispute);
const [selectedOption, setSelectedOption] = useState<number | undefined>();

return (
<CountdownContext.Provider
Expand All @@ -94,12 +94,11 @@ export const ClassicAppealProvider: React.FC<{
value={useMemo(
() => ({
winningChoice,
paidFees,
loserRequiredFunding,
winnerRequiredFunding,
fundedChoices,
}),
[winningChoice, paidFees, loserRequiredFunding, winnerRequiredFunding, fundedChoices]
[winningChoice, loserRequiredFunding, winnerRequiredFunding, fundedChoices]
)}
>
<OptionsContext.Provider value={options}>{children}</OptionsContext.Provider>
Expand All @@ -126,9 +125,22 @@ const getCurrentLocalRound = (dispute?: ClassicAppealQuery["dispute"]) => {
return getLocalRounds(dispute.disputeKitDispute)[adjustedRoundIndex];
};

const getPaidFees = (dispute?: ClassicAppealQuery["dispute"]) => {
const currentLocalRound = getCurrentLocalRound(dispute);
return currentLocalRound?.paidFees.map((amount: string) => BigInt(amount));
const getOptions = (dispute?: DisputeDetails, classicDispute?: ClassicAppealQuery["dispute"]) => {
if (!dispute) return [];
const currentLocalRound = getCurrentLocalRound(classicDispute);
const classicAnswers = currentLocalRound?.answers;

const options = dispute.answers.map((answer) => {
const classicAnswer = classicAnswers?.find((classicAnswer) => BigInt(classicAnswer.answerId) == BigInt(answer.id));
// converting hexadecimal id to stringified bigint to match id fomr subgraph
return {
...answer,
id: BigInt(answer.id).toString(),
paidFee: classicAnswer?.paidFee ?? "0",
funded: classicAnswer?.funded ?? false,
};
});
return options;
};

const getFundedChoices = (dispute?: ClassicAppealQuery["dispute"]) => {
Expand Down
14 changes: 7 additions & 7 deletions web/src/pages/Cases/CaseDetails/Appeal/AppealHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface IAppealHistory {

const AppealHistory: React.FC<IAppealHistory> = ({ isAppealMiniGuideOpen, toggleAppealMiniGuide }) => {
const options = useOptionsContext();
const { winningChoice, paidFees, fundedChoices } = useFundingContext();
const { winningChoice, fundedChoices } = useFundingContext();

return options && options.length > 2 ? (
<div>
Expand All @@ -38,13 +38,13 @@ const AppealHistory: React.FC<IAppealHistory> = ({ isAppealMiniGuideOpen, toggle
/>
</AppealHeader>
<OptionsContainer>
{options.map((option, index) => (
{options?.map((option) => (
<OptionCard
key={option + index}
text={option}
winner={index.toString() === winningChoice}
funding={BigInt(paidFees?.[index] ?? "0")}
required={fundedChoices?.includes(index.toString()) ? BigInt(paidFees?.[index] ?? "0") : undefined}
key={option.id}
text={option.title}
winner={option.id === winningChoice}
funding={BigInt(option.paidFee ?? 0)}
required={fundedChoices?.includes(option.id) ? BigInt(option.paidFee ?? 0) : undefined}
canBeSelected={false}
/>
))}
Expand Down
Loading
Loading