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 1 commit
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 = timestamp;
}
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 = timestamp;
} 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
122 changes: 64 additions & 58 deletions web/src/hooks/queries/useHomePageBlockQuery.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
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) {
query HomePageBlock($pastTimestamp: BigInt) {
presentCourts: courts(orderBy: id, orderDirection: asc) {
id
parent {
Expand All @@ -21,21 +17,19 @@ 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) {
court {
id
}
name
numberDisputes
numberVotes
feeForJuror
effectiveStake
}
}
`);

type Court = HomePageBlockQuery["presentCourts"][number];
type CourtCounter = HomePageBlockQuery["pastCourts"][number];
type CourtWithTree = Court & {
numberDisputes: number;
numberVotes: number;
Expand All @@ -58,56 +52,52 @@ 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: 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];

const processData = (data: HomePageBlockQuery, allTime: boolean) => {
const presentCourts = data.presentCourts;
const pastCourts = data.pastCourts;

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 processedCourts: CourtWithTree[] = Array(presentCourts.length);
const processed = new Set();
const processed = new Set<number>();

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 court = presentCourts[id];
const pastCourt = pastCourtsMap.get(court.id);
const courtWithTree = !allTime && pastCourt ? addTreeValuesWithDiff(court, pastCourt) : addTreeValues(court);
const parentIndex = court.parent ? Number(court.parent.id) - 1 : 0;

if (id === parentIndex) {
processedCourts[id] = court;
return court;
processedCourts[id] = 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,
...courtWithTree,
treeNumberDisputes: courtWithTree.treeNumberDisputes + processCourt(parentIndex).treeNumberDisputes,
treeNumberVotes: courtWithTree.treeNumberVotes + processCourt(parentIndex).treeNumberVotes,
treeVotesPerPnk: courtWithTree.treeVotesPerPnk + processCourt(parentIndex).treeVotesPerPnk,
treeDisputesPerPnk: courtWithTree.treeDisputesPerPnk + processCourt(parentIndex).treeDisputesPerPnk,
treeExpectedRewardPerPnk:
courtWithTree.treeExpectedRewardPerPnk + processCourt(parentIndex).treeExpectedRewardPerPnk,
};

return processedCourts[id];
Expand Down Expand Up @@ -148,21 +138,25 @@ 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;
const pastNumberVotes = pastCourt ? Number(pastCourt.numberVotes) : 0;
const pastNumberDisputes = pastCourt ? Number(pastCourt.numberDisputes) : 0;
const pastEffectiveStake = pastCourt ? BigInt(pastCourt.effectiveStake) : BigInt(0);

const diffNumberVotes = presentCourtWithTree.numberVotes - pastNumberVotes;
const diffNumberDisputes = presentCourtWithTree.numberDisputes - pastNumberDisputes;
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 +167,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);
},
});
};
22 changes: 8 additions & 14 deletions web/src/hooks/queries/useHomePageExtraStats.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import { useEffect, useState } from "react";

import { UseQueryResult } from "@tanstack/react-query";
import { useBlockNumber } from "wagmi";

import { averageBlockTimeInSeconds } from "consts/averageBlockTimeInSeconds";
import { DEFAULT_CHAIN } from "consts/chains";

import { useHomePageBlockQuery, HomePageBlockStats } from "./useHomePageBlockQuery";

type ReturnType = UseQueryResult<HomePageBlockStats, Error>;

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

useEffect(() => {
if (typeof days !== "string" && currentBlockNumber?.data) {
const timeInBlocks = Math.floor((days * 24 * 3600) / averageBlockTimeInSeconds[DEFAULT_CHAIN]);
setPastBlockNumber(Number(currentBlockNumber.data) - timeInBlocks);
if (typeof days !== "string") {
const currentTimestamp = BigInt(Math.floor(Date.now() / 1000)); // Current time in seconds
const secondsInDays = BigInt(days * 24 * 3600);
const pastTime = currentTimestamp - secondsInDays;
setPastTimestamp(pastTime);
}
}, [currentBlockNumber, days]);

const data = useHomePageBlockQuery(pastBlockNumber, days === "allTime");
}, [days]);

const data = useHomePageBlockQuery(pastTimestamp, days === "allTime");
return data;
};
Loading