From 0552fffdf9c1ec13e01568d5b7e5df0ebb281052 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Thu, 3 Apr 2025 20:36:17 +0200 Subject: [PATCH 1/4] feat(subgraph/web): time travel query refactor --- subgraph/core/schema.graphql | 9 ++ subgraph/core/src/KlerosCore.ts | 4 + subgraph/core/src/datapoint.ts | 76 ++++++++++- .../core/src/entities/JurorTokensPerCourt.ts | 3 +- .../hooks/queries/useHomePageBlockQuery.ts | 122 +++++++++--------- .../hooks/queries/useHomePageExtraStats.ts | 22 ++-- 6 files changed, 162 insertions(+), 74 deletions(-) diff --git a/subgraph/core/schema.graphql b/subgraph/core/schema.graphql index 956d102af..243d4bb9d 100644 --- a/subgraph/core/schema.graphql +++ b/subgraph/core/schema.graphql @@ -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! diff --git a/subgraph/core/src/KlerosCore.ts b/subgraph/core/src/KlerosCore.ts index 187b292ca..42a54644f 100644 --- a/subgraph/core/src/KlerosCore.ts +++ b/subgraph/core/src/KlerosCore.ts @@ -23,6 +23,7 @@ import { updateCasesAppealing, updateCasesRuled, updateCasesVoting, + updateCourtCumulativeMetric, updateTotalLeaderboardJurors, } from "./datapoint"; import { addUserActiveDispute, computeCoherenceScore, ensureUser } from "./entities/User"; @@ -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); @@ -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); diff --git a/subgraph/core/src/datapoint.ts b/subgraph/core/src/datapoint.ts index 7ddc149ce..c6ff476de 100644 --- a/subgraph/core/src/datapoint.ts +++ b/subgraph/core/src/datapoint.ts @@ -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 { @@ -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(); + } +} diff --git a/subgraph/core/src/entities/JurorTokensPerCourt.ts b/subgraph/core/src/entities/JurorTokensPerCourt.ts index 8f5e31a7c..98fa8cb24 100644 --- a/subgraph/core/src/entities/JurorTokensPerCourt.ts +++ b/subgraph/core/src/entities/JurorTokensPerCourt.ts @@ -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"; @@ -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 { diff --git a/web/src/hooks/queries/useHomePageBlockQuery.ts b/web/src/hooks/queries/useHomePageBlockQuery.ts index 6bf142549..14aabfd2e 100644 --- a/web/src/hooks/queries/useHomePageBlockQuery.ts +++ b/web/src/hooks/queries/useHomePageBlockQuery.ts @@ -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 { @@ -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; @@ -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({ - 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(); + 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(); 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]; @@ -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, @@ -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({ + 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); + }, + }); +}; diff --git a/web/src/hooks/queries/useHomePageExtraStats.ts b/web/src/hooks/queries/useHomePageExtraStats.ts index cd2a0b47a..8d3ccb2f0 100644 --- a/web/src/hooks/queries/useHomePageExtraStats.ts +++ b/web/src/hooks/queries/useHomePageExtraStats.ts @@ -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; export const useHomePageExtraStats = (days: number | string): ReturnType => { - const [pastBlockNumber, setPastBlockNumber] = useState(); - const currentBlockNumber = useBlockNumber({ chainId: DEFAULT_CHAIN }); + const [pastTimestamp, setPastTimestamp] = useState(); 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; }; From d5788be92435bc704536719bd3606469f4218b15 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 4 Apr 2025 02:02:20 +0200 Subject: [PATCH 2/4] chore(subgraph): update package json version --- subgraph/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subgraph/package.json b/subgraph/package.json index c44d470c2..9741db307 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-subgraph", - "version": "0.13.0", + "version": "0.14.0", "drtVersion": "0.11.0", "license": "MIT", "scripts": { From 8bdf724b091e656602c90d34c97f5531725258a2 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:02:08 +0200 Subject: [PATCH 3/4] chore: bump core version --- subgraph/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subgraph/package.json b/subgraph/package.json index 5cf8bbe21..d38bdffab 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-subgraph", - "version": "0.14.3", + "version": "0.15.0", "drtVersion": "0.12.0", "license": "MIT", "scripts": { From 0e4770183716a96a190a8d84b6957ac68636130b Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 11 Apr 2025 18:59:57 +0200 Subject: [PATCH 4/4] fix: order and timestamp bug --- subgraph/core/src/datapoint.ts | 4 +- .../hooks/queries/useHomePageBlockQuery.ts | 76 ++++++++++++------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/subgraph/core/src/datapoint.ts b/subgraph/core/src/datapoint.ts index c6ff476de..6eeb07312 100644 --- a/subgraph/core/src/datapoint.ts +++ b/subgraph/core/src/datapoint.ts @@ -102,7 +102,7 @@ export function updateCourtCumulativeMetric(courtId: string, delta: BigInt, time currentCounter.numberDisputes = ZERO; currentCounter.numberVotes = ZERO; currentCounter.effectiveStake = ZERO; - currentCounter.timestamp = timestamp; + currentCounter.timestamp = ZERO; } if (metric === "numberDisputes") { currentCounter.numberDisputes = currentCounter.numberDisputes.plus(delta); @@ -140,7 +140,7 @@ export function updateCourtStateVariable(courtId: string, newValue: BigInt, time currentCounter.numberDisputes = ZERO; currentCounter.numberVotes = ZERO; currentCounter.effectiveStake = newValue; - currentCounter.timestamp = timestamp; + currentCounter.timestamp = ZERO; } else { if (variable === "effectiveStake") { currentCounter.effectiveStake = newValue; diff --git a/web/src/hooks/queries/useHomePageBlockQuery.ts b/web/src/hooks/queries/useHomePageBlockQuery.ts index 14aabfd2e..aa8cfb075 100644 --- a/web/src/hooks/queries/useHomePageBlockQuery.ts +++ b/web/src/hooks/queries/useHomePageBlockQuery.ts @@ -6,7 +6,7 @@ import { HomePageBlockQuery } from "src/graphql/graphql"; const homePageBlockQuery = graphql(` query HomePageBlock($pastTimestamp: BigInt) { - presentCourts: courts(orderBy: id, orderDirection: asc) { + presentCourts: courts(orderBy: id, orderDirection: asc, first: 1000) { id parent { id @@ -17,13 +17,19 @@ const homePageBlockQuery = graphql(` feeForJuror effectiveStake } - pastCourts: courtCounters(where: { timestamp_lte: $pastTimestamp }, orderBy: timestamp, orderDirection: desc) { + pastCourts: courtCounters( + where: { timestamp_lte: $pastTimestamp } + orderBy: timestamp + orderDirection: desc + first: 1000 + ) { court { id } numberDisputes numberVotes effectiveStake + timestamp } } `); @@ -53,16 +59,17 @@ export type HomePageBlockStats = { }; const getCourtMostDisputes = (courts: CourtWithTree[]) => - courts.toSorted((a: CourtWithTree, b: CourtWithTree) => b.numberDisputes - a.numberDisputes)[0]; + 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 presentCourtsMap = new Map(presentCourts.map((c) => [c.id, c])); const pastCourtsMap = new Map(); if (!allTime) { for (const pastCourt of pastCourts) { @@ -73,41 +80,40 @@ const processData = (data: HomePageBlockQuery, allTime: boolean) => { } } - const processedCourts: CourtWithTree[] = Array(presentCourts.length); - const processed = new Set(); + const processedCourtsMap = new Map(); + const processCourt = (courtId: string): CourtWithTree => { + if (processedCourtsMap.has(courtId)) return processedCourtsMap.get(courtId)!; - const processCourt = (id: number): CourtWithTree => { - if (processed.has(id)) return processedCourts[id]; + const court = presentCourtsMap.get(courtId)!; + const pastCourt = pastCourtsMap.get(courtId); - processed.add(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] = courtWithTree; + const parentId = court.parent?.id; + if (!parentId || courtId === parentId) { + processedCourtsMap.set(courtId, courtWithTree); return courtWithTree; } - processedCourts[id] = { + const parentCourt = processCourt(parentId); + const fullTreeCourt: CourtWithTree = { ...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, + 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), @@ -140,16 +146,32 @@ const addTreeValues = (court: Court): CourtWithTree => { const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: CourtCounter | undefined): CourtWithTree => { const presentCourtWithTree = addTreeValues(presentCourt); - const pastNumberVotes = pastCourt ? Number(pastCourt.numberVotes) : 0; - const pastNumberDisputes = pastCourt ? Number(pastCourt.numberDisputes) : 0; - const pastEffectiveStake = pastCourt ? BigInt(pastCourt.effectiveStake) : BigInt(0); + + 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: diffNumberDisputes,