Skip to content

Commit edd79fa

Browse files
committed
feat(web,subgraph): real data, add coherence score to subgraph schema & mappings, abstract utils
1 parent f118da4 commit edd79fa

File tree

13 files changed

+166
-120
lines changed

13 files changed

+166
-120
lines changed

subgraph/schema.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type User @entity {
7272
totalResolvedDisputes: BigInt!
7373
totalDisputes: BigInt!
7474
totalCoherent: BigInt!
75+
coherenceScore: BigInt!
7576
totalAppealingDisputes: BigInt!
7677
votes: [Vote!]! @derivedFrom(field: "juror")
7778
contributions: [Contribution!]! @derivedFrom(field: "contributor")

subgraph/src/entities/User.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
1-
import { BigInt } from "@graphprotocol/graph-ts";
1+
import { BigInt, BigDecimal } from "@graphprotocol/graph-ts";
22
import { User } from "../../generated/schema";
33
import { ONE, ZERO } from "../utils";
44

5+
function computeCoherenceScore(totalCoherent: BigInt, totalResolvedDisputes: BigInt): BigInt {
6+
const smoothingFactor = BigInt.fromI32(10);
7+
const shiftFactor = BigInt.fromI32(1000);
8+
let denominator = totalResolvedDisputes.plus(smoothingFactor);
9+
let coherencyRatio = totalCoherent.toBigDecimal().div(denominator.toBigDecimal());
10+
const coherencyScore = coherencyRatio.times(BigDecimal.fromString("100"));
11+
const shiftedValue = coherencyScore.times(BigDecimal.fromString("1000"));
12+
const shiftedBigInt = BigInt.fromString(shiftedValue.toString().split(".")[0]);
13+
const halfShiftFactor = shiftFactor.div(BigInt.fromI32(2));
14+
const remainder = shiftedBigInt.mod(shiftFactor);
15+
16+
if (remainder.ge(halfShiftFactor)) {
17+
return shiftedBigInt.div(shiftFactor).plus(BigInt.fromI32(1));
18+
} else {
19+
return shiftedBigInt.div(shiftFactor);
20+
}
21+
}
22+
523
export function ensureUser(id: string): User {
624
const user = User.load(id);
725

@@ -24,6 +42,7 @@ export function createUserFromAddress(id: string): User {
2442
user.totalAppealingDisputes = ZERO;
2543
user.totalDisputes = ZERO;
2644
user.totalCoherent = ZERO;
45+
user.coherenceScore = ZERO;
2746
user.save();
2847

2948
return user;
@@ -61,5 +80,6 @@ export function resolveUserDispute(id: string, previousFeeAmount: BigInt, feeAmo
6180
user.totalCoherent = user.totalCoherent.plus(ONE);
6281
}
6382
user.activeDisputes = user.activeDisputes.minus(ONE);
83+
user.coherenceScore = computeCoherenceScore(user.totalCoherent, user.totalResolvedDisputes);
6484
user.save();
6585
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { graphql } from "src/graphql";
3+
import { graphqlQueryFnHelper } from "utils/graphqlQueryFnHelper";
4+
import { TopUsersByCoherenceScoreQuery } from "src/graphql/graphql";
5+
import { isUndefined } from "utils/index";
6+
export type { TopUsersByCoherenceScoreQuery };
7+
8+
const topUsersByCoherenceScoreQuery = graphql(`
9+
query TopUsersByCoherenceScore($first: Int!, $orderBy: User_orderBy, $orderDirection: OrderDirection) {
10+
users(first: $first, orderBy: $orderBy, orderDirection: $orderDirection) {
11+
id
12+
coherenceScore
13+
totalCoherent
14+
totalResolvedDisputes
15+
}
16+
}
17+
`);
18+
19+
export const useTopUsersByCoherenceScore = (first = 5) => {
20+
const isEnabled = !isUndefined(first);
21+
22+
return useQuery<TopUsersByCoherenceScoreQuery>({
23+
queryKey: [`TopUsersByCoherenceScore${first}`],
24+
enabled: isEnabled,
25+
staleTime: Infinity,
26+
queryFn: async () =>
27+
isEnabled
28+
? await graphqlQueryFnHelper(topUsersByCoherenceScoreQuery, {
29+
first: first,
30+
orderBy: "coherenceScore",
31+
orderDirection: "desc",
32+
})
33+
: undefined,
34+
});
35+
};

web/src/hooks/queries/useUser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const userFragment = graphql(`
1111
totalResolvedDisputes
1212
totalAppealingDisputes
1313
totalCoherent
14+
coherenceScore
1415
tokens {
1516
court {
1617
id

web/src/pages/Dashboard/JurorInfo/Coherency.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,14 @@ interface ICoherency {
3030
title: string;
3131
};
3232
score: number;
33-
totalCoherent: number;
34-
totalResolvedDisputes: number;
3533
}
3634

37-
const Coherency: React.FC<ICoherency> = ({ userLevelData, score, totalCoherent, totalResolvedDisputes }) => {
35+
const Coherency: React.FC<ICoherency> = ({ userLevelData, score }) => {
3836
return (
3937
<Container>
4038
<small>{userLevelData.title}</small>
4139
<label>Level {userLevelData.level}</label>
42-
<CircularProgress
43-
progress={parseFloat(((totalCoherent / Math.max(totalResolvedDisputes, 1)) * 100).toFixed(2))}
44-
/>
40+
<CircularProgress progress={parseFloat(score.toFixed(2))} />
4541
<WithHelpTooltip place="left" {...{ tooltipMsg }}>
4642
<label>
4743
Coherency Score:
Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import React from "react";
22
import styled from "styled-components";
3-
import { formatUnits, formatEther } from "viem";
43
import { useAccount } from "wagmi";
54
import TokenRewards from "./TokenRewards";
65
import WithHelpTooltip from "../WithHelpTooltip";
7-
import { isUndefined } from "utils/index";
6+
import { getFormattedRewards } from "utils/jurorRewardConfig";
87
import { CoinIds } from "consts/coingecko";
9-
import { useUserQuery, UserQuery } from "queries/useUser";
8+
import { useUserQuery } from "queries/useUser";
109
import { useCoinPrice } from "hooks/useCoinPrice";
1110

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

25-
interface IReward {
26-
token: "ETH" | "PNK";
27-
coinId: number;
28-
getAmount: (amount: bigint) => string;
29-
getValue: (amount: bigint, coinPrice?: number) => string;
30-
}
31-
32-
const rewards: IReward[] = [
33-
{
34-
token: "ETH",
35-
coinId: 1,
36-
getAmount: (amount) => Number(formatEther(amount)).toFixed(3).toString(),
37-
getValue: (amount, coinPrice) => (Number(formatEther(amount)) * (coinPrice ?? 0)).toFixed(2).toString(),
38-
},
39-
{
40-
token: "PNK",
41-
coinId: 0,
42-
getAmount: (amount) => Number(formatUnits(amount, 18)).toFixed(3).toString(),
43-
getValue: (amount, coinPrice) => (Number(formatUnits(amount, 18)) * (coinPrice ?? 0)).toFixed(2).toString(),
44-
},
45-
];
46-
47-
const calculateTotalReward = (coinId: number, data: UserQuery): bigint => {
48-
const total = data.user?.shifts
49-
.map((shift) => parseInt(coinId === 0 ? shift.pnkAmount : shift.ethAmount))
50-
.reduce((acc, curr) => acc + curr, 0);
51-
52-
return BigInt(total ?? 0);
53-
};
54-
55-
const Coherency: React.FC = () => {
24+
const JurorRewards: React.FC = () => {
5625
const { address } = useAccount();
5726
const { data } = useUserQuery(address?.toLowerCase());
5827
const coinIds = [CoinIds.PNK, CoinIds.ETH];
5928
const { prices: pricesData } = useCoinPrice(coinIds);
6029

30+
const formattedRewards = getFormattedRewards(data, pricesData);
31+
6132
return (
6233
<>
6334
<Container>
6435
<WithHelpTooltip place="bottom" {...{ tooltipMsg }}>
6536
<label> Juror Rewards </label>
6637
</WithHelpTooltip>
67-
{rewards.map(({ token, coinId, getValue, getAmount }) => {
68-
const coinPrice = !isUndefined(pricesData) ? pricesData[coinIds[coinId]]?.price : undefined;
69-
const totalReward = data && calculateTotalReward(coinId, data);
70-
return (
71-
<TokenRewards
72-
key={coinId}
73-
{...{ token }}
74-
amount={!isUndefined(totalReward) ? getAmount(totalReward) : undefined}
75-
value={!isUndefined(totalReward) ? getValue(totalReward, coinPrice) : undefined}
76-
/>
77-
);
78-
})}
38+
{formattedRewards.map(({ token, amount, value }) => (
39+
<TokenRewards key={token} {...{ token }} amount={amount} value={value} />
40+
))}
7941
</Container>
8042
</>
8143
);
8244
};
8345

84-
export default Coherency;
46+
export default JurorRewards;

web/src/pages/Dashboard/JurorInfo/index.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import JurorRewards from "./JurorRewards";
77
import PixelArt from "./PixelArt";
88
import { useAccount } from "wagmi";
99
import { useUserQuery } from "queries/useUser";
10-
import { getCoherencyScore, getUserLevelData } from "utils/userLevelCalculation";
10+
import { getUserLevelData } from "utils/userLevelCalculation";
1111
// import StakingRewards from "./StakingRewards";
1212

1313
const Container = styled.div``;
@@ -39,23 +39,16 @@ const Card = styled(_Card)`
3939
const JurorInfo: React.FC = () => {
4040
const { address } = useAccount();
4141
const { data } = useUserQuery(address?.toLowerCase());
42-
const totalCoherent = data?.user ? parseInt(data?.user?.totalCoherent) : 0;
43-
const totalResolvedDisputes = data?.user ? parseInt(data?.user?.totalResolvedDisputes) : 1;
42+
const coherenceScore = data?.user ? parseInt(data?.user?.coherenceScore) : 0;
4443

45-
const coherencyScore = getCoherencyScore(totalCoherent, totalResolvedDisputes);
46-
const userLevelData = getUserLevelData(totalCoherent, totalResolvedDisputes);
44+
const userLevelData = getUserLevelData(coherenceScore);
4745

4846
return (
4947
<Container>
5048
<Header>Juror Dashboard</Header>
5149
<Card>
5250
<PixelArt level={userLevelData.level} width="189px" height="189px" />
53-
<Coherency
54-
userLevelData={userLevelData}
55-
score={coherencyScore}
56-
totalCoherent={totalCoherent}
57-
totalResolvedDisputes={totalResolvedDisputes}
58-
/>
51+
<Coherency userLevelData={userLevelData} score={coherenceScore} />
5952
<JurorRewards />
6053
</Card>
6154
</Container>

web/src/pages/Home/TopJurors/JurorCard.tsx

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { IdenticonOrAvatar, AddressOrName } from "components/ConnectWallet/Accou
55
import EthIcon from "assets/svgs/icons/eth.svg";
66
import PnkIcon from "assets/svgs/icons/kleros.svg";
77
import PixelArt from "pages/Dashboard/JurorInfo/PixelArt";
8+
import { getFormattedRewards } from "utils/jurorRewardConfig";
89
import { getUserLevelData } from "utils/userLevelCalculation";
910
import { useUserQuery } from "hooks/queries/useUser";
1011

11-
const Container = styled.div<{ id?: number }>`
12+
const Container = styled.div`
1213
display: flex;
1314
justify-content: space-between;
1415
flex-wrap: wrap;
@@ -17,7 +18,7 @@ const Container = styled.div<{ id?: number }>`
1718
background-color: ${({ theme }) => theme.whiteBackground};
1819
padding: 24px;
1920
border 1px solid ${({ theme }) => theme.stroke};
20-
border-top: ${({ id }) => (id === 1 ? "" : "none")};
21+
border-top: none;
2122
align-items: center;
2223
2324
label {
@@ -85,7 +86,7 @@ const JurorTitle = styled.div`
8586
8687
${landscapeStyle(
8788
() => css`
88-
width: calc(40px + (232 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
89+
width: calc(40px + (220 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
8990
gap: 36px;
9091
`
9192
)}
@@ -98,13 +99,13 @@ const Rewards = styled.div`
9899
label {
99100
font-weight: 600;
100101
}
101-
width: 132px;
102+
width: 164px;
102103
flex-wrap: wrap;
103104
104105
${landscapeStyle(
105106
() =>
106107
css`
107-
width: calc(60px + (180 - 60) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
108+
width: calc(60px + (240 - 60) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
108109
`
109110
)}
110111
`;
@@ -136,28 +137,28 @@ const HowItWorks = styled.div`
136137
const StyledIdenticonOrAvatar = styled(IdenticonOrAvatar)``;
137138

138139
interface IJurorCard {
139-
id: number;
140+
rank: number;
140141
address: `0x${string}`;
142+
coherenceScore: number;
143+
totalCoherent: number;
144+
totalResolvedDisputes: number;
141145
}
142146

143-
const JurorCard: React.FC<IJurorCard> = ({ id, address }) => {
144-
const ethReward = "11";
145-
const pnkReward = "30K";
146-
const coherentVotes = "10/12";
147-
147+
const JurorCard: React.FC<IJurorCard> = ({ rank, address, coherenceScore, totalCoherent, totalResolvedDisputes }) => {
148148
const { data } = useUserQuery(address?.toLowerCase());
149-
const totalCoherent = data?.user ? parseInt(data?.user?.totalCoherent) : 0;
150-
const totalResolvedDisputes = data?.user ? parseInt(data?.user?.totalResolvedDisputes) : 1;
151-
// const userLevelData = getUserLevelData(totalCoherent, totalResolvedDisputes);
152-
const userLevelData = {
153-
level: 4,
154-
};
149+
150+
const coherenceRatio = `${totalCoherent}/${totalResolvedDisputes}`;
151+
const userLevelData = getUserLevelData(coherenceScore);
152+
153+
const formattedRewards = getFormattedRewards(data, {});
154+
const ethReward = formattedRewards.find((r) => r.token === "ETH")?.amount;
155+
const pnkReward = formattedRewards.find((r) => r.token === "PNK")?.amount;
155156

156157
return (
157-
<Container id={id}>
158+
<Container>
158159
<PlaceAndTitleAndRewardsAndCoherency>
159160
<JurorPlace>
160-
<label>{id}</label>
161+
<label>{rank}</label>
161162
</JurorPlace>
162163
<JurorTitle>
163164
<LogoAndAddress>
@@ -173,7 +174,7 @@ const JurorCard: React.FC<IJurorCard> = ({ id, address }) => {
173174
<StyledIcon as={PnkIcon} />
174175
</Rewards>
175176
<Coherency>
176-
<label>{coherentVotes}</label>
177+
<label>{coherenceRatio}</label>
177178
</Coherency>
178179
</PlaceAndTitleAndRewardsAndCoherency>
179180
<HowItWorks>

web/src/pages/Home/TopJurors/TopJurorsHeader.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const Container = styled.div`
1414
border 1px solid ${({ theme }) => theme.stroke};
1515
border-top-left-radius: 3px;
1616
border-top-right-radius: 3px;
17-
border-bottom: none;
1817
flex-wrap: wrap;
1918
2019
${landscapeStyle(
@@ -80,7 +79,7 @@ const JurorTitle = styled.div`
8079
${landscapeStyle(
8180
() =>
8281
css`
83-
width: calc(40px + (232 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
82+
width: calc(40px + (220 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
8483
gap: 36px;
8584
`
8685
)}
@@ -90,7 +89,7 @@ const Rewards = styled.div`
9089
${landscapeStyle(
9190
() =>
9291
css`
93-
width: calc(60px + (180 - 60) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
92+
width: calc(60px + (240 - 60) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
9493
`
9594
)}
9695
`;

0 commit comments

Comments
 (0)