Skip to content

Commit 74142c1

Browse files
authored
Merge pull request #1939 from kleros/fix/time-travel-query-refactor
feat(subgraph/web): time travel query refactor
2 parents 7ce00c3 + 0e47701 commit 74142c1

File tree

7 files changed

+200
-90
lines changed

7 files changed

+200
-90
lines changed

subgraph/core/schema.graphql

+9
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,15 @@ type Counter @entity {
242242
totalLeaderboardJurors: BigInt!
243243
}
244244

245+
type CourtCounter @entity {
246+
id: ID! # court.id-timestamp
247+
court: Court!
248+
numberDisputes: BigInt!
249+
numberVotes: BigInt!
250+
effectiveStake: BigInt!
251+
timestamp: BigInt!
252+
}
253+
245254
type FeeToken @entity {
246255
id: ID! # The address of the ERC20 token.
247256
accepted: Boolean!

subgraph/core/src/KlerosCore.ts

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
updateCasesAppealing,
2424
updateCasesRuled,
2525
updateCasesVoting,
26+
updateCourtCumulativeMetric,
2627
updateTotalLeaderboardJurors,
2728
} from "./datapoint";
2829
import { addUserActiveDispute, computeCoherenceScore, ensureUser } from "./entities/User";
@@ -81,9 +82,11 @@ export function handleDisputeCreation(event: DisputeCreation): void {
8182
const court = Court.load(courtID);
8283
if (!court) return;
8384
court.numberDisputes = court.numberDisputes.plus(ONE);
85+
updateCourtCumulativeMetric(courtID, ONE, event.block.timestamp, "numberDisputes");
8486

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

8891
court.save();
8992
createDisputeFromEvent(event);
@@ -225,6 +228,7 @@ export function handleAppealDecision(event: AppealDecision): void {
225228
if (!court) return;
226229

227230
court.numberVotes = court.numberVotes.plus(roundInfo.nbVotes);
231+
updateCourtCumulativeMetric(courtID, roundInfo.nbVotes, event.block.timestamp, "numberVotes");
228232
court.save();
229233

230234
createRoundFromRoundInfo(KlerosCore.bind(event.address), disputeID, newRoundIndex, roundInfo);

subgraph/core/src/datapoint.ts

+75-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BigInt, Entity, Value, store } from "@graphprotocol/graph-ts";
2-
import { Counter } from "../generated/schema";
2+
import { Counter, CourtCounter } from "../generated/schema";
33
import { ZERO } from "./utils";
44

55
export function getDelta(previousValue: BigInt, newValue: BigInt): BigInt {
@@ -92,3 +92,77 @@ export function updateCasesAppealing(delta: BigInt, timestamp: BigInt): void {
9292
export function updateTotalLeaderboardJurors(delta: BigInt, timestamp: BigInt): void {
9393
updateDataPoint(delta, timestamp, "totalLeaderboardJurors");
9494
}
95+
96+
export function updateCourtCumulativeMetric(courtId: string, delta: BigInt, timestamp: BigInt, metric: string): void {
97+
// Load or create the current CourtCounter (ID: courtId-0)
98+
let currentCounter = CourtCounter.load(courtId + "-0");
99+
if (!currentCounter) {
100+
currentCounter = new CourtCounter(courtId + "-0");
101+
currentCounter.court = courtId;
102+
currentCounter.numberDisputes = ZERO;
103+
currentCounter.numberVotes = ZERO;
104+
currentCounter.effectiveStake = ZERO;
105+
currentCounter.timestamp = ZERO;
106+
}
107+
if (metric === "numberDisputes") {
108+
currentCounter.numberDisputes = currentCounter.numberDisputes.plus(delta);
109+
} else if (metric === "numberVotes") {
110+
currentCounter.numberVotes = currentCounter.numberVotes.plus(delta);
111+
}
112+
currentCounter.save();
113+
114+
// Update daily snapshot
115+
let dayID = timestamp.toI32() / 86400; // Seconds to days
116+
let dayStartTimestamp = dayID * 86400;
117+
let dailyCounter = CourtCounter.load(courtId + "-" + dayStartTimestamp.toString());
118+
if (!dailyCounter) {
119+
dailyCounter = new CourtCounter(courtId + "-" + dayStartTimestamp.toString());
120+
dailyCounter.court = courtId;
121+
dailyCounter.numberDisputes = currentCounter.numberDisputes.minus(delta); // State before this update
122+
dailyCounter.numberVotes = currentCounter.numberVotes.minus(delta);
123+
dailyCounter.effectiveStake = currentCounter.effectiveStake;
124+
dailyCounter.timestamp = BigInt.fromI32(dayStartTimestamp);
125+
}
126+
if (metric === "numberDisputes") {
127+
dailyCounter.numberDisputes = dailyCounter.numberDisputes.plus(delta);
128+
} else if (metric === "numberVotes") {
129+
dailyCounter.numberVotes = dailyCounter.numberVotes.plus(delta);
130+
}
131+
dailyCounter.save();
132+
}
133+
134+
export function updateCourtStateVariable(courtId: string, newValue: BigInt, timestamp: BigInt, variable: string): void {
135+
// Load or create the current CourtCounter (ID: courtId-0)
136+
let currentCounter = CourtCounter.load(courtId + "-0");
137+
if (!currentCounter) {
138+
currentCounter = new CourtCounter(courtId + "-0");
139+
currentCounter.court = courtId;
140+
currentCounter.numberDisputes = ZERO;
141+
currentCounter.numberVotes = ZERO;
142+
currentCounter.effectiveStake = newValue;
143+
currentCounter.timestamp = ZERO;
144+
} else {
145+
if (variable === "effectiveStake") {
146+
currentCounter.effectiveStake = newValue;
147+
}
148+
currentCounter.save();
149+
}
150+
151+
// Update daily snapshot
152+
let dayID = timestamp.toI32() / 86400;
153+
let dayStartTimestamp = dayID * 86400;
154+
let dailyCounter = CourtCounter.load(courtId + "-" + dayStartTimestamp.toString());
155+
if (!dailyCounter) {
156+
dailyCounter = new CourtCounter(courtId + "-" + dayStartTimestamp.toString());
157+
dailyCounter.court = courtId;
158+
dailyCounter.numberDisputes = currentCounter.numberDisputes;
159+
dailyCounter.numberVotes = currentCounter.numberVotes;
160+
dailyCounter.effectiveStake = newValue;
161+
dailyCounter.timestamp = BigInt.fromI32(dayStartTimestamp);
162+
} else {
163+
if (variable === "effectiveStake") {
164+
dailyCounter.effectiveStake = newValue;
165+
}
166+
dailyCounter.save();
167+
}
168+
}

subgraph/core/src/entities/JurorTokensPerCourt.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BigInt, Address } from "@graphprotocol/graph-ts";
22
import { Court, JurorTokensPerCourt } from "../../generated/schema";
3-
import { updateActiveJurors, getDelta, updateStakedPNK } from "../datapoint";
3+
import { updateActiveJurors, getDelta, updateStakedPNK, updateCourtStateVariable } from "../datapoint";
44
import { ensureUser } from "./User";
55
import { ONE, ZERO } from "../utils";
66
import { SortitionModule } from "../../generated/SortitionModule/SortitionModule";
@@ -94,6 +94,7 @@ export function updateJurorStake(
9494
court.save();
9595
updateEffectiveStake(courtID);
9696
updateJurorEffectiveStake(jurorAddress, courtID);
97+
updateCourtStateVariable(courtID, court.effectiveStake, timestamp, "effectiveStake");
9798
}
9899

99100
export function updateJurorDelayedStake(jurorAddress: string, courtID: string, amount: BigInt): void {

subgraph/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kleros/kleros-v2-subgraph",
3-
"version": "0.14.2",
3+
"version": "0.15.0",
44
"drtVersion": "0.12.0",
55
"license": "MIT",
66
"scripts": {
+101-73
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import { useQuery } from "@tanstack/react-query";
2-
32
import { useGraphqlBatcher } from "context/GraphqlBatcher";
43
import { isUndefined } from "utils/index";
5-
64
import { graphql } from "src/graphql";
75
import { HomePageBlockQuery } from "src/graphql/graphql";
8-
import useGenesisBlock from "../useGenesisBlock";
9-
export type { HomePageBlockQuery };
106

117
const homePageBlockQuery = graphql(`
12-
query HomePageBlock($blockNumber: Int) {
13-
presentCourts: courts(orderBy: id, orderDirection: asc) {
8+
query HomePageBlock($pastTimestamp: BigInt) {
9+
presentCourts: courts(orderBy: id, orderDirection: asc, first: 1000) {
1410
id
1511
parent {
1612
id
@@ -21,21 +17,25 @@ const homePageBlockQuery = graphql(`
2117
feeForJuror
2218
effectiveStake
2319
}
24-
pastCourts: courts(orderBy: id, orderDirection: asc, block: { number: $blockNumber }) {
25-
id
26-
parent {
20+
pastCourts: courtCounters(
21+
where: { timestamp_lte: $pastTimestamp }
22+
orderBy: timestamp
23+
orderDirection: desc
24+
first: 1000
25+
) {
26+
court {
2727
id
2828
}
29-
name
3029
numberDisputes
3130
numberVotes
32-
feeForJuror
3331
effectiveStake
32+
timestamp
3433
}
3534
}
3635
`);
3736

3837
type Court = HomePageBlockQuery["presentCourts"][number];
38+
type CourtCounter = HomePageBlockQuery["pastCourts"][number];
3939
type CourtWithTree = Court & {
4040
numberDisputes: number;
4141
numberVotes: number;
@@ -58,66 +58,62 @@ export type HomePageBlockStats = {
5858
courts: CourtWithTree[];
5959
};
6060

61-
export const useHomePageBlockQuery = (blockNumber: number | undefined, allTime: boolean) => {
62-
const genesisBlock = useGenesisBlock();
63-
const isEnabled = !isUndefined(blockNumber) || allTime || !isUndefined(genesisBlock);
64-
const { graphqlBatcher } = useGraphqlBatcher();
65-
66-
return useQuery<HomePageBlockStats>({
67-
queryKey: [`homePageBlockQuery${blockNumber}-${allTime}`],
68-
enabled: isEnabled,
69-
staleTime: Infinity,
70-
queryFn: async () => {
71-
const targetBlock = Math.max(blockNumber!, genesisBlock!);
72-
const data = await graphqlBatcher.fetch({
73-
id: crypto.randomUUID(),
74-
document: homePageBlockQuery,
75-
variables: { blockNumber: targetBlock },
76-
});
77-
78-
return processData(data, allTime);
79-
},
80-
});
81-
};
61+
const getCourtMostDisputes = (courts: CourtWithTree[]) =>
62+
courts.toSorted((a, b) => b.numberDisputes - a.numberDisputes)[0];
63+
const getCourtBestDrawingChances = (courts: CourtWithTree[]) =>
64+
courts.toSorted((a, b) => b.treeVotesPerPnk - a.treeVotesPerPnk)[0];
65+
const getBestExpectedRewardCourt = (courts: CourtWithTree[]) =>
66+
courts.toSorted((a, b) => b.treeExpectedRewardPerPnk - a.treeExpectedRewardPerPnk)[0];
8267

8368
const processData = (data: HomePageBlockQuery, allTime: boolean) => {
84-
const presentCourts = data.presentCourts;
69+
const presentCourts = [...data.presentCourts].sort((a, b) => Number(a.id) - Number(b.id));
8570
const pastCourts = data.pastCourts;
86-
const processedCourts: CourtWithTree[] = Array(presentCourts.length);
87-
const processed = new Set();
88-
89-
const processCourt = (id: number): CourtWithTree => {
90-
if (processed.has(id)) return processedCourts[id];
91-
92-
processed.add(id);
93-
const court =
94-
!allTime && id < data.pastCourts.length
95-
? addTreeValuesWithDiff(presentCourts[id], pastCourts[id])
96-
: addTreeValues(presentCourts[id]);
97-
const parentIndex = court.parent ? Number(court.parent.id) - 1 : 0;
98-
99-
if (id === parentIndex) {
100-
processedCourts[id] = court;
101-
return court;
71+
72+
const presentCourtsMap = new Map(presentCourts.map((c) => [c.id, c]));
73+
const pastCourtsMap = new Map<string, CourtCounter>();
74+
if (!allTime) {
75+
for (const pastCourt of pastCourts) {
76+
const courtId = pastCourt.court.id;
77+
if (!pastCourtsMap.has(courtId)) {
78+
pastCourtsMap.set(courtId, pastCourt);
79+
}
80+
}
81+
}
82+
83+
const processedCourtsMap = new Map<string, CourtWithTree>();
84+
const processCourt = (courtId: string): CourtWithTree => {
85+
if (processedCourtsMap.has(courtId)) return processedCourtsMap.get(courtId)!;
86+
87+
const court = presentCourtsMap.get(courtId)!;
88+
const pastCourt = pastCourtsMap.get(courtId);
89+
90+
const courtWithTree = !allTime && pastCourt ? addTreeValuesWithDiff(court, pastCourt) : addTreeValues(court);
91+
92+
const parentId = court.parent?.id;
93+
if (!parentId || courtId === parentId) {
94+
processedCourtsMap.set(courtId, courtWithTree);
95+
return courtWithTree;
10296
}
10397

104-
processedCourts[id] = {
105-
...court,
106-
treeNumberDisputes: court.treeNumberDisputes + processCourt(parentIndex).treeNumberDisputes,
107-
treeNumberVotes: court.treeNumberVotes + processCourt(parentIndex).treeNumberVotes,
108-
treeVotesPerPnk: court.treeVotesPerPnk + processCourt(parentIndex).treeVotesPerPnk,
109-
treeDisputesPerPnk: court.treeDisputesPerPnk + processCourt(parentIndex).treeDisputesPerPnk,
110-
treeExpectedRewardPerPnk: court.treeExpectedRewardPerPnk + processCourt(parentIndex).treeExpectedRewardPerPnk,
98+
const parentCourt = processCourt(parentId);
99+
const fullTreeCourt: CourtWithTree = {
100+
...courtWithTree,
101+
treeNumberDisputes: courtWithTree.treeNumberDisputes + parentCourt.treeNumberDisputes,
102+
treeNumberVotes: courtWithTree.treeNumberVotes + parentCourt.treeNumberVotes,
103+
treeVotesPerPnk: courtWithTree.treeVotesPerPnk + parentCourt.treeVotesPerPnk,
104+
treeDisputesPerPnk: courtWithTree.treeDisputesPerPnk + parentCourt.treeDisputesPerPnk,
105+
treeExpectedRewardPerPnk: courtWithTree.treeExpectedRewardPerPnk + parentCourt.treeExpectedRewardPerPnk,
111106
};
112107

113-
return processedCourts[id];
108+
processedCourtsMap.set(courtId, fullTreeCourt);
109+
return fullTreeCourt;
114110
};
115111

116112
for (const court of presentCourts.toReversed()) {
117-
processCourt(Number(court.id) - 1);
113+
processCourt(court.id);
118114
}
119115

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

122118
return {
123119
mostDisputedCourt: getCourtMostDisputes(processedCourts),
@@ -148,21 +144,41 @@ const addTreeValues = (court: Court): CourtWithTree => {
148144
};
149145
};
150146

151-
const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: Court): CourtWithTree => {
147+
const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: CourtCounter | undefined): CourtWithTree => {
152148
const presentCourtWithTree = addTreeValues(presentCourt);
153-
const pastCourtWithTree = addTreeValues(pastCourt);
154-
const diffNumberVotes = presentCourtWithTree.numberVotes - pastCourtWithTree.numberVotes;
155-
const diffNumberDisputes = presentCourtWithTree.numberDisputes - pastCourtWithTree.numberDisputes;
156-
const avgEffectiveStake = (presentCourtWithTree.effectiveStake + pastCourtWithTree.effectiveStake) / 2n;
149+
150+
if (!pastCourt) {
151+
console.warn(`Missing snapshot for court ${presentCourt.id}, falling back to live`);
152+
return presentCourtWithTree;
153+
}
154+
155+
const pastNumberVotes = Number(pastCourt.numberVotes);
156+
const pastNumberDisputes = Number(pastCourt.numberDisputes);
157+
const pastEffectiveStake = BigInt(pastCourt.effectiveStake);
158+
159+
const diffNumberVotes = presentCourtWithTree.numberVotes - pastNumberVotes;
160+
const diffNumberDisputes = presentCourtWithTree.numberDisputes - pastNumberDisputes;
161+
162+
const hasLiveActivity = presentCourtWithTree.numberDisputes > 0 || presentCourtWithTree.numberVotes > 0;
163+
const hasSnapshotActivity = diffNumberDisputes > 0 || diffNumberVotes > 0;
164+
165+
if (!hasSnapshotActivity && hasLiveActivity) {
166+
console.warn(`Snapshot shows no delta for court ${presentCourt.id}, using live`);
167+
return presentCourtWithTree;
168+
}
169+
170+
const avgEffectiveStake = (presentCourtWithTree.effectiveStake + pastEffectiveStake) / 2n;
157171
const votesPerPnk = diffNumberVotes / (Number(avgEffectiveStake) / 1e18) || 0;
158172
const disputesPerPnk = diffNumberDisputes / (Number(avgEffectiveStake) / 1e18) || 0;
159173
const expectedRewardPerPnk = votesPerPnk * (Number(presentCourt.feeForJuror) / 1e18);
174+
160175
return {
161176
...presentCourt,
162-
numberDisputes: presentCourtWithTree.numberDisputes - pastCourtWithTree.numberDisputes,
163-
treeNumberDisputes: presentCourtWithTree.treeNumberDisputes - pastCourtWithTree.treeNumberDisputes,
177+
numberDisputes: diffNumberDisputes,
178+
treeNumberDisputes: diffNumberDisputes,
164179
numberVotes: diffNumberVotes,
165-
treeNumberVotes: presentCourtWithTree.treeNumberVotes - pastCourtWithTree.treeNumberVotes,
180+
treeNumberVotes: diffNumberVotes,
181+
feeForJuror: presentCourtWithTree.feeForJuror,
166182
effectiveStake: avgEffectiveStake,
167183
votesPerPnk,
168184
treeVotesPerPnk: votesPerPnk,
@@ -173,9 +189,21 @@ const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: Court): CourtWith
173189
};
174190
};
175191

176-
const getCourtMostDisputes = (courts: CourtWithTree[]) =>
177-
courts.toSorted((a: CourtWithTree, b: CourtWithTree) => b.numberDisputes - a.numberDisputes)[0];
178-
const getCourtBestDrawingChances = (courts: CourtWithTree[]) =>
179-
courts.toSorted((a, b) => b.treeVotesPerPnk - a.treeVotesPerPnk)[0];
180-
const getBestExpectedRewardCourt = (courts: CourtWithTree[]) =>
181-
courts.toSorted((a, b) => b.treeExpectedRewardPerPnk - a.treeExpectedRewardPerPnk)[0];
192+
export const useHomePageBlockQuery = (pastTimestamp: bigint | undefined, allTime: boolean) => {
193+
const { graphqlBatcher } = useGraphqlBatcher();
194+
const isEnabled = !isUndefined(pastTimestamp) || allTime;
195+
196+
return useQuery<HomePageBlockStats>({
197+
queryKey: [`homePageBlockQuery${pastTimestamp?.toString()}-${allTime}`],
198+
enabled: isEnabled,
199+
staleTime: Infinity,
200+
queryFn: async () => {
201+
const data = await graphqlBatcher.fetch({
202+
id: crypto.randomUUID(),
203+
document: homePageBlockQuery,
204+
variables: { pastTimestamp: allTime ? "0" : pastTimestamp?.toString() },
205+
});
206+
return processData(data, allTime);
207+
},
208+
});
209+
};

0 commit comments

Comments
 (0)