diff --git a/subgraph/core/schema.graphql b/subgraph/core/schema.graphql index 6f7c13e5f..3ed29d85f 100644 --- a/subgraph/core/schema.graphql +++ b/subgraph/core/schema.graphql @@ -79,9 +79,10 @@ type User @entity { resolvedDisputes: [Dispute!]! totalResolvedDisputes: BigInt! totalDisputes: BigInt! - totalCoherent: BigInt! - coherenceScore: BigInt! totalAppealingDisputes: BigInt! + totalCoherentVotes: BigInt! + totalResolvedVotes: BigInt! + coherenceScore: BigInt! votes: [Vote!]! @derivedFrom(field: "juror") contributions: [Contribution!]! @derivedFrom(field: "contributor") evidences: [Evidence!]! @derivedFrom(field: "sender") diff --git a/subgraph/core/src/KlerosCore.ts b/subgraph/core/src/KlerosCore.ts index c252fcce1..8f968f83d 100644 --- a/subgraph/core/src/KlerosCore.ts +++ b/subgraph/core/src/KlerosCore.ts @@ -19,12 +19,12 @@ import { createDisputeKitFromEvent, filterSupportedDisputeKits } from "./entitie import { createDisputeFromEvent } from "./entities/Dispute"; import { createRoundFromRoundInfo, updateRoundTimeline } from "./entities/Round"; import { updateCases, updateCasesAppealing, updateCasesRuled, updateCasesVoting } from "./datapoint"; -import { addUserActiveDispute, ensureUser } from "./entities/User"; +import { addUserActiveDispute, computeCoherenceScore, ensureUser } from "./entities/User"; import { updateJurorStake } from "./entities/JurorTokensPerCourt"; import { createDrawFromEvent } from "./entities/Draw"; import { updateTokenAndEthShiftFromEvent } from "./entities/TokenAndEthShift"; import { updateArbitrableCases } from "./entities/Arbitrable"; -import { Court, Dispute, Round, User } from "../generated/schema"; +import { ClassicVote, Court, Dispute, Draw, Round, User } from "../generated/schema"; import { BigInt } from "@graphprotocol/graph-ts"; import { updatePenalty } from "./entities/Penalty"; import { ensureFeeToken } from "./entities/FeeToken"; @@ -127,6 +127,41 @@ export function handleNewPeriod(event: NewPeriod): void { dispute.currentRuling = currentRulingInfo.getRuling(); dispute.overridden = currentRulingInfo.getOverridden(); dispute.tied = currentRulingInfo.getTied(); + + const rounds = dispute.rounds.load(); + for (let i = 0; i < rounds.length; i++) { + const round = Round.load(rounds[i].id); + if (!round) continue; + + const draws = round.drawnJurors.load(); + // Iterate over all draws in the round + for (let j = 0; j < draws.length; j++) { + const draw = Draw.load(draws[j].id); + if (!draw) continue; + + // Since this is a ClassicVote entity, this will only work for the Classic DisputeKit (which has ID "1"). + const vote = ClassicVote.load(`${round.disputeKit}-${draw.id}`); + + if (!vote) continue; + + const juror = ensureUser(draw.juror); + juror.totalResolvedVotes = juror.totalResolvedVotes.plus(ONE); + + if (vote.choice === null) continue; + + // Check if the vote choice matches the final ruling + if (vote.choice!.equals(dispute.currentRuling)) { + juror.totalCoherentVotes = juror.totalCoherentVotes.plus(ONE); + } + + // Recalculate coherenceScore + if (juror.totalResolvedVotes.gt(ZERO)) { + juror.coherenceScore = computeCoherenceScore(juror.totalCoherentVotes, juror.totalResolvedVotes); + } + + juror.save(); + } + } } dispute.period = newPeriod; diff --git a/subgraph/core/src/entities/TokenAndEthShift.ts b/subgraph/core/src/entities/TokenAndEthShift.ts index 5fca5dec4..6225c3d17 100644 --- a/subgraph/core/src/entities/TokenAndEthShift.ts +++ b/subgraph/core/src/entities/TokenAndEthShift.ts @@ -29,7 +29,7 @@ export function updateTokenAndEthShiftFromEvent(event: TokenAndETHShiftEvent): v const previousEthAmount = shift.ethAmount; const newEthAmount = previousEthAmount.plus(ethAmount); shift.ethAmount = newEthAmount; - resolveUserDispute(jurorAddress.toHexString(), previousEthAmount, newEthAmount, disputeID.toString()); + resolveUserDispute(jurorAddress.toHexString(), disputeID.toString()); court.paidETH = court.paidETH.plus(ethAmount); updatePaidETH(ethAmount, event.block.timestamp); if (pnkAmount.gt(ZERO)) { diff --git a/subgraph/core/src/entities/User.ts b/subgraph/core/src/entities/User.ts index 85a9da833..f038ad5ac 100644 --- a/subgraph/core/src/entities/User.ts +++ b/subgraph/core/src/entities/User.ts @@ -2,11 +2,11 @@ import { BigInt, BigDecimal } from "@graphprotocol/graph-ts"; import { User } from "../../generated/schema"; import { ONE, ZERO } from "../utils"; -export function computeCoherenceScore(totalCoherent: BigInt, totalResolvedDisputes: BigInt): BigInt { +export function computeCoherenceScore(totalCoherentVotes: BigInt, totalResolvedVotes: BigInt): BigInt { const smoothingFactor = BigDecimal.fromString("10"); - let denominator = totalResolvedDisputes.toBigDecimal().plus(smoothingFactor); - let coherencyRatio = totalCoherent.toBigDecimal().div(denominator); + let denominator = totalResolvedVotes.toBigDecimal().plus(smoothingFactor); + let coherencyRatio = totalCoherentVotes.toBigDecimal().div(denominator); const coherencyScore = coherencyRatio.times(BigDecimal.fromString("100")); @@ -36,7 +36,8 @@ export function createUserFromAddress(id: string): User { user.totalResolvedDisputes = ZERO; user.totalAppealingDisputes = ZERO; user.totalDisputes = ZERO; - user.totalCoherent = ZERO; + user.totalCoherentVotes = ZERO; + user.totalResolvedVotes = ZERO; user.coherenceScore = ZERO; user.save(); @@ -54,28 +55,13 @@ export function addUserActiveDispute(id: string, disputeID: string): void { user.save(); } -export function resolveUserDispute(id: string, previousFeeAmount: BigInt, feeAmount: BigInt, disputeID: string): void { +export function resolveUserDispute(id: string, disputeID: string): void { const user = ensureUser(id); if (user.resolvedDisputes.includes(disputeID)) { - if (previousFeeAmount.gt(ZERO)) { - if (feeAmount.le(ZERO)) { - user.totalCoherent = user.totalCoherent.minus(ONE); - } - } else if (previousFeeAmount.le(ZERO)) { - if (feeAmount.gt(ZERO)) { - user.totalCoherent = user.totalCoherent.plus(ONE); - } - } - user.coherenceScore = computeCoherenceScore(user.totalCoherent, user.totalResolvedDisputes); - user.save(); return; } user.resolvedDisputes = user.resolvedDisputes.concat([disputeID]); user.totalResolvedDisputes = user.totalResolvedDisputes.plus(ONE); - if (feeAmount.gt(ZERO)) { - user.totalCoherent = user.totalCoherent.plus(ONE); - } user.activeDisputes = user.activeDisputes.minus(ONE); - user.coherenceScore = computeCoherenceScore(user.totalCoherent, user.totalResolvedDisputes); user.save(); } diff --git a/subgraph/package.json b/subgraph/package.json index 9448c3b0b..71f1166ef 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-subgraph", - "version": "0.7.4", + "version": "0.7.6", "license": "MIT", "scripts": { "update:core:arbitrum-sepolia-devnet": "./scripts/update.sh arbitrumSepoliaDevnet arbitrum-sepolia core/subgraph.yaml", diff --git a/web/src/components/Popup/MiniGuides/JurorLevels.tsx b/web/src/components/Popup/MiniGuides/JurorLevels.tsx index 43bd79a54..6d1083211 100644 --- a/web/src/components/Popup/MiniGuides/JurorLevels.tsx +++ b/web/src/components/Popup/MiniGuides/JurorLevels.tsx @@ -63,32 +63,32 @@ const userLevelData = [ { level: 1, title: "Phytagoras", - totalCoherent: 6, - totalResolvedDisputes: 10, + totalCoherentVotes: 6, + totalResolvedVotes: 10, }, { level: 2, title: "Socrates", - totalCoherent: 7, - totalResolvedDisputes: 10, + totalCoherentVotes: 7, + totalResolvedVotes: 10, }, { level: 3, title: "Plato", - totalCoherent: 8, - totalResolvedDisputes: 10, + totalCoherentVotes: 8, + totalResolvedVotes: 10, }, { level: 4, title: "Aristotle", - totalCoherent: 9, - totalResolvedDisputes: 10, + totalCoherentVotes: 9, + totalResolvedVotes: 10, }, { level: 0, title: "Diogenes", - totalCoherent: 3, - totalResolvedDisputes: 10, + totalCoherentVotes: 3, + totalResolvedVotes: 10, }, ]; @@ -114,8 +114,8 @@ const RightContent: React.FC<{ currentPage: number }> = ({ currentPage }) => { diff --git a/web/src/hooks/queries/useTopUsersByCoherenceScore.ts b/web/src/hooks/queries/useTopUsersByCoherenceScore.ts index f45ad08d5..add534914 100644 --- a/web/src/hooks/queries/useTopUsersByCoherenceScore.ts +++ b/web/src/hooks/queries/useTopUsersByCoherenceScore.ts @@ -12,7 +12,8 @@ const topUsersByCoherenceScoreQuery = graphql(` users(first: $first, orderBy: $orderBy, orderDirection: $orderDirection) { id coherenceScore - totalCoherent + totalCoherentVotes + totalResolvedVotes totalResolvedDisputes } } diff --git a/web/src/hooks/queries/useUser.ts b/web/src/hooks/queries/useUser.ts index 0fe7216c0..d453035c9 100644 --- a/web/src/hooks/queries/useUser.ts +++ b/web/src/hooks/queries/useUser.ts @@ -12,7 +12,8 @@ export const userFragment = graphql(` totalDisputes totalResolvedDisputes totalAppealingDisputes - totalCoherent + totalCoherentVotes + totalResolvedVotes coherenceScore tokens { court { diff --git a/web/src/pages/Dashboard/JurorInfo/Coherency.tsx b/web/src/pages/Dashboard/JurorInfo/Coherency.tsx index bef8db218..ae164a75d 100644 --- a/web/src/pages/Dashboard/JurorInfo/Coherency.tsx +++ b/web/src/pages/Dashboard/JurorInfo/Coherency.tsx @@ -30,18 +30,18 @@ interface ICoherency { level: number; title: string; }; - totalCoherent: number; - totalResolvedDisputes: number; + totalCoherentVotes: number; + totalResolvedVotes: number; isMiniGuide: boolean; } -const Coherency: React.FC = ({ userLevelData, totalCoherent, totalResolvedDisputes, isMiniGuide }) => { +const Coherency: React.FC = ({ userLevelData, totalCoherentVotes, totalResolvedVotes, isMiniGuide }) => { const votesContent = ( ); @@ -51,7 +51,7 @@ const Coherency: React.FC = ({ userLevelData, totalCoherent, totalRe {userLevelData.title} {!isMiniGuide ? ( diff --git a/web/src/pages/Dashboard/JurorInfo/Header.tsx b/web/src/pages/Dashboard/JurorInfo/Header.tsx index 8db90b3c8..459e6c143 100644 --- a/web/src/pages/Dashboard/JurorInfo/Header.tsx +++ b/web/src/pages/Dashboard/JurorInfo/Header.tsx @@ -65,16 +65,16 @@ const StyledLink = styled.a` interface IHeader { levelTitle: string; levelNumber: number; - totalCoherent: number; - totalResolvedDisputes: number; + totalCoherentVotes: number; + totalResolvedVotes: number; } -const Header: React.FC = ({ levelTitle, levelNumber, totalCoherent, totalResolvedDisputes }) => { +const Header: React.FC = ({ levelTitle, levelNumber, totalCoherentVotes, totalResolvedVotes }) => { const [isJurorLevelsMiniGuideOpen, toggleJurorLevelsMiniGuide] = useToggle(false); - const coherencePercentage = parseFloat(((totalCoherent / Math.max(totalResolvedDisputes, 1)) * 100).toFixed(2)); + const coherencePercentage = parseFloat(((totalCoherentVotes / Math.max(totalResolvedVotes, 1)) * 100).toFixed(2)); const courtUrl = window.location.origin; - const xPostText = `Hey I've been busy as a Juror on the Kleros court, check out my score: \n\nLevel: ${levelNumber} (${levelTitle})\nCoherence Percentage: ${coherencePercentage}%\nCoherent Votes: ${totalCoherent}/${totalResolvedDisputes}\n\nBe a juror with me! ➡️ ${courtUrl}`; + const xPostText = `Hey I've been busy as a Juror on the Kleros court, check out my score: \n\nLevel: ${levelNumber} (${levelTitle})\nCoherence Percentage: ${coherencePercentage}%\nCoherent Votes: ${totalCoherentVotes}/${totalResolvedVotes}\n\nBe a juror with me! ➡️ ${courtUrl}`; const xShareUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(xPostText)}`; return ( @@ -86,7 +86,7 @@ const Header: React.FC = ({ levelTitle, levelNumber, totalCoherent, tot toggleMiniGuide={toggleJurorLevelsMiniGuide} MiniGuideComponent={JurorLevels} /> - {totalResolvedDisputes > 0 ? ( + {totalResolvedVotes > 0 ? ( Share your juror score diff --git a/web/src/pages/Dashboard/JurorInfo/index.tsx b/web/src/pages/Dashboard/JurorInfo/index.tsx index dc0609ed9..db9f3ba43 100644 --- a/web/src/pages/Dashboard/JurorInfo/index.tsx +++ b/web/src/pages/Dashboard/JurorInfo/index.tsx @@ -44,27 +44,22 @@ const JurorInfo: React.FC = () => { const { data } = useUserQuery(address?.toLowerCase() as `0x${string}`); // TODO check graph schema const coherenceScore = data?.user ? parseInt(data?.user?.coherenceScore) : 0; - const totalCoherent = data?.user ? parseInt(data?.user?.totalCoherent) : 0; + const totalCoherentVotes = data?.user ? parseInt(data?.user?.totalCoherentVotes) : 0; + const totalResolvedVotes = data?.user ? parseInt(data?.user?.totalResolvedVotes) : 0; const totalResolvedDisputes = data?.user ? parseInt(data?.user?.totalResolvedDisputes) : 0; - const userLevelData = getUserLevelData(coherenceScore); + const userLevelData = getUserLevelData(coherenceScore, totalResolvedDisputes); return (
- + diff --git a/web/src/pages/Home/TopJurors/JurorCard/Coherency.tsx b/web/src/pages/Home/TopJurors/JurorCard/Coherency.tsx index 7b3b19448..3a57c46a8 100644 --- a/web/src/pages/Home/TopJurors/JurorCard/Coherency.tsx +++ b/web/src/pages/Home/TopJurors/JurorCard/Coherency.tsx @@ -11,12 +11,12 @@ const Container = styled.div` `; interface ICoherency { - totalCoherent: number; - totalResolvedDisputes: number; + totalCoherentVotes: number; + totalResolvedVotes: number; } -const Coherency: React.FC = ({ totalCoherent, totalResolvedDisputes }) => { - const coherenceRatio = `${totalCoherent}/${totalResolvedDisputes}`; +const Coherency: React.FC = ({ totalCoherentVotes, totalResolvedVotes }) => { + const coherenceRatio = `${totalCoherentVotes}/${totalResolvedVotes}`; return {coherenceRatio}; }; diff --git a/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx b/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx index 12b39f4c2..dddc5112b 100644 --- a/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx +++ b/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx @@ -33,7 +33,8 @@ const Container = styled.div` interface IDesktopCard { rank: number; address: string; - totalCoherent: number; + totalCoherentVotes: number; + totalResolvedVotes: number; totalResolvedDisputes: number; coherenceScore: number; } @@ -41,7 +42,8 @@ interface IDesktopCard { const DesktopCard: React.FC = ({ rank, address, - totalCoherent, + totalCoherentVotes, + totalResolvedVotes, totalResolvedDisputes, coherenceScore, }) => { @@ -50,8 +52,8 @@ const DesktopCard: React.FC = ({ - - + + ); }; diff --git a/web/src/pages/Home/TopJurors/JurorCard/JurorLevel.tsx b/web/src/pages/Home/TopJurors/JurorCard/JurorLevel.tsx index 3becac4c0..cdfce9ed2 100644 --- a/web/src/pages/Home/TopJurors/JurorCard/JurorLevel.tsx +++ b/web/src/pages/Home/TopJurors/JurorCard/JurorLevel.tsx @@ -40,10 +40,11 @@ const StyledLabel = styled.label` interface IJurorLevel { coherenceScore: number; + totalResolvedDisputes: number; } -const JurorLevel: React.FC = ({ coherenceScore }) => { - const userLevelData = getUserLevelData(coherenceScore); +const JurorLevel: React.FC = ({ coherenceScore, totalResolvedDisputes }) => { + const userLevelData = getUserLevelData(coherenceScore, totalResolvedDisputes); const level = userLevelData.level; return ( diff --git a/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx b/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx index 54f0be3e1..dd34de339 100644 --- a/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx +++ b/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx @@ -75,11 +75,19 @@ interface IMobileCard { rank: number; address: string; coherenceScore: number; - totalCoherent: number; + totalCoherentVotes: number; + totalResolvedVotes: number; totalResolvedDisputes: number; } -const MobileCard: React.FC = ({ rank, address, coherenceScore, totalCoherent, totalResolvedDisputes }) => { +const MobileCard: React.FC = ({ + rank, + address, + coherenceScore, + totalCoherentVotes, + totalResolvedVotes, + totalResolvedDisputes, +}) => { return ( @@ -87,7 +95,7 @@ const MobileCard: React.FC = ({ rank, address, coherenceScore, tota - + @@ -96,7 +104,7 @@ const MobileCard: React.FC = ({ rank, address, coherenceScore, tota - + diff --git a/web/src/pages/Home/TopJurors/JurorCard/index.tsx b/web/src/pages/Home/TopJurors/JurorCard/index.tsx index 9ae084dbf..6b6a5c973 100644 --- a/web/src/pages/Home/TopJurors/JurorCard/index.tsx +++ b/web/src/pages/Home/TopJurors/JurorCard/index.tsx @@ -7,12 +7,20 @@ interface IJurorCard { rank: number; address: `0x${string}`; coherenceScore: number; - totalCoherent: number; + totalCoherentVotes: number; + totalResolvedVotes: number; totalResolvedDisputes: number; } -const JurorCard: React.FC = ({ rank, address, coherenceScore, totalCoherent, totalResolvedDisputes }) => { - const allProps = { rank, address, coherenceScore, totalCoherent, totalResolvedDisputes }; +const JurorCard: React.FC = ({ + rank, + address, + coherenceScore, + totalCoherentVotes, + totalResolvedVotes, + totalResolvedDisputes, +}) => { + const allProps = { rank, address, coherenceScore, totalCoherentVotes, totalResolvedVotes, totalResolvedDisputes }; return ( <> diff --git a/web/src/utils/userLevelCalculation.ts b/web/src/utils/userLevelCalculation.ts index 69344098e..e77071786 100644 --- a/web/src/utils/userLevelCalculation.ts +++ b/web/src/utils/userLevelCalculation.ts @@ -1,15 +1,29 @@ -export const levelTitles = [ - { scoreRange: [0, 20], level: 0, title: "Diogenes" }, - { scoreRange: [20, 40], level: 1, title: "Pythagoras" }, - { scoreRange: [40, 60], level: 2, title: "Socrates" }, - { scoreRange: [60, 80], level: 3, title: "Plato" }, - { scoreRange: [80, 100], level: 4, title: "Aristotle" }, +interface ILevelCriteria { + level: number; + title: string; + minDisputes: number; + minScore: number; + maxScore: number; +} + +const levelCriteria: ILevelCriteria[] = [ + { level: 0, title: "Diogenes", minDisputes: 3, minScore: 0, maxScore: 49 }, + { level: 1, title: "Pythagoras", minDisputes: 0, minScore: 0, maxScore: 70 }, + { level: 2, title: "Socrates", minDisputes: 3, minScore: 71, maxScore: 80 }, + { level: 3, title: "Plato", minDisputes: 7, minScore: 81, maxScore: 90 }, + { level: 4, title: "Aristotle", minDisputes: 10, minScore: 91, maxScore: 100 }, ]; -export const getUserLevelData = (coherencyScore: number) => { - return ( - levelTitles.find(({ scoreRange }) => { - return coherencyScore >= scoreRange[0] && coherencyScore < scoreRange[1]; - }) ?? levelTitles[0] - ); +export const getUserLevelData = (coherencyScore: number, totalResolvedDisputes: number) => { + for (const criteria of levelCriteria) { + if ( + totalResolvedDisputes >= criteria.minDisputes && + coherencyScore >= criteria.minScore && + coherencyScore <= criteria.maxScore + ) { + return levelCriteria.find(({ level }) => level === criteria.level); + } + } + + return levelCriteria.find(({ level }) => level === 1); };