Skip to content
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

feat(web): extra statistic on homepage #1671

Merged
merged 27 commits into from
Oct 7, 2024
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

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