diff --git a/subgraph/package.json b/subgraph/package.json index 5b00b41f5..dcb15a5df 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -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", @@ -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" diff --git a/subgraph/schema.graphql b/subgraph/schema.graphql index 851cfc015..e0826be6e 100644 --- a/subgraph/schema.graphql +++ b/subgraph/schema.graphql @@ -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") diff --git a/subgraph/src/entities/User.ts b/subgraph/src/entities/User.ts index 918878bcb..85a9da833 100644 --- a/subgraph/src/entities/User.ts +++ b/subgraph/src/entities/User.ts @@ -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); @@ -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; @@ -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; } @@ -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(); } diff --git a/subgraph/tests/user.test.ts b/subgraph/tests/user.test.ts new file mode 100644 index 000000000..c69548b48 --- /dev/null +++ b/subgraph/tests/user.test.ts @@ -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))); + }); +}); diff --git a/web/src/components/ConnectWallet/AccountDisplay.tsx b/web/src/components/ConnectWallet/AccountDisplay.tsx index d39e7c60e..1d677637e 100644 --- a/web/src/components/ConnectWallet/AccountDisplay.tsx +++ b/web/src/components/ConnectWallet/AccountDisplay.tsx @@ -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 = ({ size = "16", address: propAddress }) => { + const { address: defaultAddress } = useAccount(); + const address = propAddress || defaultAddress; + const { data: name } = useEnsName({ address, chainId: 1, @@ -106,6 +113,7 @@ export const IdenticonOrAvatar: React.FC<{ size: `${number}` }> = ({ size } = { name, chainId: 1, }); + return avatar ? ( ) : ( @@ -113,12 +121,19 @@ export const IdenticonOrAvatar: React.FC<{ size: `${number}` }> = ({ size } = { ); }; -export const AddressOrName: React.FC = () => { - const { address } = useAccount(); +interface IAddressOrName { + address?: `0x${string}`; +} + +export const AddressOrName: React.FC = ({ address: propAddress }) => { + const { address: defaultAddress } = useAccount(); + const address = propAddress || defaultAddress; + const { data } = useEnsName({ address, chainId: 1, }); + return ; }; diff --git a/web/src/hooks/queries/useTopUsersByCoherenceScore.ts b/web/src/hooks/queries/useTopUsersByCoherenceScore.ts new file mode 100644 index 000000000..f09575ece --- /dev/null +++ b/web/src/hooks/queries/useTopUsersByCoherenceScore.ts @@ -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({ + queryKey: [`TopUsersByCoherenceScore${first}`], + enabled: isEnabled, + staleTime: Infinity, + queryFn: async () => + isEnabled + ? await graphqlQueryFnHelper(topUsersByCoherenceScoreQuery, { + first: first, + orderBy: "coherenceScore", + orderDirection: "desc", + }) + : undefined, + }); +}; diff --git a/web/src/hooks/queries/useUser.ts b/web/src/hooks/queries/useUser.ts index 94aa77828..6fa58a617 100644 --- a/web/src/hooks/queries/useUser.ts +++ b/web/src/hooks/queries/useUser.ts @@ -11,6 +11,7 @@ export const userFragment = graphql(` totalResolvedDisputes totalAppealingDisputes totalCoherent + coherenceScore tokens { court { id diff --git a/web/src/pages/Dashboard/JurorInfo/Coherency.tsx b/web/src/pages/Dashboard/JurorInfo/Coherency.tsx index e836754cf..e340065e2 100644 --- a/web/src/pages/Dashboard/JurorInfo/Coherency.tsx +++ b/web/src/pages/Dashboard/JurorInfo/Coherency.tsx @@ -19,9 +19,8 @@ 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: { @@ -29,12 +28,11 @@ interface ICoherency { level: number; title: string; }; - score: number; totalCoherent: number; totalResolvedDisputes: number; } -const Coherency: React.FC = ({ userLevelData, score, totalCoherent, totalResolvedDisputes }) => { +const Coherency: React.FC = ({ userLevelData, totalCoherent, totalResolvedDisputes }) => { return ( {userLevelData.title} @@ -44,8 +42,11 @@ const Coherency: React.FC = ({ userLevelData, score, totalCoherent, /> diff --git a/web/src/pages/Dashboard/JurorInfo/JurorRewards.tsx b/web/src/pages/Dashboard/JurorInfo/JurorRewards.tsx index f51c5b515..2c8653e5a 100644 --- a/web/src/pages/Dashboard/JurorInfo/JurorRewards.tsx +++ b/web/src/pages/Dashboard/JurorInfo/JurorRewards.tsx @@ -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` @@ -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 ( <> - {rewards.map(({ token, coinId, getValue, getAmount }) => { - const coinPrice = !isUndefined(pricesData) ? pricesData[coinIds[coinId]]?.price : undefined; - const totalReward = data && calculateTotalReward(coinId, data); - return ( - - ); - })} + {formattedRewards.map(({ token, amount, value }) => ( + + ))} ); }; -export default Coherency; +export default JurorRewards; diff --git a/web/src/pages/Dashboard/JurorInfo/PixelArt.tsx b/web/src/pages/Dashboard/JurorInfo/PixelArt.tsx index 2545ac9ed..7b360be1d 100644 --- a/web/src/pages/Dashboard/JurorInfo/PixelArt.tsx +++ b/web/src/pages/Dashboard/JurorInfo/PixelArt.tsx @@ -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` + 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)` + 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 = ({ level }) => { +const PixelArt: React.FC = ({ level, width, height }) => { const [imageLoaded, setImageLoaded] = useState(false); return (
- {!imageLoaded && } + {!imageLoaded && } setImageLoaded(true)} show={imageLoaded} + width={width} + height={height} />
); diff --git a/web/src/pages/Dashboard/JurorInfo/index.tsx b/web/src/pages/Dashboard/JurorInfo/index.tsx index 9a2e803b8..e0dc87b2e 100644 --- a/web/src/pages/Dashboard/JurorInfo/index.tsx +++ b/web/src/pages/Dashboard/JurorInfo/index.tsx @@ -7,6 +7,7 @@ import JurorRewards from "./JurorRewards"; import PixelArt from "./PixelArt"; import { useAccount } from "wagmi"; import { useUserQuery } from "queries/useUser"; +import { getUserLevelData } from "utils/userLevelCalculation"; // import StakingRewards from "./StakingRewards"; const Container = styled.div``; @@ -35,38 +36,22 @@ const Card = styled(_Card)` )} `; -const levelTitles = [ - { scoreRange: [0, 20], level: 0, title: "Diogenes" }, - { scoreRange: [20, 40], level: 1, title: "Pythagoras" }, - { scoreRange: [40, 60], level: 2, title: "Socrates" }, - { scoreRange: [60, 80], level: 3, title: "Plato" }, - { scoreRange: [80, 100], level: 4, title: "Aristotle" }, -]; - -const calculateCoherencyScore = (totalCoherent: number, totalDisputes: number): number => - totalCoherent / (Math.max(totalDisputes, 1) + 10); - const JurorInfo: React.FC = () => { const { address } = useAccount(); const { data } = useUserQuery(address?.toLowerCase()); + const coherenceScore = data?.user ? parseInt(data?.user?.coherenceScore) : 0; const totalCoherent = data?.user ? parseInt(data?.user?.totalCoherent) : 0; - const totalResolvedDisputes = data?.user ? parseInt(data?.user?.totalResolvedDisputes) : 1; + const totalResolvedDisputes = data?.user ? parseInt(data?.user?.totalResolvedDisputes) : 0; - const coherencyScore = calculateCoherencyScore(totalCoherent, totalResolvedDisputes); - const roundedCoherencyScore = Math.round(coherencyScore * 100); - const userLevelData = - levelTitles.find(({ scoreRange }) => { - return roundedCoherencyScore >= scoreRange[0] && roundedCoherencyScore < scoreRange[1]; - }) ?? levelTitles[0]; + const userLevelData = getUserLevelData(coherenceScore); return (
Juror Dashboard
- + diff --git a/web/src/pages/Home/TopJurors/JurorCard.tsx b/web/src/pages/Home/TopJurors/JurorCard.tsx new file mode 100644 index 000000000..558212f5e --- /dev/null +++ b/web/src/pages/Home/TopJurors/JurorCard.tsx @@ -0,0 +1,188 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; +import { IdenticonOrAvatar, AddressOrName } from "components/ConnectWallet/AccountDisplay"; +import EthIcon from "assets/svgs/icons/eth.svg"; +import PnkIcon from "assets/svgs/icons/kleros.svg"; +import PixelArt from "pages/Dashboard/JurorInfo/PixelArt"; +import { getFormattedRewards } from "utils/jurorRewardConfig"; +import { getUserLevelData } from "utils/userLevelCalculation"; +import { useUserQuery } from "hooks/queries/useUser"; + +const Container = styled.div` + display: flex; + justify-content: space-between; + flex-wrap: wrap; + width: 100%; + height: 100%; + background-color: ${({ theme }) => theme.whiteBackground}; + padding: 24px; + border 1px solid ${({ theme }) => theme.stroke}; + border-top: none; + align-items: center; + + label { + font-size: 16px; + } + + ${landscapeStyle( + () => css` + gap: 0px; + padding: 15.55px 32px; + flex-wrap: nowrap; + ` + )} +`; + +const LogoAndAddress = styled.div` + display: flex; + gap: 10px; + align-items: center; + + canvas { + width: 20px; + height: 20px; + border-radius: 10%; + } +`; + +const PlaceAndTitleAndRewardsAndCoherency = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + + ${landscapeStyle( + () => + css` + flex-direction: row; + gap: 32px; + ` + )} +`; + +const JurorPlace = styled.div` + width: 100%; + + label::before { + content: "#"; + display: inline; + } + + ${landscapeStyle( + () => css` + width: calc(16px + (24 - 16) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + label::before { + display: none; + } + ` + )} +`; + +const JurorTitle = styled.div` + display: flex; + gap: 16px; + align-items: center; + justify-content: flex-start; + + ${landscapeStyle( + () => css` + width: calc(40px + (220 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + gap: 36px; + ` + )} +`; + +const Rewards = styled.div` + display: flex; + gap: 8px; + align-items: center; + label { + font-weight: 600; + } + width: 164px; + flex-wrap: wrap; + + ${landscapeStyle( + () => + css` + width: calc(60px + (240 - 60) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + ` + )} +`; + +const Coherency = styled.div` + display: flex; + align-items: center; + label { + font-weight: 600; + } + flex-wrap: wrap; +`; + +const StyledIcon = styled.div` + width: 16px; + height: 16px; + + path { + fill: ${({ theme }) => theme.secondaryPurple}; + } +`; + +const HowItWorks = styled.div` + display: flex; + align-items: center; + gap: 16px; +`; + +const StyledIdenticonOrAvatar = styled(IdenticonOrAvatar)``; + +interface IJurorCard { + rank: number; + address: `0x${string}`; + coherenceScore: number; + totalCoherent: number; + totalResolvedDisputes: number; +} + +const JurorCard: React.FC = ({ rank, address, coherenceScore, totalCoherent, totalResolvedDisputes }) => { + const { data } = useUserQuery(address?.toLowerCase()); + + const coherenceRatio = `${totalCoherent}/${totalResolvedDisputes}`; + const userLevelData = getUserLevelData(coherenceScore); + + const formattedRewards = getFormattedRewards(data, {}); + const ethReward = formattedRewards.find((r) => r.token === "ETH")?.amount; + const pnkReward = formattedRewards.find((r) => r.token === "PNK")?.amount; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default JurorCard; diff --git a/web/src/pages/Home/TopJurors/TopJurorsHeader.tsx b/web/src/pages/Home/TopJurors/TopJurorsHeader.tsx new file mode 100644 index 000000000..1130b8580 --- /dev/null +++ b/web/src/pages/Home/TopJurors/TopJurorsHeader.tsx @@ -0,0 +1,156 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; +import WithHelpTooltip from "pages/Dashboard/WithHelpTooltip"; +import BookOpenIcon from "tsx:assets/svgs/icons/book-open.svg"; + +const Container = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + height: 100%; + background-color: ${({ theme }) => theme.lightBlue}; + padding: 24px; + border 1px solid ${({ theme }) => theme.stroke}; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + flex-wrap: wrap; + + ${landscapeStyle( + () => + css` + flex-wrap: nowrap; + gap: 0px; + padding: 18.6px 32px; + ` + )} +`; + +const PlaceAndTitleAndRewardsAndCoherency = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + + ${landscapeStyle( + () => + css` + flex-direction: row; + gap: 32px; + ` + )} +`; + +const JurorPlace = styled.div` + width: 100%; + + label { + &::before { + content: "# Rank"; + visibility: visible; + } + } + + ${landscapeStyle( + () => + css` + width: calc(16px + (24 - 16) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + + label { + &::before { + content: "#"; + } + } + ` + )} +`; + +const JurorTitle = styled.div` + display: flex; + gap: 16px; + align-items: center; + + label { + font-weight: 400; + font-size: 14px; + line-height: 19px; + color: ${({ theme }) => theme.secondaryText} !important; + } + + ${landscapeStyle( + () => + css` + width: calc(40px + (220 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + gap: 36px; + ` + )} +`; + +const Rewards = styled.div` + ${landscapeStyle( + () => + css` + width: calc(60px + (240 - 60) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + ` + )} +`; + +const Coherency = styled.div``; + +const HowItWorks = styled.div` + display: flex; + align-items: center; + gap: 8px; + + label { + color: ${({ theme }) => theme.primaryBlue}; + } + + svg { + path { + fill: ${({ theme }) => theme.primaryBlue}; + } + } +`; + +const totalRewardsTooltipMsg = + "Users have an economic interest in serving as jurors in Kleros: " + + "collecting the Juror Rewards in exchange for their work. Each juror who " + + "is coherent with the final ruling receive the Juror Rewards composed of " + + "arbitration fees (ETH) + PNK redistribution between jurors."; + +const coherentVotesTooltipMsg = + "This is the ratio of coherent votes made by a juror: " + + "the number in the left is the number of times where the juror " + + "voted coherently and the number in the right is the total number of times " + + "the juror voted"; + +const TopJurorsHeader: React.FC = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default TopJurorsHeader; diff --git a/web/src/pages/Home/TopJurors/index.tsx b/web/src/pages/Home/TopJurors/index.tsx new file mode 100644 index 000000000..043912bbd --- /dev/null +++ b/web/src/pages/Home/TopJurors/index.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import styled from "styled-components"; +import { SkeletonDisputeListItem } from "components/StyledSkeleton"; +import { isUndefined } from "utils/index"; +import TopJurorsHeader from "./TopJurorsHeader"; +import JurorCard from "./JurorCard"; +import { useTopUsersByCoherenceScore } from "queries/useTopUsersByCoherenceScore"; + +const Container = styled.div` + margin-top: calc(64px + (80 - 64) * (min(max(100vw, 375px), 1250px) - 375px) / 875); +`; + +const Title = styled.h1` + margin-bottom: calc(16px + (48 - 16) * (min(max(100vw, 375px), 1250px) - 375px) / 875); +`; + +const ListContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; +`; + +const TopJurors: React.FC = () => { + const { data: queryJurors } = useTopUsersByCoherenceScore(); + + const topJurors = queryJurors?.users?.map((juror, index) => ({ + ...juror, + rank: index + 1, + })); + + return ( + + Top Jurors + + + {!isUndefined(topJurors) + ? topJurors.map((juror) => ) + : [...Array(5)].map((_, i) => )} + + + ); +}; +export default TopJurors; diff --git a/web/src/pages/Home/index.tsx b/web/src/pages/Home/index.tsx index 319dff94f..962e71467 100644 --- a/web/src/pages/Home/index.tsx +++ b/web/src/pages/Home/index.tsx @@ -6,6 +6,7 @@ import Community from "./Community"; import HeroImage from "./HeroImage"; import { HomePageProvider } from "hooks/useHomePageContext"; import { getOneYearAgoTimestamp } from "utils/date"; +import TopJurors from "./TopJurors"; const Container = styled.div` width: 100%; @@ -24,6 +25,7 @@ const Home: React.FC = () => { + diff --git a/web/src/utils/jurorRewardConfig.ts b/web/src/utils/jurorRewardConfig.ts new file mode 100644 index 000000000..dcce97751 --- /dev/null +++ b/web/src/utils/jurorRewardConfig.ts @@ -0,0 +1,45 @@ +import { formatUnits, formatEther } from "viem"; +import { isUndefined } from "utils/index"; +import { UserQuery } from "queries/useUser"; + +export interface IReward { + token: "ETH" | "PNK"; + coinId: number; + getAmount: (amount: bigint) => string; + getValue?: (amount: bigint, coinPrice?: number) => string; +} + +export 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(), + }, +]; + +export const calculateTotalJurorReward = (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); +}; + +export const getFormattedRewards = (data: any, pricesData: any) => { + return rewards.map(({ token, coinId, getValue, getAmount }) => { + const coinPrice = !isUndefined(pricesData) ? pricesData[coinId]?.price : undefined; + const totalReward = data && calculateTotalJurorReward(coinId, data); + return { + token, + amount: !isUndefined(totalReward) ? getAmount(totalReward) : undefined, + value: getValue ? (!isUndefined(totalReward) ? getValue(totalReward, coinPrice) : undefined) : undefined, + }; + }); +}; diff --git a/web/src/utils/userLevelCalculation.ts b/web/src/utils/userLevelCalculation.ts new file mode 100644 index 000000000..69344098e --- /dev/null +++ b/web/src/utils/userLevelCalculation.ts @@ -0,0 +1,15 @@ +export const levelTitles = [ + { scoreRange: [0, 20], level: 0, title: "Diogenes" }, + { scoreRange: [20, 40], level: 1, title: "Pythagoras" }, + { scoreRange: [40, 60], level: 2, title: "Socrates" }, + { scoreRange: [60, 80], level: 3, title: "Plato" }, + { scoreRange: [80, 100], level: 4, title: "Aristotle" }, +]; + +export const getUserLevelData = (coherencyScore: number) => { + return ( + levelTitles.find(({ scoreRange }) => { + return coherencyScore >= scoreRange[0] && coherencyScore < scoreRange[1]; + }) ?? levelTitles[0] + ); +}; diff --git a/yarn.lock b/yarn.lock index aed203c19..2fa79d135 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5349,6 +5349,7 @@ __metadata: "@kleros/kleros-v2-eslint-config": "workspace:^" "@kleros/kleros-v2-prettier-config": "workspace:^" gluegun: ^5.1.2 + matchstick-as: 0.6.0-beta.2 languageName: unknown linkType: soft @@ -6081,161 +6082,161 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/ethereumjs-block@npm:5.0.1": - version: 5.0.1 - resolution: "@nomicfoundation/ethereumjs-block@npm:5.0.1" +"@nomicfoundation/ethereumjs-block@npm:5.0.2": + version: 5.0.2 + resolution: "@nomicfoundation/ethereumjs-block@npm:5.0.2" dependencies: - "@nomicfoundation/ethereumjs-common": 4.0.1 - "@nomicfoundation/ethereumjs-rlp": 5.0.1 - "@nomicfoundation/ethereumjs-trie": 6.0.1 - "@nomicfoundation/ethereumjs-tx": 5.0.1 - "@nomicfoundation/ethereumjs-util": 9.0.1 + "@nomicfoundation/ethereumjs-common": 4.0.2 + "@nomicfoundation/ethereumjs-rlp": 5.0.2 + "@nomicfoundation/ethereumjs-trie": 6.0.2 + "@nomicfoundation/ethereumjs-tx": 5.0.2 + "@nomicfoundation/ethereumjs-util": 9.0.2 ethereum-cryptography: 0.1.3 ethers: ^5.7.1 - checksum: 02591bc9ba02b56edc5faf75a7991d6b9430bd98542864f2f6ab202f0f4aed09be156fdba60948375beb10e524ffa4e461475edc8a15b3098b1c58ff59a0137e + checksum: 7ff744f44a01f1c059ca7812a1cfc8089f87aa506af6cb39c78331dca71b32993cbd6fa05ad03f8c4f4fab73bb998a927af69e0d8ff01ae192ee5931606e09f5 languageName: node linkType: hard -"@nomicfoundation/ethereumjs-blockchain@npm:7.0.1": - version: 7.0.1 - resolution: "@nomicfoundation/ethereumjs-blockchain@npm:7.0.1" - dependencies: - "@nomicfoundation/ethereumjs-block": 5.0.1 - "@nomicfoundation/ethereumjs-common": 4.0.1 - "@nomicfoundation/ethereumjs-ethash": 3.0.1 - "@nomicfoundation/ethereumjs-rlp": 5.0.1 - "@nomicfoundation/ethereumjs-trie": 6.0.1 - "@nomicfoundation/ethereumjs-tx": 5.0.1 - "@nomicfoundation/ethereumjs-util": 9.0.1 +"@nomicfoundation/ethereumjs-blockchain@npm:7.0.2": + version: 7.0.2 + resolution: "@nomicfoundation/ethereumjs-blockchain@npm:7.0.2" + dependencies: + "@nomicfoundation/ethereumjs-block": 5.0.2 + "@nomicfoundation/ethereumjs-common": 4.0.2 + "@nomicfoundation/ethereumjs-ethash": 3.0.2 + "@nomicfoundation/ethereumjs-rlp": 5.0.2 + "@nomicfoundation/ethereumjs-trie": 6.0.2 + "@nomicfoundation/ethereumjs-tx": 5.0.2 + "@nomicfoundation/ethereumjs-util": 9.0.2 abstract-level: ^1.0.3 debug: ^4.3.3 ethereum-cryptography: 0.1.3 level: ^8.0.0 lru-cache: ^5.1.1 memory-level: ^1.0.0 - checksum: 8b7a4e3613c2abbf59e92a927cb074d1df8640fbf6a0ec4be7fcb5ecaead1310ebbe3a41613c027253742f6dccca6eaeee8dde0a38315558de156313d0c8f313 + checksum: b7e440dcd73e32aa72d13bfd28cb472773c9c60ea808a884131bf7eb3f42286ad594a0864215f599332d800f3fe1f772fff4b138d2dcaa8f41e4d8389bff33e7 languageName: node linkType: hard -"@nomicfoundation/ethereumjs-common@npm:4.0.1": - version: 4.0.1 - resolution: "@nomicfoundation/ethereumjs-common@npm:4.0.1" +"@nomicfoundation/ethereumjs-common@npm:4.0.2": + version: 4.0.2 + resolution: "@nomicfoundation/ethereumjs-common@npm:4.0.2" dependencies: - "@nomicfoundation/ethereumjs-util": 9.0.1 + "@nomicfoundation/ethereumjs-util": 9.0.2 crc-32: ^1.2.0 - checksum: af5b599bcc07430b57017e516b0bad70af04e812b970be9bfae0c1d3433ab26656b3d1db71717b3b0fb38a889db2b93071b45adc1857000e7cd58a99a8e29495 + checksum: f0d84704d6254d374299c19884312bd5666974b4b6f342d3f10bc76e549de78d20e45a53d25fbdc146268a52335497127e4f069126da7c60ac933a158e704887 languageName: node linkType: hard -"@nomicfoundation/ethereumjs-ethash@npm:3.0.1": - version: 3.0.1 - resolution: "@nomicfoundation/ethereumjs-ethash@npm:3.0.1" +"@nomicfoundation/ethereumjs-ethash@npm:3.0.2": + version: 3.0.2 + resolution: "@nomicfoundation/ethereumjs-ethash@npm:3.0.2" dependencies: - "@nomicfoundation/ethereumjs-block": 5.0.1 - "@nomicfoundation/ethereumjs-rlp": 5.0.1 - "@nomicfoundation/ethereumjs-util": 9.0.1 + "@nomicfoundation/ethereumjs-block": 5.0.2 + "@nomicfoundation/ethereumjs-rlp": 5.0.2 + "@nomicfoundation/ethereumjs-util": 9.0.2 abstract-level: ^1.0.3 bigint-crypto-utils: ^3.0.23 ethereum-cryptography: 0.1.3 - checksum: beeec9788a9ed57020ee47271447715bdc0a98990a0bd0e9d598c6de74ade836db17c0590275e6aab12fa9b0fbd81f1d02e3cdf1fb8497583cec693ec3ed6aed + checksum: e4011e4019dd9b92f7eeebfc1e6c9a9685c52d8fd0ee4f28f03e50048a23b600c714490827f59fdce497b3afb503b3fd2ebf6815ff307e9949c3efeff1403278 languageName: node linkType: hard -"@nomicfoundation/ethereumjs-evm@npm:2.0.1": - version: 2.0.1 - resolution: "@nomicfoundation/ethereumjs-evm@npm:2.0.1" +"@nomicfoundation/ethereumjs-evm@npm:2.0.2": + version: 2.0.2 + resolution: "@nomicfoundation/ethereumjs-evm@npm:2.0.2" dependencies: "@ethersproject/providers": ^5.7.1 - "@nomicfoundation/ethereumjs-common": 4.0.1 - "@nomicfoundation/ethereumjs-tx": 5.0.1 - "@nomicfoundation/ethereumjs-util": 9.0.1 + "@nomicfoundation/ethereumjs-common": 4.0.2 + "@nomicfoundation/ethereumjs-tx": 5.0.2 + "@nomicfoundation/ethereumjs-util": 9.0.2 debug: ^4.3.3 ethereum-cryptography: 0.1.3 mcl-wasm: ^0.7.1 rustbn.js: ~0.2.0 - checksum: 0aa2e1460e1c311506fd3bf9d03602c7c3a5e03f352173a55a274a9cc1840bd774692d1c4e5c6e82a7eee015a7cf1585f1c5be02cfdf54cc2a771421820e3f84 + checksum: a23cf570836ddc147606b02df568069de946108e640f902358fef67e589f6b371d856056ee44299d9b4e3497f8ae25faa45e6b18fefd90e9b222dc6a761d85f0 languageName: node linkType: hard -"@nomicfoundation/ethereumjs-rlp@npm:5.0.1": - version: 5.0.1 - resolution: "@nomicfoundation/ethereumjs-rlp@npm:5.0.1" +"@nomicfoundation/ethereumjs-rlp@npm:5.0.2": + version: 5.0.2 + resolution: "@nomicfoundation/ethereumjs-rlp@npm:5.0.2" bin: rlp: bin/rlp - checksum: 5a51d2cf92b84e50ce516cbdadff5d39cb4c6b71335e92eaf447dfb7d88f5499d78d599024b9252efd7ba99495de36f4d983cec6a89e77db286db691fc6328f7 + checksum: a74434cadefca9aa8754607cc1ad7bb4bbea4ee61c6214918e60a5bbee83206850346eb64e39fd1fe97f854c7ec0163e01148c0c881dda23881938f0645a0ef2 languageName: node linkType: hard -"@nomicfoundation/ethereumjs-statemanager@npm:2.0.1": - version: 2.0.1 - resolution: "@nomicfoundation/ethereumjs-statemanager@npm:2.0.1" +"@nomicfoundation/ethereumjs-statemanager@npm:2.0.2": + version: 2.0.2 + resolution: "@nomicfoundation/ethereumjs-statemanager@npm:2.0.2" dependencies: - "@nomicfoundation/ethereumjs-common": 4.0.1 - "@nomicfoundation/ethereumjs-rlp": 5.0.1 + "@nomicfoundation/ethereumjs-common": 4.0.2 + "@nomicfoundation/ethereumjs-rlp": 5.0.2 debug: ^4.3.3 ethereum-cryptography: 0.1.3 ethers: ^5.7.1 js-sdsl: ^4.1.4 - checksum: 157b503fa3e45a3695ba2eba5b089b56719f7790274edd09c95bb0d223570820127f6a2cbfcb14f2d9d876d1440ea4dccb04a4922fa9e9e34b416fddd6517c20 + checksum: 3ab6578e252e53609afd98d8ba42a99f182dcf80252f23ed9a5e0471023ffb2502130f85fc47fa7c94cd149f9be799ed9a0942ca52a143405be9267f4ad94e64 languageName: node linkType: hard -"@nomicfoundation/ethereumjs-trie@npm:6.0.1": - version: 6.0.1 - resolution: "@nomicfoundation/ethereumjs-trie@npm:6.0.1" +"@nomicfoundation/ethereumjs-trie@npm:6.0.2": + version: 6.0.2 + resolution: "@nomicfoundation/ethereumjs-trie@npm:6.0.2" dependencies: - "@nomicfoundation/ethereumjs-rlp": 5.0.1 - "@nomicfoundation/ethereumjs-util": 9.0.1 + "@nomicfoundation/ethereumjs-rlp": 5.0.2 + "@nomicfoundation/ethereumjs-util": 9.0.2 "@types/readable-stream": ^2.3.13 ethereum-cryptography: 0.1.3 readable-stream: ^3.6.0 - checksum: 7001c3204120fd4baba673b4bb52015594f5ad28311f24574cd16f38c015ef87ed51188d6f46d6362ffb9da589359a9e0f99e6068ef7a2f61cb66213e2f493d7 + checksum: d4da918d333851b9f2cce7dbd25ab5753e0accd43d562d98fd991b168b6a08d1794528f0ade40fe5617c84900378376fe6256cdbe52c8d66bf4c53293bbc7c40 languageName: node linkType: hard -"@nomicfoundation/ethereumjs-tx@npm:5.0.1": - version: 5.0.1 - resolution: "@nomicfoundation/ethereumjs-tx@npm:5.0.1" +"@nomicfoundation/ethereumjs-tx@npm:5.0.2": + version: 5.0.2 + resolution: "@nomicfoundation/ethereumjs-tx@npm:5.0.2" dependencies: "@chainsafe/ssz": ^0.9.2 "@ethersproject/providers": ^5.7.2 - "@nomicfoundation/ethereumjs-common": 4.0.1 - "@nomicfoundation/ethereumjs-rlp": 5.0.1 - "@nomicfoundation/ethereumjs-util": 9.0.1 + "@nomicfoundation/ethereumjs-common": 4.0.2 + "@nomicfoundation/ethereumjs-rlp": 5.0.2 + "@nomicfoundation/ethereumjs-util": 9.0.2 ethereum-cryptography: 0.1.3 - checksum: aa3829e4a43f5e10cfd66b87eacb3e737ba98f5e3755a3e6a4ccfbc257dbf10d926838cc3acb8fef8afa3362a023b7fd11b53e6ba53f94bb09c345f083cd29a8 + checksum: 0bbcea75786b2ccb559afe2ecc9866fb4566a9f157b6ffba4f50960d14f4b3da2e86e273f6fadda9b860e67cfcabf589970fb951b328cb5f900a585cd21842a2 languageName: node linkType: hard -"@nomicfoundation/ethereumjs-util@npm:9.0.1": - version: 9.0.1 - resolution: "@nomicfoundation/ethereumjs-util@npm:9.0.1" +"@nomicfoundation/ethereumjs-util@npm:9.0.2": + version: 9.0.2 + resolution: "@nomicfoundation/ethereumjs-util@npm:9.0.2" dependencies: "@chainsafe/ssz": ^0.10.0 - "@nomicfoundation/ethereumjs-rlp": 5.0.1 + "@nomicfoundation/ethereumjs-rlp": 5.0.2 ethereum-cryptography: 0.1.3 - checksum: 5f8a50a25c68c974b717f36ad0a5828b786ce1aaea3c874663c2014593fa387de5ad5c8cea35e94379df306dbd1a58c55b310779fd82197dcb993d5dbd4de7a1 + checksum: 3a08f7b88079ef9f53b43da9bdcb8195498fd3d3911c2feee2571f4d1204656053f058b2f650471c86f7d2d0ba2f814768c7cfb0f266eede41c848356afc4900 languageName: node linkType: hard -"@nomicfoundation/ethereumjs-vm@npm:7.0.1": - version: 7.0.1 - resolution: "@nomicfoundation/ethereumjs-vm@npm:7.0.1" - dependencies: - "@nomicfoundation/ethereumjs-block": 5.0.1 - "@nomicfoundation/ethereumjs-blockchain": 7.0.1 - "@nomicfoundation/ethereumjs-common": 4.0.1 - "@nomicfoundation/ethereumjs-evm": 2.0.1 - "@nomicfoundation/ethereumjs-rlp": 5.0.1 - "@nomicfoundation/ethereumjs-statemanager": 2.0.1 - "@nomicfoundation/ethereumjs-trie": 6.0.1 - "@nomicfoundation/ethereumjs-tx": 5.0.1 - "@nomicfoundation/ethereumjs-util": 9.0.1 +"@nomicfoundation/ethereumjs-vm@npm:7.0.2": + version: 7.0.2 + resolution: "@nomicfoundation/ethereumjs-vm@npm:7.0.2" + dependencies: + "@nomicfoundation/ethereumjs-block": 5.0.2 + "@nomicfoundation/ethereumjs-blockchain": 7.0.2 + "@nomicfoundation/ethereumjs-common": 4.0.2 + "@nomicfoundation/ethereumjs-evm": 2.0.2 + "@nomicfoundation/ethereumjs-rlp": 5.0.2 + "@nomicfoundation/ethereumjs-statemanager": 2.0.2 + "@nomicfoundation/ethereumjs-trie": 6.0.2 + "@nomicfoundation/ethereumjs-tx": 5.0.2 + "@nomicfoundation/ethereumjs-util": 9.0.2 debug: ^4.3.3 ethereum-cryptography: 0.1.3 mcl-wasm: ^0.7.1 rustbn.js: ~0.2.0 - checksum: 0f637316322744140d6f75d894c21b8055e27a94c72dd8ae9b0b9b93c0d54d7f30fa2aaf909e802e183a3f1020b4aa6a8178dedb823a4ce70a227ac7b432f8c1 + checksum: 1c25ba4d0644cadb8a2b0241a4bb02e578bfd7f70e3492b855c2ab5c120cb159cb8f7486f84dc1597884bd1697feedbfb5feb66e91352afb51f3694fd8e4a043 languageName: node linkType: hard @@ -18099,26 +18100,25 @@ __metadata: linkType: hard "hardhat@npm:^2.15.0": - version: 2.15.0 - resolution: "hardhat@npm:2.15.0" + version: 2.18.2 + resolution: "hardhat@npm:2.18.2" dependencies: "@ethersproject/abi": ^5.1.2 "@metamask/eth-sig-util": ^4.0.0 - "@nomicfoundation/ethereumjs-block": 5.0.1 - "@nomicfoundation/ethereumjs-blockchain": 7.0.1 - "@nomicfoundation/ethereumjs-common": 4.0.1 - "@nomicfoundation/ethereumjs-evm": 2.0.1 - "@nomicfoundation/ethereumjs-rlp": 5.0.1 - "@nomicfoundation/ethereumjs-statemanager": 2.0.1 - "@nomicfoundation/ethereumjs-trie": 6.0.1 - "@nomicfoundation/ethereumjs-tx": 5.0.1 - "@nomicfoundation/ethereumjs-util": 9.0.1 - "@nomicfoundation/ethereumjs-vm": 7.0.1 + "@nomicfoundation/ethereumjs-block": 5.0.2 + "@nomicfoundation/ethereumjs-blockchain": 7.0.2 + "@nomicfoundation/ethereumjs-common": 4.0.2 + "@nomicfoundation/ethereumjs-evm": 2.0.2 + "@nomicfoundation/ethereumjs-rlp": 5.0.2 + "@nomicfoundation/ethereumjs-statemanager": 2.0.2 + "@nomicfoundation/ethereumjs-trie": 6.0.2 + "@nomicfoundation/ethereumjs-tx": 5.0.2 + "@nomicfoundation/ethereumjs-util": 9.0.2 + "@nomicfoundation/ethereumjs-vm": 7.0.2 "@nomicfoundation/solidity-analyzer": ^0.1.0 "@sentry/node": ^5.18.1 "@types/bn.js": ^5.1.0 "@types/lru-cache": ^5.1.0 - abort-controller: ^3.0.0 adm-zip: ^0.4.16 aggregate-error: ^3.0.0 ansi-escapes: ^4.3.0 @@ -18141,7 +18141,6 @@ __metadata: mnemonist: ^0.38.0 mocha: ^10.0.0 p-map: ^4.0.0 - qs: ^6.7.0 raw-body: ^2.4.1 resolve: 1.17.0 semver: ^6.3.0 @@ -18162,7 +18161,7 @@ __metadata: optional: true bin: hardhat: internal/cli/bootstrap.js - checksum: 46767f0eb75f08e1f47585d3aec3261932251b47909051bfffcbff317f7efe06fdab7cb8686cb67c46cc7ed4cedb80d0c21157fe03f103054001b2762085ef92 + checksum: b234ec9c4030ee91c0c91b78acbebf7fd6354ea46c7a5d1ced245b2e0e6045996d130944e6d2d5b2139fb53895a15c5eb054ab76571f9835e2021d9f70beccbe languageName: node linkType: hard @@ -22389,6 +22388,15 @@ __metadata: languageName: node linkType: hard +"matchstick-as@npm:0.6.0-beta.2": + version: 0.6.0-beta.2 + resolution: "matchstick-as@npm:0.6.0-beta.2" + dependencies: + wabt: 1.0.24 + checksum: d3a72d9c35efd0388eabe24ed4726cf3d522769ba7beca3696baa8e0eaebd05b28d27a07ee8687eec543862a12b796982ec8d44d7cb5a15484d99bb94413f4cd + languageName: node + linkType: hard + "mcl-wasm@npm:^0.7.1": version: 0.7.9 resolution: "mcl-wasm@npm:0.7.9" @@ -26162,7 +26170,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.3, qs@npm:^6.4.0, qs@npm:^6.7.0, qs@npm:^6.9.4": +"qs@npm:^6.10.3, qs@npm:^6.4.0, qs@npm:^6.9.4": version: 6.11.2 resolution: "qs@npm:6.11.2" dependencies: @@ -31126,6 +31134,23 @@ __metadata: languageName: node linkType: hard +"wabt@npm:1.0.24": + version: 1.0.24 + resolution: "wabt@npm:1.0.24" + bin: + wasm-decompile: bin/wasm-decompile + wasm-interp: bin/wasm-interp + wasm-objdump: bin/wasm-objdump + wasm-opcodecnt: bin/wasm-opcodecnt + wasm-strip: bin/wasm-strip + wasm-validate: bin/wasm-validate + wasm2c: bin/wasm2c + wasm2wat: bin/wasm2wat + wat2wasm: bin/wat2wasm + checksum: 7d404acaa0605b5cde99585839b97db37998754512d89e1c63147bc62f4bfd46484595cc8f2e8d6f9741f67206ed936336a0828a3e1e6f3d348567cda6176333 + languageName: node + linkType: hard + "wagmi@npm:^1.4.3": version: 1.4.3 resolution: "wagmi@npm:1.4.3"