Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7b615f4
feat(web): extra statistic on homepage
nikhilverma360 Aug 1, 2024
0bc4fd4
fix(web): fix HomePageExtraStatsTypes
nikhilverma360 Aug 7, 2024
8e5b49d
Merge branch 'dev' into feat(web)/Extra-statistics-on-the-Home-page
kemuru Sep 13, 2024
0d0a60e
feat: styling according to figma
kemuru Sep 17, 2024
154ea0c
fix: slight styling, and most cases fetching optimiz, delete ineffici…
kemuru Sep 17, 2024
9da8250
fix: one week in seconds
kemuru Sep 18, 2024
80912c4
chore: better naming for clarity
kemuru Sep 18, 2024
a774c26
feat: courtesy of green, activity stats, most drawing chance, most re…
kemuru Sep 19, 2024
20a27f8
fix: random style fix for margin bottom in case cards title skeletons
kemuru Sep 19, 2024
09ef75d
feat: change number of disputes for number of votes, subgraph changes…
kemuru Sep 19, 2024
e614e6c
fix: add missing number votes fields
kemuru Sep 19, 2024
7a648af
feat: add selector according to figma of past blocks and times
kemuru Sep 20, 2024
0dfb08a
feat: add support for all time filtering, add skeletons for styling,
kemuru Sep 20, 2024
be37823
chore: add staletime to the query, comment older days
kemuru Sep 20, 2024
0448987
fix: few code smells
kemuru Sep 20, 2024
56af5a7
chore: add subgraph endpoint back
kemuru Sep 25, 2024
0ec4f0f
chore: bump subgraph package json version
kemuru Sep 25, 2024
a1361c1
feat: add effectivestake to the subgraph, modify hook
kemuru Sep 26, 2024
48d7869
chore: add my subgraph endpoint for local testing
kemuru Sep 26, 2024
db1b451
chore: changed tosorted to prevent array mutations
kemuru Sep 26, 2024
c2d85fa
fix: always iterate through presentcourts and check if pastcourt exists
kemuru Sep 27, 2024
9ddd81a
fix: remove one unnecessary loop and move the variables to the return…
kemuru Sep 27, 2024
0075331
chore: update subgraph version
kemuru Sep 27, 2024
781c226
chore(web): add config to use sorting without mutation on arrays
alcercu Oct 2, 2024
e85461b
refactor(web): extra stats block query algorithm improvement
alcercu Oct 2, 2024
82d8e7a
chore: readd correct subgraph endpoint
kemuru Oct 4, 2024
94c4074
Merge branch 'dev' into feat(web)/Extra-statistics-on-the-Home-page
kemuru Oct 4, 2024
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions subgraph/core/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,11 @@ type Court @entity {
numberClosedDisputes: BigInt!
numberVotingDisputes: BigInt!
numberAppealingDisputes: BigInt!
numberVotes: BigInt!
stakedJurors: [JurorTokensPerCourt!]! @derivedFrom(field: "court")
numberStakedJurors: BigInt!
stake: BigInt!
effectiveStake: BigInt!
delayedStake: BigInt!
paidETH: BigInt!
paidPNK: BigInt!
Expand Down
14 changes: 13 additions & 1 deletion subgraph/core/src/KlerosCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,12 @@ export function handleDisputeCreation(event: DisputeCreation): void {
const court = Court.load(courtID);
if (!court) return;
court.numberDisputes = court.numberDisputes.plus(ONE);

const roundInfo = contract.getRoundInfo(disputeID, ZERO);
court.numberVotes = court.numberVotes.plus(roundInfo.nbVotes);

court.save();
createDisputeFromEvent(event);
const roundInfo = contract.getRoundInfo(disputeID, ZERO);
createRoundFromRoundInfo(KlerosCore.bind(event.address), disputeID, ZERO, roundInfo);
const arbitrable = event.params._arbitrable.toHexString();
updateArbitrableCases(arbitrable, ONE);
Expand Down Expand Up @@ -164,6 +167,15 @@ export function handleAppealDecision(event: AppealDecision): void {
dispute.currentRound = roundID;
dispute.save();
const roundInfo = contract.getRoundInfo(disputeID, newRoundIndex);

const disputeStorage = contract.disputes(disputeID);
const courtID = disputeStorage.value0.toString();
const court = Court.load(courtID);
if (!court) return;

court.numberVotes = court.numberVotes.plus(roundInfo.nbVotes);
court.save();

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

Expand Down
31 changes: 31 additions & 0 deletions subgraph/core/src/entities/Court.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,35 @@ import { CourtCreated } from "../../generated/KlerosCore/KlerosCore";
import { Court } from "../../generated/schema";
import { ZERO } from "../utils";

// This function calculates the "effective" stake, which is the specific stake
// of the current court + the specific stake of all of its children courts
export function updateEffectiveStake(courtID: string): void {
let court = Court.load(courtID);
if (!court) return;

while (court) {
let totalStake = court.stake;

const childrenCourts = court.children.load();

for (let i = 0; i < childrenCourts.length; i++) {
const childCourt = Court.load(childrenCourts[i].id);
if (childCourt) {
totalStake = totalStake.plus(childCourt.effectiveStake);
}
}

court.effectiveStake = totalStake;
court.save();

if (court.parent && court.parent !== null) {
court = Court.load(court.parent as string);
} else {
break;
}
}
}

export function createCourtFromEvent(event: CourtCreated): void {
const court = new Court(event.params._courtID.toString());
court.hiddenVotes = event.params._hiddenVotes;
Expand All @@ -17,8 +46,10 @@ export function createCourtFromEvent(event: CourtCreated): void {
court.numberClosedDisputes = ZERO;
court.numberVotingDisputes = ZERO;
court.numberAppealingDisputes = ZERO;
court.numberVotes = ZERO;
court.numberStakedJurors = ZERO;
court.stake = ZERO;
court.effectiveStake = ZERO;
court.delayedStake = ZERO;
court.paidETH = ZERO;
court.paidPNK = ZERO;
Expand Down
2 changes: 2 additions & 0 deletions subgraph/core/src/entities/JurorTokensPerCourt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { updateActiveJurors, getDelta, updateStakedPNK } from "../datapoint";
import { ensureUser } from "./User";
import { ONE, ZERO } from "../utils";
import { SortitionModule } from "../../generated/SortitionModule/SortitionModule";
import { updateEffectiveStake } from "./Court";

export function ensureJurorTokensPerCourt(jurorAddress: string, courtID: string): JurorTokensPerCourt {
const id = `${jurorAddress}-${courtID}`;
Expand Down Expand Up @@ -59,6 +60,7 @@ export function updateJurorStake(
updateActiveJurors(activeJurorsDelta, timestamp);
juror.save();
court.save();
updateEffectiveStake(courtID);
}

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.7.2",
"version": "0.7.4",
"license": "MIT",
"scripts": {
"update:core:arbitrum-sepolia-devnet": "./scripts/update.sh arbitrumSepoliaDevnet arbitrum-sepolia core/subgraph.yaml",
Expand Down
10 changes: 10 additions & 0 deletions web/src/assets/svgs/icons/long-arrow-up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion web/src/components/DisputeView/DisputeCardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const CardContainer = styled.div`
flex-direction: column;
justify-content: space-between;
`;

const StyledCaseCardTitleSkeleton = styled(StyledSkeleton)`
margin-bottom: 16px;
`;

const TruncatedTitle = ({ text, maxLength }) => {
const truncatedText = text.length <= maxLength ? text : text.slice(0, maxLength) + "…";
return <h3>{truncatedText}</h3>;
Expand All @@ -54,7 +59,7 @@ const DisputeCardView: React.FC<IDisputeCardView> = ({ isLoading, ...props }) =>
<StyledCard hover onClick={() => navigate(`/cases/${props?.disputeID?.toString()}`)}>
<PeriodBanner id={parseInt(props?.disputeID)} period={props?.period} />
<CardContainer>
{isLoading ? <StyledSkeleton /> : <TruncatedTitle text={props?.title} maxLength={100} />}
{isLoading ? <StyledCaseCardTitleSkeleton /> : <TruncatedTitle text={props?.title} maxLength={100} />}
<DisputeInfo {...props} />
</CardContainer>
</StyledCard>
Expand Down
62 changes: 62 additions & 0 deletions web/src/components/ExtraStatsDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from "react";
import styled from "styled-components";

import { StyledSkeleton } from "components/StyledSkeleton";
import { isUndefined } from "utils/index";

const Container = styled.div`
display: flex;
gap: 8px;
align-items: center;
margin-top: 24px;
`;

const SVGContainer = styled.div`
display: flex;
height: 14px;
width: 14px;
align-items: center;
justify-content: center;
svg {
fill: ${({ theme }) => theme.secondaryPurple};
}
`;

const TextContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
`;

const StyledP = styled.p`
font-size: 14px;
font-weight: 600;
margin: 0;
`;

const StyledExtraStatTitleSkeleton = styled(StyledSkeleton)`
width: 100px;
`;

export interface IExtraStatsDisplay {
title: string;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
content?: React.ReactNode;
text?: string;
}

const ExtraStatsDisplay: React.FC<IExtraStatsDisplay> = ({ title, text, content, icon: Icon, ...props }) => {
return (
<Container {...props}>
<SVGContainer>{<Icon />}</SVGContainer>
<TextContainer>
<label>{title}:</label>
{content ? content : <StyledP>{!isUndefined(text) ? text : <StyledExtraStatTitleSkeleton />}</StyledP>}
</TextContainer>
</Container>
);
};

export default ExtraStatsDisplay;
3 changes: 3 additions & 0 deletions web/src/consts/averageBlockTimeInSeconds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { arbitrum, arbitrumSepolia } from "viem/chains";

export const averageBlockTimeInSeconds = { [arbitrum.id]: 0.26, [arbitrumSepolia.id]: 0.268 };
168 changes: 168 additions & 0 deletions web/src/hooks/queries/useHomePageBlockQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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";
export type { HomePageBlockQuery };

const homePageBlockQuery = graphql(`
query HomePageBlock($blockNumber: Int) {
presentCourts: courts(orderBy: id, orderDirection: asc) {
id
parent {
id
}
name
numberDisputes
numberVotes
feeForJuror
effectiveStake
}
pastCourts: courts(orderBy: id, orderDirection: asc, block: { number: $blockNumber }) {
id
parent {
id
}
name
numberDisputes
numberVotes
feeForJuror
effectiveStake
}
}
`);

type Court = HomePageBlockQuery["presentCourts"][number];
type CourtWithTree = Court & {
numberDisputes: number;
numberVotes: number;
feeForJuror: bigint;
effectiveStake: bigint;
treeNumberDisputes: number;
treeNumberVotes: number;
votesPerPnk: number;
treeVotesPerPnk: number;
expectedRewardPerPnk: number;
treeExpectedRewardPerPnk: number;
};

export type HomePageBlockStats = {
mostDisputedCourt: CourtWithTree;
bestDrawingChancesCourt: CourtWithTree;
bestExpectedRewardCourt: CourtWithTree;
courts: CourtWithTree[];
};

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

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

return processData(data, allTime);
},
});
};

const processData = (data: HomePageBlockQuery, allTime: boolean) => {
const presentCourts = data.presentCourts;
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;
}

processedCourts[id] = {
...court,
treeNumberDisputes: court.treeNumberDisputes + processCourt(parentIndex).treeNumberDisputes,
treeNumberVotes: court.treeNumberVotes + processCourt(parentIndex).treeNumberVotes,
treeVotesPerPnk: court.treeVotesPerPnk + processCourt(parentIndex).treeVotesPerPnk,
treeExpectedRewardPerPnk: court.treeExpectedRewardPerPnk + processCourt(parentIndex).treeExpectedRewardPerPnk,
};

return processedCourts[id];
};

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

processedCourts.reverse();

return {
mostDisputedCourt: getCourtMostDisputes(processedCourts),
bestDrawingChancesCourt: getCourtBestDrawingChances(processedCourts),
bestExpectedRewardCourt: getBestExpectedRewardCourt(processedCourts),
courts: processedCourts,
};
};

const addTreeValues = (court: Court): CourtWithTree => {
const votesPerPnk = Number(court.numberVotes) / (Number(court.effectiveStake) / 1e18);
const expectedRewardPerPnk = votesPerPnk * (Number(court.feeForJuror) / 1e18);
return {
...court,
numberDisputes: Number(court.numberDisputes),
numberVotes: Number(court.numberVotes),
feeForJuror: BigInt(court.feeForJuror) / BigInt(1e18),
effectiveStake: BigInt(court.effectiveStake),
treeNumberDisputes: Number(court.numberDisputes),
treeNumberVotes: Number(court.numberVotes),
votesPerPnk,
treeVotesPerPnk: votesPerPnk,
expectedRewardPerPnk,
treeExpectedRewardPerPnk: expectedRewardPerPnk,
};
};

const addTreeValuesWithDiff = (presentCourt: Court, pastCourt: Court): CourtWithTree => {
const presentCourtWithTree = addTreeValues(presentCourt);
const pastCourtWithTree = addTreeValues(pastCourt);
const diffNumberVotes = presentCourtWithTree.numberVotes - pastCourtWithTree.numberVotes;
const avgEffectiveStake = (presentCourtWithTree.effectiveStake + pastCourtWithTree.effectiveStake) / 2n;
const votesPerPnk = diffNumberVotes / Number(avgEffectiveStake);
const expectedRewardPerPnk = votesPerPnk * Number(presentCourt.feeForJuror);
return {
...presentCourt,
numberDisputes: presentCourtWithTree.numberDisputes - pastCourtWithTree.numberDisputes,
treeNumberDisputes: presentCourtWithTree.treeNumberDisputes - pastCourtWithTree.treeNumberDisputes,
numberVotes: diffNumberVotes,
treeNumberVotes: presentCourtWithTree.treeNumberVotes - pastCourtWithTree.treeNumberVotes,
effectiveStake: avgEffectiveStake,
votesPerPnk,
treeVotesPerPnk: votesPerPnk,
expectedRewardPerPnk,
treeExpectedRewardPerPnk: expectedRewardPerPnk,
};
};

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];
Loading
Loading