Skip to content

feat(subgraph/web): time travel query refactor #1939

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

Merged
merged 5 commits into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions subgraph/core/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,15 @@ type Counter @entity {
totalLeaderboardJurors: BigInt!
}

type CourtCounter @entity {
id: ID! # court.id-timestamp
court: Court!
numberDisputes: BigInt!
numberVotes: BigInt!
effectiveStake: BigInt!
timestamp: BigInt!
}

type FeeToken @entity {
id: ID! # The address of the ERC20 token.
accepted: Boolean!
Expand Down
4 changes: 4 additions & 0 deletions subgraph/core/src/KlerosCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
updateCasesAppealing,
updateCasesRuled,
updateCasesVoting,
updateCourtCumulativeMetric,
updateTotalLeaderboardJurors,
} from "./datapoint";
import { addUserActiveDispute, computeCoherenceScore, ensureUser } from "./entities/User";
Expand Down Expand Up @@ -81,9 +82,11 @@ export function handleDisputeCreation(event: DisputeCreation): void {
const court = Court.load(courtID);
if (!court) return;
court.numberDisputes = court.numberDisputes.plus(ONE);
updateCourtCumulativeMetric(courtID, ONE, event.block.timestamp, "numberDisputes");

const roundInfo = contract.getRoundInfo(disputeID, ZERO);
court.numberVotes = court.numberVotes.plus(roundInfo.nbVotes);
updateCourtCumulativeMetric(courtID, roundInfo.nbVotes, event.block.timestamp, "numberVotes");

court.save();
createDisputeFromEvent(event);
Expand Down Expand Up @@ -225,6 +228,7 @@ export function handleAppealDecision(event: AppealDecision): void {
if (!court) return;

court.numberVotes = court.numberVotes.plus(roundInfo.nbVotes);
updateCourtCumulativeMetric(courtID, roundInfo.nbVotes, event.block.timestamp, "numberVotes");
court.save();

createRoundFromRoundInfo(KlerosCore.bind(event.address), disputeID, newRoundIndex, roundInfo);
Expand Down
76 changes: 75 additions & 1 deletion subgraph/core/src/datapoint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BigInt, Entity, Value, store } from "@graphprotocol/graph-ts";
import { Counter } from "../generated/schema";
import { Counter, CourtCounter } from "../generated/schema";
import { ZERO } from "./utils";

export function getDelta(previousValue: BigInt, newValue: BigInt): BigInt {
Expand Down Expand Up @@ -92,3 +92,77 @@ export function updateCasesAppealing(delta: BigInt, timestamp: BigInt): void {
export function updateTotalLeaderboardJurors(delta: BigInt, timestamp: BigInt): void {
updateDataPoint(delta, timestamp, "totalLeaderboardJurors");
}

export function updateCourtCumulativeMetric(courtId: string, delta: BigInt, timestamp: BigInt, metric: string): void {
// Load or create the current CourtCounter (ID: courtId-0)
let currentCounter = CourtCounter.load(courtId + "-0");
if (!currentCounter) {
currentCounter = new CourtCounter(courtId + "-0");
currentCounter.court = courtId;
currentCounter.numberDisputes = ZERO;
currentCounter.numberVotes = ZERO;
currentCounter.effectiveStake = ZERO;
currentCounter.timestamp = ZERO;
}
if (metric === "numberDisputes") {
currentCounter.numberDisputes = currentCounter.numberDisputes.plus(delta);
} else if (metric === "numberVotes") {
currentCounter.numberVotes = currentCounter.numberVotes.plus(delta);
}
currentCounter.save();

// Update daily snapshot
let dayID = timestamp.toI32() / 86400; // Seconds to days
let dayStartTimestamp = dayID * 86400;
let dailyCounter = CourtCounter.load(courtId + "-" + dayStartTimestamp.toString());
if (!dailyCounter) {
dailyCounter = new CourtCounter(courtId + "-" + dayStartTimestamp.toString());
dailyCounter.court = courtId;
dailyCounter.numberDisputes = currentCounter.numberDisputes.minus(delta); // State before this update
dailyCounter.numberVotes = currentCounter.numberVotes.minus(delta);
dailyCounter.effectiveStake = currentCounter.effectiveStake;
dailyCounter.timestamp = BigInt.fromI32(dayStartTimestamp);
}
if (metric === "numberDisputes") {
dailyCounter.numberDisputes = dailyCounter.numberDisputes.plus(delta);
} else if (metric === "numberVotes") {
dailyCounter.numberVotes = dailyCounter.numberVotes.plus(delta);
}
dailyCounter.save();
}

export function updateCourtStateVariable(courtId: string, newValue: BigInt, timestamp: BigInt, variable: string): void {
// Load or create the current CourtCounter (ID: courtId-0)
let currentCounter = CourtCounter.load(courtId + "-0");
if (!currentCounter) {
currentCounter = new CourtCounter(courtId + "-0");
currentCounter.court = courtId;
currentCounter.numberDisputes = ZERO;
currentCounter.numberVotes = ZERO;
currentCounter.effectiveStake = newValue;
currentCounter.timestamp = ZERO;
} else {
if (variable === "effectiveStake") {
currentCounter.effectiveStake = newValue;
}
currentCounter.save();
}

// Update daily snapshot
let dayID = timestamp.toI32() / 86400;
let dayStartTimestamp = dayID * 86400;
let dailyCounter = CourtCounter.load(courtId + "-" + dayStartTimestamp.toString());
if (!dailyCounter) {
dailyCounter = new CourtCounter(courtId + "-" + dayStartTimestamp.toString());
dailyCounter.court = courtId;
dailyCounter.numberDisputes = currentCounter.numberDisputes;
dailyCounter.numberVotes = currentCounter.numberVotes;
dailyCounter.effectiveStake = newValue;
dailyCounter.timestamp = BigInt.fromI32(dayStartTimestamp);
} else {
if (variable === "effectiveStake") {
dailyCounter.effectiveStake = newValue;
}
dailyCounter.save();
}
}
3 changes: 2 additions & 1 deletion subgraph/core/src/entities/JurorTokensPerCourt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BigInt, Address } from "@graphprotocol/graph-ts";
import { Court, JurorTokensPerCourt } from "../../generated/schema";
import { updateActiveJurors, getDelta, updateStakedPNK } from "../datapoint";
import { updateActiveJurors, getDelta, updateStakedPNK, updateCourtStateVariable } from "../datapoint";
import { ensureUser } from "./User";
import { ONE, ZERO } from "../utils";
import { SortitionModule } from "../../generated/SortitionModule/SortitionModule";
Expand Down Expand Up @@ -94,6 +94,7 @@ export function updateJurorStake(
court.save();
updateEffectiveStake(courtID);
updateJurorEffectiveStake(jurorAddress, courtID);
updateCourtStateVariable(courtID, court.effectiveStake, timestamp, "effectiveStake");
}

export function updateJurorDelayedStake(jurorAddress: string, courtID: string, amount: BigInt): void {
Expand Down
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.14.2",
"version": "0.15.0",
"drtVersion": "0.12.0",
"license": "MIT",
"scripts": {
Expand Down
174 changes: 101 additions & 73 deletions web/src/hooks/queries/useHomePageBlockQuery.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { useQuery } from "@tanstack/react-query";

import { useGraphqlBatcher } from "context/GraphqlBatcher";
import { isUndefined } from "utils/index";

import { graphql } from "src/graphql";
import { HomePageBlockQuery } from "src/graphql/graphql";
import useGenesisBlock from "../useGenesisBlock";
export type { HomePageBlockQuery };

const homePageBlockQuery = graphql(`
query HomePageBlock($blockNumber: Int) {
presentCourts: courts(orderBy: id, orderDirection: asc) {
query HomePageBlock($pastTimestamp: BigInt) {
presentCourts: courts(orderBy: id, orderDirection: asc, first: 1000) {
id
parent {
id
Expand All @@ -21,21 +17,25 @@ const homePageBlockQuery = graphql(`
feeForJuror
effectiveStake
}
pastCourts: courts(orderBy: id, orderDirection: asc, block: { number: $blockNumber }) {
id
parent {
pastCourts: courtCounters(
where: { timestamp_lte: $pastTimestamp }
orderBy: timestamp
orderDirection: desc
first: 1000
) {
court {
id
}
name
numberDisputes
numberVotes
feeForJuror
effectiveStake
timestamp
}
}
`);

type Court = HomePageBlockQuery["presentCourts"][number];
type CourtCounter = HomePageBlockQuery["pastCourts"][number];
type CourtWithTree = Court & {
numberDisputes: number;
numberVotes: number;
Expand All @@ -58,66 +58,62 @@ export type HomePageBlockStats = {
courts: CourtWithTree[];
};

export const useHomePageBlockQuery = (blockNumber: number | undefined, allTime: boolean) => {
const genesisBlock = useGenesisBlock();
const isEnabled = !isUndefined(blockNumber) || allTime || !isUndefined(genesisBlock);
const { graphqlBatcher } = useGraphqlBatcher();

return useQuery<HomePageBlockStats>({
queryKey: [`homePageBlockQuery${blockNumber}-${allTime}`],
enabled: isEnabled,
staleTime: Infinity,
queryFn: async () => {
const targetBlock = Math.max(blockNumber!, genesisBlock!);
const data = await graphqlBatcher.fetch({
id: crypto.randomUUID(),
document: homePageBlockQuery,
variables: { blockNumber: targetBlock },
});

return processData(data, allTime);
},
});
};
const getCourtMostDisputes = (courts: CourtWithTree[]) =>
courts.toSorted((a, b) => b.numberDisputes - a.numberDisputes)[0];
const getCourtBestDrawingChances = (courts: CourtWithTree[]) =>
courts.toSorted((a, b) => b.treeVotesPerPnk - a.treeVotesPerPnk)[0];
const getBestExpectedRewardCourt = (courts: CourtWithTree[]) =>
courts.toSorted((a, b) => b.treeExpectedRewardPerPnk - a.treeExpectedRewardPerPnk)[0];

const processData = (data: HomePageBlockQuery, allTime: boolean) => {
const presentCourts = data.presentCourts;
const presentCourts = [...data.presentCourts].sort((a, b) => Number(a.id) - Number(b.id));
const pastCourts = data.pastCourts;
const processedCourts: CourtWithTree[] = Array(presentCourts.length);
const processed = new Set();

const processCourt = (id: number): CourtWithTree => {
if (processed.has(id)) return processedCourts[id];

processed.add(id);
const court =
!allTime && id < data.pastCourts.length
? addTreeValuesWithDiff(presentCourts[id], pastCourts[id])
: addTreeValues(presentCourts[id]);
const parentIndex = court.parent ? Number(court.parent.id) - 1 : 0;

if (id === parentIndex) {
processedCourts[id] = court;
return court;

const presentCourtsMap = new Map(presentCourts.map((c) => [c.id, c]));
const pastCourtsMap = new Map<string, CourtCounter>();
if (!allTime) {
for (const pastCourt of pastCourts) {
const courtId = pastCourt.court.id;
if (!pastCourtsMap.has(courtId)) {
pastCourtsMap.set(courtId, pastCourt);
}
}
}

const processedCourtsMap = new Map<string, CourtWithTree>();
const processCourt = (courtId: string): CourtWithTree => {
if (processedCourtsMap.has(courtId)) return processedCourtsMap.get(courtId)!;

const court = presentCourtsMap.get(courtId)!;
const pastCourt = pastCourtsMap.get(courtId);

const courtWithTree = !allTime && pastCourt ? addTreeValuesWithDiff(court, pastCourt) : addTreeValues(court);

const parentId = court.parent?.id;
if (!parentId || courtId === parentId) {
processedCourtsMap.set(courtId, courtWithTree);
return courtWithTree;
}

processedCourts[id] = {
...court,
treeNumberDisputes: court.treeNumberDisputes + processCourt(parentIndex).treeNumberDisputes,
treeNumberVotes: court.treeNumberVotes + processCourt(parentIndex).treeNumberVotes,
treeVotesPerPnk: court.treeVotesPerPnk + processCourt(parentIndex).treeVotesPerPnk,
treeDisputesPerPnk: court.treeDisputesPerPnk + processCourt(parentIndex).treeDisputesPerPnk,
treeExpectedRewardPerPnk: court.treeExpectedRewardPerPnk + processCourt(parentIndex).treeExpectedRewardPerPnk,
const parentCourt = processCourt(parentId);
const fullTreeCourt: CourtWithTree = {
...courtWithTree,
treeNumberDisputes: courtWithTree.treeNumberDisputes + parentCourt.treeNumberDisputes,
treeNumberVotes: courtWithTree.treeNumberVotes + parentCourt.treeNumberVotes,
treeVotesPerPnk: courtWithTree.treeVotesPerPnk + parentCourt.treeVotesPerPnk,
treeDisputesPerPnk: courtWithTree.treeDisputesPerPnk + parentCourt.treeDisputesPerPnk,
treeExpectedRewardPerPnk: courtWithTree.treeExpectedRewardPerPnk + parentCourt.treeExpectedRewardPerPnk,
};

return processedCourts[id];
processedCourtsMap.set(courtId, fullTreeCourt);
return fullTreeCourt;
};

for (const court of presentCourts.toReversed()) {
processCourt(Number(court.id) - 1);
processCourt(court.id);
}

processedCourts.reverse();
const processedCourts = [...processedCourtsMap.values()].sort((a, b) => Number(a.id) - Number(b.id));

return {
mostDisputedCourt: getCourtMostDisputes(processedCourts),
Expand Down Expand Up @@ -148,21 +144,41 @@ const addTreeValues = (court: Court): CourtWithTree => {
};
};

const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: Court): CourtWithTree => {
const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: CourtCounter | undefined): CourtWithTree => {
const presentCourtWithTree = addTreeValues(presentCourt);
const pastCourtWithTree = addTreeValues(pastCourt);
const diffNumberVotes = presentCourtWithTree.numberVotes - pastCourtWithTree.numberVotes;
const diffNumberDisputes = presentCourtWithTree.numberDisputes - pastCourtWithTree.numberDisputes;
const avgEffectiveStake = (presentCourtWithTree.effectiveStake + pastCourtWithTree.effectiveStake) / 2n;

if (!pastCourt) {
console.warn(`Missing snapshot for court ${presentCourt.id}, falling back to live`);
return presentCourtWithTree;
}

const pastNumberVotes = Number(pastCourt.numberVotes);
const pastNumberDisputes = Number(pastCourt.numberDisputes);
const pastEffectiveStake = BigInt(pastCourt.effectiveStake);

const diffNumberVotes = presentCourtWithTree.numberVotes - pastNumberVotes;
const diffNumberDisputes = presentCourtWithTree.numberDisputes - pastNumberDisputes;

const hasLiveActivity = presentCourtWithTree.numberDisputes > 0 || presentCourtWithTree.numberVotes > 0;
const hasSnapshotActivity = diffNumberDisputes > 0 || diffNumberVotes > 0;

if (!hasSnapshotActivity && hasLiveActivity) {
console.warn(`Snapshot shows no delta for court ${presentCourt.id}, using live`);
return presentCourtWithTree;
}

const avgEffectiveStake = (presentCourtWithTree.effectiveStake + pastEffectiveStake) / 2n;
const votesPerPnk = diffNumberVotes / (Number(avgEffectiveStake) / 1e18) || 0;
const disputesPerPnk = diffNumberDisputes / (Number(avgEffectiveStake) / 1e18) || 0;
const expectedRewardPerPnk = votesPerPnk * (Number(presentCourt.feeForJuror) / 1e18);

return {
...presentCourt,
numberDisputes: presentCourtWithTree.numberDisputes - pastCourtWithTree.numberDisputes,
treeNumberDisputes: presentCourtWithTree.treeNumberDisputes - pastCourtWithTree.treeNumberDisputes,
numberDisputes: diffNumberDisputes,
treeNumberDisputes: diffNumberDisputes,
numberVotes: diffNumberVotes,
treeNumberVotes: presentCourtWithTree.treeNumberVotes - pastCourtWithTree.treeNumberVotes,
treeNumberVotes: diffNumberVotes,
feeForJuror: presentCourtWithTree.feeForJuror,
effectiveStake: avgEffectiveStake,
votesPerPnk,
treeVotesPerPnk: votesPerPnk,
Expand All @@ -173,9 +189,21 @@ const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: Court): CourtWith
};
};

const getCourtMostDisputes = (courts: CourtWithTree[]) =>
courts.toSorted((a: CourtWithTree, b: CourtWithTree) => b.numberDisputes - a.numberDisputes)[0];
const getCourtBestDrawingChances = (courts: CourtWithTree[]) =>
courts.toSorted((a, b) => b.treeVotesPerPnk - a.treeVotesPerPnk)[0];
const getBestExpectedRewardCourt = (courts: CourtWithTree[]) =>
courts.toSorted((a, b) => b.treeExpectedRewardPerPnk - a.treeExpectedRewardPerPnk)[0];
export const useHomePageBlockQuery = (pastTimestamp: bigint | undefined, allTime: boolean) => {
const { graphqlBatcher } = useGraphqlBatcher();
const isEnabled = !isUndefined(pastTimestamp) || allTime;

return useQuery<HomePageBlockStats>({
queryKey: [`homePageBlockQuery${pastTimestamp?.toString()}-${allTime}`],
enabled: isEnabled,
staleTime: Infinity,
queryFn: async () => {
const data = await graphqlBatcher.fetch({
id: crypto.randomUUID(),
document: homePageBlockQuery,
variables: { pastTimestamp: allTime ? "0" : pastTimestamp?.toString() },
});
return processData(data, allTime);
},
});
};
Loading
Loading