Skip to content

feat(web): top jurors component #1290

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 14 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 3 additions & 1 deletion subgraph/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"update:local": "./scripts/update.sh localhost mainnet",
"codegen": "graph codegen",
"build": "graph build",
"test": "graph test",
"clean": "graph clean && rm subgraph.yaml.bak.*",
"deploy:arbitrum-goerli": "graph deploy --product hosted-service kleros/kleros-v2-core-testnet-2",
"deploy:arbitrum-goerli-devnet": "graph deploy --product hosted-service kleros/kleros-v2-core-devnet",
Expand All @@ -30,7 +31,8 @@
"@graphprotocol/graph-cli": "0.52.0",
"@kleros/kleros-v2-eslint-config": "workspace:^",
"@kleros/kleros-v2-prettier-config": "workspace:^",
"gluegun": "^5.1.2"
"gluegun": "^5.1.2",
"matchstick-as": "0.6.0-beta.2"
},
"dependenciesComments": {
"@graphprotocol/graph-cli": "pinned because of this issue: https://github.com/graphprotocol/graph-tooling/issues/1399#issuecomment-1676104540"
Expand Down
1 change: 1 addition & 0 deletions subgraph/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type User @entity {
totalResolvedDisputes: BigInt!
totalDisputes: BigInt!
totalCoherent: BigInt!
coherenceScore: BigInt!
totalAppealingDisputes: BigInt!
votes: [Vote!]! @derivedFrom(field: "juror")
contributions: [Contribution!]! @derivedFrom(field: "contributor")
Expand Down
18 changes: 17 additions & 1 deletion subgraph/src/entities/User.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { BigInt } from "@graphprotocol/graph-ts";
import { BigInt, BigDecimal } from "@graphprotocol/graph-ts";
import { User } from "../../generated/schema";
import { ONE, ZERO } from "../utils";

export function computeCoherenceScore(totalCoherent: BigInt, totalResolvedDisputes: BigInt): BigInt {
const smoothingFactor = BigDecimal.fromString("10");

let denominator = totalResolvedDisputes.toBigDecimal().plus(smoothingFactor);
let coherencyRatio = totalCoherent.toBigDecimal().div(denominator);

const coherencyScore = coherencyRatio.times(BigDecimal.fromString("100"));

const roundedScore = coherencyScore.plus(BigDecimal.fromString("0.5"));

return BigInt.fromString(roundedScore.toString().split(".")[0]);
}

export function ensureUser(id: string): User {
const user = User.load(id);

Expand All @@ -24,6 +37,7 @@ export function createUserFromAddress(id: string): User {
user.totalAppealingDisputes = ZERO;
user.totalDisputes = ZERO;
user.totalCoherent = ZERO;
user.coherenceScore = ZERO;
user.save();

return user;
Expand Down Expand Up @@ -52,6 +66,7 @@ export function resolveUserDispute(id: string, previousFeeAmount: BigInt, feeAmo
user.totalCoherent = user.totalCoherent.plus(ONE);
}
}
user.coherenceScore = computeCoherenceScore(user.totalCoherent, user.totalResolvedDisputes);
user.save();
return;
}
Expand All @@ -61,5 +76,6 @@ export function resolveUserDispute(id: string, previousFeeAmount: BigInt, feeAmo
user.totalCoherent = user.totalCoherent.plus(ONE);
}
user.activeDisputes = user.activeDisputes.minus(ONE);
user.coherenceScore = computeCoherenceScore(user.totalCoherent, user.totalResolvedDisputes);
user.save();
}
9 changes: 9 additions & 0 deletions subgraph/tests/user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { assert, test, describe } from "matchstick-as/assembly/index";
import { BigInt } from "@graphprotocol/graph-ts";
import { computeCoherenceScore } from "../src/entities/User";

describe("Compute coherence score", () => {
test("Slam BigInts together", () => {
assert.bigIntEquals(BigInt.fromI32(8), computeCoherenceScore(BigInt.fromI32(1), BigInt.fromI32(2)));
});
});
23 changes: 19 additions & 4 deletions web/src/components/ConnectWallet/AccountDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,15 @@ const StyledAvatar = styled.img<{ size: `${number}` }>`
height: ${({ size }) => size + "px"};
`;

export const IdenticonOrAvatar: React.FC<{ size: `${number}` }> = ({ size } = { size: "16" }) => {
const { address } = useAccount();
interface IIdenticonOrAvatar {
size?: `${number}`;
address?: `0x${string}`;
}

export const IdenticonOrAvatar: React.FC<IIdenticonOrAvatar> = ({ size = "16", address: propAddress }) => {
const { address: defaultAddress } = useAccount();
const address = propAddress || defaultAddress;

const { data: name } = useEnsName({
address,
chainId: 1,
Expand All @@ -106,19 +113,27 @@ export const IdenticonOrAvatar: React.FC<{ size: `${number}` }> = ({ size } = {
name,
chainId: 1,
});

return avatar ? (
<StyledAvatar src={avatar} alt="avatar" size={size} />
) : (
<StyledIdenticon size={size} string={address} />
);
};

export const AddressOrName: React.FC = () => {
const { address } = useAccount();
interface IAddressOrName {
address?: `0x${string}`;
}

export const AddressOrName: React.FC<IAddressOrName> = ({ address: propAddress }) => {
const { address: defaultAddress } = useAccount();
const address = propAddress || defaultAddress;

const { data } = useEnsName({
address,
chainId: 1,
});

return <label>{data ?? (address && shortenAddress(address))}</label>;
};

Expand Down
35 changes: 35 additions & 0 deletions web/src/hooks/queries/useTopUsersByCoherenceScore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useQuery } from "@tanstack/react-query";
import { graphql } from "src/graphql";
import { graphqlQueryFnHelper } from "utils/graphqlQueryFnHelper";
import { TopUsersByCoherenceScoreQuery } from "src/graphql/graphql";
import { isUndefined } from "utils/index";
export type { TopUsersByCoherenceScoreQuery };

const topUsersByCoherenceScoreQuery = graphql(`
query TopUsersByCoherenceScore($first: Int!, $orderBy: User_orderBy, $orderDirection: OrderDirection) {
users(first: $first, orderBy: $orderBy, orderDirection: $orderDirection) {
id
coherenceScore
totalCoherent
totalResolvedDisputes
}
}
`);

export const useTopUsersByCoherenceScore = (first = 5) => {
const isEnabled = !isUndefined(first);

return useQuery<TopUsersByCoherenceScoreQuery>({
queryKey: [`TopUsersByCoherenceScore${first}`],
enabled: isEnabled,
staleTime: Infinity,
queryFn: async () =>
isEnabled
? await graphqlQueryFnHelper(topUsersByCoherenceScoreQuery, {
first: first,
orderBy: "coherenceScore",
orderDirection: "desc",
})
: undefined,
});
};
1 change: 1 addition & 0 deletions web/src/hooks/queries/useUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const userFragment = graphql(`
totalResolvedDisputes
totalAppealingDisputes
totalCoherent
coherenceScore
tokens {
court {
id
Expand Down
15 changes: 8 additions & 7 deletions web/src/pages/Dashboard/JurorInfo/Coherency.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,20 @@ const Container = styled.div`

const tooltipMsg =
"A Coherent Vote is a vote coherent with the final jury decision" +
" (after all the appeal instances). Your coherency score is calculated" +
" using the number of times you have been coherent and the total cases you" +
" have been in.";
" (after all the appeal instances). If the juror vote is the same as " +
" the majority of jurors it's considered a Coherent Vote.";

interface ICoherency {
userLevelData: {
scoreRange: number[];
level: number;
title: string;
};
score: number;
totalCoherent: number;
totalResolvedDisputes: number;
}

const Coherency: React.FC<ICoherency> = ({ userLevelData, score, totalCoherent, totalResolvedDisputes }) => {
const Coherency: React.FC<ICoherency> = ({ userLevelData, totalCoherent, totalResolvedDisputes }) => {
return (
<Container>
<small>{userLevelData.title}</small>
Expand All @@ -44,8 +42,11 @@ const Coherency: React.FC<ICoherency> = ({ userLevelData, score, totalCoherent,
/>
<WithHelpTooltip place="left" {...{ tooltipMsg }}>
<label>
Coherency Score:
<small> {score.toFixed(2)} </small>
Coherent Votes:
<small>
{" "}
{totalCoherent}/{totalResolvedDisputes}{" "}
</small>
</label>
</WithHelpTooltip>
</Container>
Expand Down
56 changes: 9 additions & 47 deletions web/src/pages/Dashboard/JurorInfo/JurorRewards.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React from "react";
import styled from "styled-components";
import { formatUnits, formatEther } from "viem";
import { useAccount } from "wagmi";
import TokenRewards from "./TokenRewards";
import WithHelpTooltip from "../WithHelpTooltip";
import { isUndefined } from "utils/index";
import { getFormattedRewards } from "utils/jurorRewardConfig";
import { CoinIds } from "consts/coingecko";
import { useUserQuery, UserQuery } from "queries/useUser";
import { useUserQuery } from "queries/useUser";
import { useCoinPrice } from "hooks/useCoinPrice";

const Container = styled.div`
Expand All @@ -22,63 +21,26 @@ const tooltipMsg =
"is coherent with the final ruling receive the Juror Rewards composed of " +
"arbitration fees (ETH) + PNK redistribution between jurors.";

interface IReward {
token: "ETH" | "PNK";
coinId: number;
getAmount: (amount: bigint) => string;
getValue: (amount: bigint, coinPrice?: number) => string;
}

const rewards: IReward[] = [
{
token: "ETH",
coinId: 1,
getAmount: (amount) => Number(formatEther(amount)).toFixed(3).toString(),
getValue: (amount, coinPrice) => (Number(formatEther(amount)) * (coinPrice ?? 0)).toFixed(2).toString(),
},
{
token: "PNK",
coinId: 0,
getAmount: (amount) => Number(formatUnits(amount, 18)).toFixed(3).toString(),
getValue: (amount, coinPrice) => (Number(formatUnits(amount, 18)) * (coinPrice ?? 0)).toFixed(2).toString(),
},
];

const calculateTotalReward = (coinId: number, data: UserQuery): bigint => {
const total = data.user?.shifts
.map((shift) => parseInt(coinId === 0 ? shift.pnkAmount : shift.ethAmount))
.reduce((acc, curr) => acc + curr, 0);

return BigInt(total ?? 0);
};

const Coherency: React.FC = () => {
const JurorRewards: React.FC = () => {
const { address } = useAccount();
const { data } = useUserQuery(address?.toLowerCase());
const coinIds = [CoinIds.PNK, CoinIds.ETH];
const { prices: pricesData } = useCoinPrice(coinIds);

const formattedRewards = getFormattedRewards(data, pricesData);

return (
<>
<Container>
<WithHelpTooltip place="bottom" {...{ tooltipMsg }}>
<label> Juror Rewards </label>
</WithHelpTooltip>
{rewards.map(({ token, coinId, getValue, getAmount }) => {
const coinPrice = !isUndefined(pricesData) ? pricesData[coinIds[coinId]]?.price : undefined;
const totalReward = data && calculateTotalReward(coinId, data);
return (
<TokenRewards
key={coinId}
{...{ token }}
amount={!isUndefined(totalReward) ? getAmount(totalReward) : undefined}
value={!isUndefined(totalReward) ? getValue(totalReward, coinPrice) : undefined}
/>
);
})}
{formattedRewards.map(({ token, amount, value }) => (
<TokenRewards key={token} {...{ token }} amount={amount} value={value} />
))}
</Container>
</>
);
};

export default Coherency;
export default JurorRewards;
31 changes: 23 additions & 8 deletions web/src/pages/Dashboard/JurorInfo/PixelArt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,48 @@ import socratesImage from "assets/pngs/dashboard/socrates.png";
import platoImage from "assets/pngs/dashboard/plato.png";
import aristotelesImage from "assets/pngs/dashboard/aristoteles.png";

const StyledImage = styled.img<{ show: boolean }>`
width: 189px;
height: 189px;
interface IStyledImage {
show: boolean;
width: number | string;
height: number | string;
}

const StyledImage = styled.img<IStyledImage>`
width: ${({ width }) => width};
height: ${({ height }) => height};
display: ${({ show }) => (show ? "block" : "none")};
`;

const StyledSkeleton = styled(Skeleton)`
width: 189px;
height: 189px;
interface IStyledSkeleton {
width: number | string;
height: number | string;
}

const StyledSkeleton = styled(Skeleton)<IStyledSkeleton>`
width: ${({ width }) => width};
height: ${({ height }) => height};
`;

const images = [diogenesImage, pythagorasImage, socratesImage, platoImage, aristotelesImage];

interface IPixelArt {
level: number;
width: number | string;
height: number | string;
}

const PixelArt: React.FC<IPixelArt> = ({ level }) => {
const PixelArt: React.FC<IPixelArt> = ({ level, width, height }) => {
const [imageLoaded, setImageLoaded] = useState(false);
return (
<div>
{!imageLoaded && <StyledSkeleton />}
{!imageLoaded && <StyledSkeleton width={width} height={height} />}
<StyledImage
src={images[level]}
alt="Pixel Art per Level"
onLoad={() => setImageLoaded(true)}
show={imageLoaded}
width={width}
height={height}
/>
</div>
);
Expand Down
Loading