Skip to content

Commit 0552fff

Browse files
committed
feat(subgraph/web): time travel query refactor
1 parent c76e4af commit 0552fff

File tree

6 files changed

+162
-74
lines changed

6 files changed

+162
-74
lines changed

subgraph/core/schema.graphql

Lines changed: 9 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 75 additions & 1 deletion
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 = timestamp;
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 = timestamp;
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

Lines changed: 2 additions & 1 deletion
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 {
Lines changed: 64 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
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) {
8+
query HomePageBlock($pastTimestamp: BigInt) {
139
presentCourts: courts(orderBy: id, orderDirection: asc) {
1410
id
1511
parent {
@@ -21,21 +17,19 @@ 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(where: { timestamp_lte: $pastTimestamp }, orderBy: timestamp, orderDirection: desc) {
21+
court {
2722
id
2823
}
29-
name
3024
numberDisputes
3125
numberVotes
32-
feeForJuror
3326
effectiveStake
3427
}
3528
}
3629
`);
3730

3831
type Court = HomePageBlockQuery["presentCourts"][number];
32+
type CourtCounter = HomePageBlockQuery["pastCourts"][number];
3933
type CourtWithTree = Court & {
4034
numberDisputes: number;
4135
numberVotes: number;
@@ -58,56 +52,52 @@ export type HomePageBlockStats = {
5852
courts: CourtWithTree[];
5953
};
6054

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-
};
55+
const getCourtMostDisputes = (courts: CourtWithTree[]) =>
56+
courts.toSorted((a: CourtWithTree, b: CourtWithTree) => b.numberDisputes - a.numberDisputes)[0];
57+
const getCourtBestDrawingChances = (courts: CourtWithTree[]) =>
58+
courts.toSorted((a, b) => b.treeVotesPerPnk - a.treeVotesPerPnk)[0];
59+
const getBestExpectedRewardCourt = (courts: CourtWithTree[]) =>
60+
courts.toSorted((a, b) => b.treeExpectedRewardPerPnk - a.treeExpectedRewardPerPnk)[0];
8261

8362
const processData = (data: HomePageBlockQuery, allTime: boolean) => {
8463
const presentCourts = data.presentCourts;
8564
const pastCourts = data.pastCourts;
65+
66+
const pastCourtsMap = new Map<string, CourtCounter>();
67+
if (!allTime) {
68+
for (const pastCourt of pastCourts) {
69+
const courtId = pastCourt.court.id;
70+
if (!pastCourtsMap.has(courtId)) {
71+
pastCourtsMap.set(courtId, pastCourt);
72+
}
73+
}
74+
}
75+
8676
const processedCourts: CourtWithTree[] = Array(presentCourts.length);
87-
const processed = new Set();
77+
const processed = new Set<number>();
8878

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

9282
processed.add(id);
93-
const court =
94-
!allTime && id < data.pastCourts.length
95-
? addTreeValuesWithDiff(presentCourts[id], pastCourts[id])
96-
: addTreeValues(presentCourts[id]);
83+
const court = presentCourts[id];
84+
const pastCourt = pastCourtsMap.get(court.id);
85+
const courtWithTree = !allTime && pastCourt ? addTreeValuesWithDiff(court, pastCourt) : addTreeValues(court);
9786
const parentIndex = court.parent ? Number(court.parent.id) - 1 : 0;
9887

9988
if (id === parentIndex) {
100-
processedCourts[id] = court;
101-
return court;
89+
processedCourts[id] = courtWithTree;
90+
return courtWithTree;
10291
}
10392

10493
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,
94+
...courtWithTree,
95+
treeNumberDisputes: courtWithTree.treeNumberDisputes + processCourt(parentIndex).treeNumberDisputes,
96+
treeNumberVotes: courtWithTree.treeNumberVotes + processCourt(parentIndex).treeNumberVotes,
97+
treeVotesPerPnk: courtWithTree.treeVotesPerPnk + processCourt(parentIndex).treeVotesPerPnk,
98+
treeDisputesPerPnk: courtWithTree.treeDisputesPerPnk + processCourt(parentIndex).treeDisputesPerPnk,
99+
treeExpectedRewardPerPnk:
100+
courtWithTree.treeExpectedRewardPerPnk + processCourt(parentIndex).treeExpectedRewardPerPnk,
111101
};
112102

113103
return processedCourts[id];
@@ -148,21 +138,25 @@ const addTreeValues = (court: Court): CourtWithTree => {
148138
};
149139
};
150140

151-
const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: Court): CourtWithTree => {
141+
const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: CourtCounter | undefined): CourtWithTree => {
152142
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;
143+
const pastNumberVotes = pastCourt ? Number(pastCourt.numberVotes) : 0;
144+
const pastNumberDisputes = pastCourt ? Number(pastCourt.numberDisputes) : 0;
145+
const pastEffectiveStake = pastCourt ? BigInt(pastCourt.effectiveStake) : BigInt(0);
146+
147+
const diffNumberVotes = presentCourtWithTree.numberVotes - pastNumberVotes;
148+
const diffNumberDisputes = presentCourtWithTree.numberDisputes - pastNumberDisputes;
149+
const avgEffectiveStake = (presentCourtWithTree.effectiveStake + pastEffectiveStake) / 2n;
157150
const votesPerPnk = diffNumberVotes / (Number(avgEffectiveStake) / 1e18) || 0;
158151
const disputesPerPnk = diffNumberDisputes / (Number(avgEffectiveStake) / 1e18) || 0;
159152
const expectedRewardPerPnk = votesPerPnk * (Number(presentCourt.feeForJuror) / 1e18);
160153
return {
161154
...presentCourt,
162-
numberDisputes: presentCourtWithTree.numberDisputes - pastCourtWithTree.numberDisputes,
163-
treeNumberDisputes: presentCourtWithTree.treeNumberDisputes - pastCourtWithTree.treeNumberDisputes,
155+
numberDisputes: diffNumberDisputes,
156+
treeNumberDisputes: diffNumberDisputes,
164157
numberVotes: diffNumberVotes,
165-
treeNumberVotes: presentCourtWithTree.treeNumberVotes - pastCourtWithTree.treeNumberVotes,
158+
treeNumberVotes: diffNumberVotes,
159+
feeForJuror: presentCourtWithTree.feeForJuror,
166160
effectiveStake: avgEffectiveStake,
167161
votesPerPnk,
168162
treeVotesPerPnk: votesPerPnk,
@@ -173,9 +167,21 @@ const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: Court): CourtWith
173167
};
174168
};
175169

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];
170+
export const useHomePageBlockQuery = (pastTimestamp: bigint | undefined, allTime: boolean) => {
171+
const { graphqlBatcher } = useGraphqlBatcher();
172+
const isEnabled = !isUndefined(pastTimestamp) || allTime;
173+
174+
return useQuery<HomePageBlockStats>({
175+
queryKey: [`homePageBlockQuery${pastTimestamp?.toString()}-${allTime}`],
176+
enabled: isEnabled,
177+
staleTime: Infinity,
178+
queryFn: async () => {
179+
const data = await graphqlBatcher.fetch({
180+
id: crypto.randomUUID(),
181+
document: homePageBlockQuery,
182+
variables: { pastTimestamp: allTime ? "0" : pastTimestamp?.toString() },
183+
});
184+
return processData(data, allTime);
185+
},
186+
});
187+
};
Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
import { useEffect, useState } from "react";
2-
32
import { UseQueryResult } from "@tanstack/react-query";
4-
import { useBlockNumber } from "wagmi";
5-
6-
import { averageBlockTimeInSeconds } from "consts/averageBlockTimeInSeconds";
7-
import { DEFAULT_CHAIN } from "consts/chains";
8-
93
import { useHomePageBlockQuery, HomePageBlockStats } from "./useHomePageBlockQuery";
104

115
type ReturnType = UseQueryResult<HomePageBlockStats, Error>;
126

137
export const useHomePageExtraStats = (days: number | string): ReturnType => {
14-
const [pastBlockNumber, setPastBlockNumber] = useState<number>();
15-
const currentBlockNumber = useBlockNumber({ chainId: DEFAULT_CHAIN });
8+
const [pastTimestamp, setPastTimestamp] = useState<bigint | undefined>();
169

1710
useEffect(() => {
18-
if (typeof days !== "string" && currentBlockNumber?.data) {
19-
const timeInBlocks = Math.floor((days * 24 * 3600) / averageBlockTimeInSeconds[DEFAULT_CHAIN]);
20-
setPastBlockNumber(Number(currentBlockNumber.data) - timeInBlocks);
11+
if (typeof days !== "string") {
12+
const currentTimestamp = BigInt(Math.floor(Date.now() / 1000)); // Current time in seconds
13+
const secondsInDays = BigInt(days * 24 * 3600);
14+
const pastTime = currentTimestamp - secondsInDays;
15+
setPastTimestamp(pastTime);
2116
}
22-
}, [currentBlockNumber, days]);
23-
24-
const data = useHomePageBlockQuery(pastBlockNumber, days === "allTime");
17+
}, [days]);
2518

19+
const data = useHomePageBlockQuery(pastTimestamp, days === "allTime");
2620
return data;
2721
};

0 commit comments

Comments
 (0)