diff --git a/bot-pinner/README.md b/bot-pinner/README.md index 76704e415..7f0edf6ec 100644 --- a/bot-pinner/README.md +++ b/bot-pinner/README.md @@ -17,6 +17,7 @@ $ docker-compose up -d 2. Evidence container that awaits new events and then scrapes the latest hashes and submits it to IPFS. 3. `src/peers.txt` contains a list of peers, by default it will add connects to Estuary & Kleros IPFS nodes. This should make it easier to find content by creating a data network around Kleros Court V2. 1. Adding these peers will make it easier to find and replicate content. + ## DappNode :warning: For the following steps, you need access to [a DappNode](https://dappnode.io) system with the IPFS service running. diff --git a/contracts/scripts/simulations/tasks.ts b/contracts/scripts/simulations/tasks.ts index e883b55b0..b41899664 100644 --- a/contracts/scripts/simulations/tasks.ts +++ b/contracts/scripts/simulations/tasks.ts @@ -268,7 +268,7 @@ task("simulate:cast-commit", "Casts a commit for a drawn juror") const tx = await disputeKitClassic.connect(wallet).castCommit(...castCommitFunctionArgs); await tx.wait(); - console.log("juror %s casted a commit on txID: %s", wallet.address, tx?.hash); + console.log("juror %s cast a commit on txID: %s", wallet.address, tx?.hash); }); task("simulate:cast-vote", "Casts a vote for a drawn juror") @@ -295,7 +295,7 @@ task("simulate:cast-vote", "Casts a vote for a drawn juror") const tx = await disputeKitClassic.connect(wallet).castVote(...castVoteFunctionArgs); await tx.wait(); - console.log("juror %s casted a vote on txID: %s", wallet.address, tx?.hash); + console.log("juror %s cast a vote on txID: %s", wallet.address, tx?.hash); }); task("simulate:fund-appeal", "Funds an appeal on a dispute") diff --git a/subgraph/schema.graphql b/subgraph/schema.graphql index 135a9d4a0..5aecfcb11 100644 --- a/subgraph/schema.graphql +++ b/subgraph/schema.graphql @@ -96,8 +96,11 @@ type TokenAndETHShift @entity { id: ID! # user.id-dispute.id juror: User! dispute: Dispute! - tokenAmount: BigInt! + pnkAmount: BigInt! ethAmount: BigInt! + isNativeCurrency: Boolean! + feeTokenAmount: BigInt! + feeToken: FeeToken } type JurorTokensPerCourt @entity { @@ -159,6 +162,7 @@ type Round @entity { penalties: BigInt! drawnJurors: [Draw!]! @derivedFrom(field: "round") dispute: Dispute! + feeToken: FeeToken } type Draw @entity { @@ -191,6 +195,17 @@ type Counter @entity { casesRuled: BigInt! } +type FeeToken @entity { + id: ID! # The address of the ERC20 token. + accepted: Boolean! + rateInEth: BigInt! + rateDecimals: Int! + totalPaid: BigInt! + totalPaidInETH: BigInt! + rounds: [Round!] @derivedFrom(field: "feeToken") + tokenAndETHShift: [TokenAndETHShift!] @derivedFrom(field: "feeToken") +} + ##################### # ClassicDisputeKit # ##################### diff --git a/subgraph/src/DisputeKitClassic.ts b/subgraph/src/DisputeKitClassic.ts index 82974fa0f..bdddb3e8b 100644 --- a/subgraph/src/DisputeKitClassic.ts +++ b/subgraph/src/DisputeKitClassic.ts @@ -47,13 +47,17 @@ export function handleEvidenceEvent(event: EvidenceEvent): void { } export function handleJustificationEvent(event: JustificationEvent): void { - const coreDisputeID = event.params._coreDisputeID.toString(); - const coreDispute = Dispute.load(coreDisputeID); + const contract = DisputeKitClassic.bind(event.address); + const coreDisputeID = event.params._coreDisputeID; + const coreDispute = Dispute.load(coreDisputeID.toString()); const classicDisputeID = `${DISPUTEKIT_ID}-${coreDisputeID}`; const classicDispute = ClassicDispute.load(classicDisputeID); if (!classicDispute || !coreDispute) return; + const choice = event.params._choice; + const coreRoundIndex = coreDispute.currentRoundIndex; + const roundInfo = contract.getRoundInfo(coreDisputeID, coreRoundIndex, choice); const currentLocalRoundID = classicDispute.id + "-" + classicDispute.currentLocalRoundIndex.toString(); - const currentRulingInfo = updateCountsAndGetCurrentRuling(currentLocalRoundID, event.params._choice); + const currentRulingInfo = updateCountsAndGetCurrentRuling(currentLocalRoundID, choice, roundInfo.getChoiceCount()); coreDispute.currentRuling = currentRulingInfo.ruling; coreDispute.tied = currentRulingInfo.tied; coreDispute.save(); diff --git a/subgraph/src/KlerosCore.ts b/subgraph/src/KlerosCore.ts index 28be0340c..e2c30e895 100644 --- a/subgraph/src/KlerosCore.ts +++ b/subgraph/src/KlerosCore.ts @@ -12,21 +12,23 @@ import { TokenAndETHShift as TokenAndETHShiftEvent, Ruling, StakeDelayed, + AcceptedFeeToken, } from "../generated/KlerosCore/KlerosCore"; import { ZERO, ONE } from "./utils"; import { createCourtFromEvent, getFeeForJuror } from "./entities/Court"; import { createDisputeKitFromEvent, filterSupportedDisputeKits } from "./entities/DisputeKit"; import { createDisputeFromEvent } from "./entities/Dispute"; import { createRoundFromRoundInfo } from "./entities/Round"; -import { updateCases, updatePaidETH, updateRedistributedPNK, updateCasesRuled, updateCasesVoting } from "./datapoint"; +import { updateCases, updateCasesRuled, updateCasesVoting } from "./datapoint"; import { addUserActiveDispute, ensureUser } from "./entities/User"; import { updateJurorDelayedStake, updateJurorStake } from "./entities/JurorTokensPerCourt"; import { createDrawFromEvent } from "./entities/Draw"; import { updateTokenAndEthShiftFromEvent } from "./entities/TokenAndEthShift"; import { updateArbitrableCases } from "./entities/Arbitrable"; -import { Court, Dispute } from "../generated/schema"; +import { Court, Dispute, FeeToken } from "../generated/schema"; import { BigInt } from "@graphprotocol/graph-ts"; import { updatePenalty } from "./entities/Penalty"; +import { ensureFeeToken } from "./entities/FeeToken"; function getPeriodName(index: i32): string { const periodArray = ["evidence", "commit", "vote", "appeal", "execution"]; @@ -156,22 +158,17 @@ export function handleStakeDelayed(event: StakeDelayed): void { } export function handleTokenAndETHShift(event: TokenAndETHShiftEvent): void { + updatePenalty(event); updateTokenAndEthShiftFromEvent(event); const jurorAddress = event.params._account.toHexString(); const disputeID = event.params._disputeID.toString(); - const pnkAmount = event.params._pnkAmount; - const feeAmount = event.params._feeAmount; const dispute = Dispute.load(disputeID); if (!dispute) return; const court = Court.load(dispute.court); if (!court) return; updateJurorStake(jurorAddress, court.id, KlerosCore.bind(event.address), event.block.timestamp); - court.paidETH = court.paidETH.plus(feeAmount); - if (pnkAmount.gt(ZERO)) { - court.paidPNK = court.paidPNK.plus(pnkAmount); - updateRedistributedPNK(pnkAmount, event.block.timestamp); - } - updatePaidETH(feeAmount, event.block.timestamp); - updatePenalty(event); - court.save(); +} + +export function handleAcceptedFeeToken(event: AcceptedFeeToken): void { + ensureFeeToken(event.params._token, event.address); } diff --git a/subgraph/src/entities/ClassicRound.ts b/subgraph/src/entities/ClassicRound.ts index d43564596..8b28aae7e 100644 --- a/subgraph/src/entities/ClassicRound.ts +++ b/subgraph/src/entities/ClassicRound.ts @@ -25,15 +25,15 @@ class CurrentRulingInfo { tied: boolean; } -export function updateCountsAndGetCurrentRuling(id: string, choice: BigInt): CurrentRulingInfo { +export function updateCountsAndGetCurrentRuling(id: string, choice: BigInt, choiceCount: BigInt): CurrentRulingInfo { const round = ClassicRound.load(id); if (!round) return { ruling: ZERO, tied: false }; const choiceNum = choice.toI32(); - const updatedCount = round.counts[choiceNum].plus(ONE); + const delta = choiceCount.minus(round.counts[choiceNum]); let newCounts: BigInt[] = []; for (let i = 0; i < round.counts.length; i++) { if (BigInt.fromI32(i).equals(choice)) { - newCounts.push(round.counts[i].plus(ONE)); + newCounts.push(choiceCount); } else { newCounts.push(round.counts[i]); } @@ -43,14 +43,14 @@ export function updateCountsAndGetCurrentRuling(id: string, choice: BigInt): Cur if (choice.equals(round.winningChoice)) { if (round.tied) round.tied = false; } else { - if (updatedCount.equals(currentWinningCount)) { + if (choiceCount.equals(currentWinningCount)) { if (!round.tied) round.tied = true; - } else if (updatedCount.gt(currentWinningCount)) { + } else if (choiceCount.gt(currentWinningCount)) { round.winningChoice = choice; round.tied = false; } } - round.totalVoted = round.totalVoted.plus(ONE); + round.totalVoted = round.totalVoted.plus(delta); round.save(); return { ruling: round.winningChoice, tied: round.tied }; } diff --git a/subgraph/src/entities/FeeToken.ts b/subgraph/src/entities/FeeToken.ts new file mode 100644 index 000000000..18cccd867 --- /dev/null +++ b/subgraph/src/entities/FeeToken.ts @@ -0,0 +1,53 @@ +import { BigInt, Address } from "@graphprotocol/graph-ts"; +import { FeeToken } from "../../generated/schema"; +import { KlerosCore } from "../../generated/KlerosCore/KlerosCore"; +import { ZERO } from "../utils"; + +export function ensureFeeToken(tokenAddress: Address, klerosCoreAddress: Address): FeeToken { + const hexTokenAddress = tokenAddress.toHexString(); + let feeToken = FeeToken.load(hexTokenAddress); + if (!feeToken) { + feeToken = new FeeToken(hexTokenAddress); + feeToken.totalPaid = ZERO; + feeToken.totalPaidInETH = ZERO; + } + const contract = KlerosCore.bind(klerosCoreAddress); + const currencyRate = contract.currencyRates(tokenAddress); + feeToken.accepted = currencyRate.value0; + feeToken.rateInEth = currencyRate.value1; + feeToken.rateDecimals = currencyRate.value2; + feeToken.save(); + return feeToken; +} + +export function updateFeeTokenRate(tokenAddress: Address, klerosCoreAddress: Address): void { + const feeToken = ensureFeeToken(tokenAddress, klerosCoreAddress); + const contract = KlerosCore.bind(klerosCoreAddress); + const currencyRate = contract.currencyRates(tokenAddress); + feeToken.accepted = currencyRate.value0; + feeToken.rateInEth = currencyRate.value1; + feeToken.rateDecimals = currencyRate.value2; + feeToken.save(); +} + +export function updateFeeTokenPaid(tokenAddress: Address, klerosCoreAddress: Address, amount: BigInt): void { + const feeToken = ensureFeeToken(tokenAddress, klerosCoreAddress); + const ethAmount = convertTokenAmountToEth(tokenAddress, amount, klerosCoreAddress); + feeToken.totalPaid = feeToken.totalPaid.plus(amount); + feeToken.totalPaidInETH = feeToken.totalPaidInETH.plus(ethAmount); + feeToken.save(); +} + +export function convertEthToTokenAmount(tokenAddress: Address, eth: BigInt, klerosCoreAddress: Address): BigInt { + const feeToken = ensureFeeToken(tokenAddress, klerosCoreAddress); + return eth.times(BigInt.fromI32(10 ** feeToken.rateDecimals)).div(feeToken.rateInEth); +} + +export function convertTokenAmountToEth( + tokenAddress: Address, + tokenAmount: BigInt, + klerosCoreAddress: Address +): BigInt { + const feeToken = ensureFeeToken(tokenAddress, klerosCoreAddress); + return tokenAmount.times(feeToken.rateInEth).div(BigInt.fromI32(10 ** feeToken.rateDecimals)); +} diff --git a/subgraph/src/entities/Round.ts b/subgraph/src/entities/Round.ts index bb369685e..320f5cf57 100644 --- a/subgraph/src/entities/Round.ts +++ b/subgraph/src/entities/Round.ts @@ -9,6 +9,7 @@ export function createRoundFromRoundInfo( ): void { const roundID = `${disputeID.toString()}-${roundIndex.toString()}`; const round = new Round(roundID); + const feeToken = roundInfo.getFeeToken(); round.disputeKit = roundInfo.getDisputeKitID.toString(); round.tokensAtStakePerJuror = roundInfo.getPnkAtStakePerJuror(); round.totalFeesForJurors = roundInfo.getTotalFeesForJurors(); @@ -16,5 +17,7 @@ export function createRoundFromRoundInfo( round.repartitions = roundInfo.getRepartitions(); round.penalties = roundInfo.getPnkPenalties(); round.dispute = disputeID.toString(); + round.feeToken = + feeToken.toHexString() === "0x0000000000000000000000000000000000000000" ? null : feeToken.toHexString(); round.save(); } diff --git a/subgraph/src/entities/TokenAndEthShift.ts b/subgraph/src/entities/TokenAndEthShift.ts index c0252d67c..5fca5dec4 100644 --- a/subgraph/src/entities/TokenAndEthShift.ts +++ b/subgraph/src/entities/TokenAndEthShift.ts @@ -1,36 +1,68 @@ +import { Address, BigInt } from "@graphprotocol/graph-ts"; import { TokenAndETHShift as TokenAndETHShiftEvent } from "../../generated/KlerosCore/KlerosCore"; -import { TokenAndETHShift } from "../../generated/schema"; +import { Court, Dispute, TokenAndETHShift } from "../../generated/schema"; +import { updatePaidETH, updateRedistributedPNK } from "../datapoint"; import { ZERO } from "../utils"; +import { convertTokenAmountToEth, updateFeeTokenPaid } from "./FeeToken"; import { resolveUserDispute } from "./User"; export function updateTokenAndEthShiftFromEvent(event: TokenAndETHShiftEvent): void { - const jurorAddress = event.params._account.toHexString(); - const disputeID = event.params._disputeID.toString(); - const shiftID = `${jurorAddress}-${disputeID}`; - const shift = TokenAndETHShift.load(shiftID); - - if (!shift) { - createTokenAndEthShiftFromEvent(event); - resolveUserDispute(jurorAddress, ZERO, event.params._feeAmount, disputeID); - return; + const jurorAddress = event.params._account; + const disputeID = event.params._disputeID; + const dispute = Dispute.load(disputeID.toString()); + if (!dispute) return; + const court = Court.load(dispute.court); + if (!court) return; + const roundIndex = event.params._roundID; + const feeTokenAddress = event.params._feeToken; + let shift = ensureTokenAndEthShift(jurorAddress, disputeID, roundIndex, feeTokenAddress); + const feeAmount = event.params._feeAmount; + const pnkAmount = event.params._pnkAmount; + let ethAmount: BigInt; + if (feeTokenAddress.toHexString() === "0x0000000000000000000000000000000000000000") { + updateFeeTokenPaid(feeTokenAddress, event.address, feeAmount); + ethAmount = convertTokenAmountToEth(feeTokenAddress, feeAmount, event.address); + shift.feeTokenAmount = shift.feeTokenAmount.plus(feeAmount); + } else { + ethAmount = feeAmount; } - - shift.tokenAmount = shift.tokenAmount.plus(event.params._pnkAmount); - const previousFeeAmount = shift.ethAmount; - const newFeeAmount = shift.ethAmount.plus(event.params._feeAmount); - shift.ethAmount = newFeeAmount; + const previousEthAmount = shift.ethAmount; + const newEthAmount = previousEthAmount.plus(ethAmount); + shift.ethAmount = newEthAmount; + resolveUserDispute(jurorAddress.toHexString(), previousEthAmount, newEthAmount, disputeID.toString()); + court.paidETH = court.paidETH.plus(ethAmount); + updatePaidETH(ethAmount, event.block.timestamp); + if (pnkAmount.gt(ZERO)) { + court.paidPNK = court.paidPNK.plus(pnkAmount); + updateRedistributedPNK(pnkAmount, event.block.timestamp); + } + shift.pnkAmount = shift.pnkAmount.plus(pnkAmount); shift.save(); - resolveUserDispute(jurorAddress, previousFeeAmount, newFeeAmount, disputeID); + court.save(); } -export function createTokenAndEthShiftFromEvent(event: TokenAndETHShiftEvent): void { - const jurorAddress = event.params._account.toHexString(); - const disputeID = event.params._disputeID.toString(); - const shiftID = `${jurorAddress}-${disputeID}`; - const shift = new TokenAndETHShift(shiftID); - shift.juror = jurorAddress; - shift.dispute = disputeID; - shift.tokenAmount = event.params._pnkAmount; - shift.ethAmount = event.params._feeAmount; - shift.save(); +export function ensureTokenAndEthShift( + jurorAddress: Address, + disputeID: BigInt, + roundIndex: BigInt, + feeTokenAddress: Address +): TokenAndETHShift { + const shiftID = `${jurorAddress.toHexString()}-${disputeID.toString()}-${roundIndex.toString()}`; + let shift = TokenAndETHShift.load(shiftID); + if (!shift) { + shift = new TokenAndETHShift(shiftID); + if (feeTokenAddress !== Address.fromI32(0)) { + shift.isNativeCurrency = false; + shift.feeToken = feeTokenAddress.toHexString(); + } else { + shift.isNativeCurrency = true; + } + shift.feeTokenAmount = ZERO; + shift.ethAmount = ZERO; + shift.juror = jurorAddress.toHexString(); + shift.dispute = disputeID.toString(); + shift.pnkAmount = ZERO; + shift.save(); + } + return shift; } diff --git a/subgraph/subgraph.yaml b/subgraph/subgraph.yaml index d69b93290..3aa625bfe 100644 --- a/subgraph/subgraph.yaml +++ b/subgraph/subgraph.yaml @@ -54,6 +54,8 @@ dataSources: handler: handleTokenAndETHShift - event: Ruling(indexed address,indexed uint256,uint256) handler: handleRuling + - event: AcceptedFeeToken(indexed address,indexed bool) + handler: handleAcceptedFeeToken file: ./src/KlerosCore.ts - kind: ethereum name: PolicyRegistry diff --git a/web/.parcelrc b/web/.parcelrc index b812a5214..8b6b0a40c 100644 --- a/web/.parcelrc +++ b/web/.parcelrc @@ -1,6 +1,8 @@ { "extends": "@parcel/config-default", "transformers": { - "*.svg": ["...", "@parcel/transformer-svg-react"] + "*.svg": ["...", "@parcel/transformer-svg-react"], + "tsx:*.svg": ["@parcel/transformer-svg-react"], + "tsx:*": ["..."] } } diff --git a/web/package.json b/web/package.json index 428cda609..7c30df9f3 100644 --- a/web/package.json +++ b/web/package.json @@ -23,8 +23,7 @@ }, "packageManager": "yarn@3.3.1", "scripts": { - "clear": "rm -r .parcel-cache", - "clean": "rm dist/bundle.js", + "clear": "rm -fr ../.parcel-cache dist/bundle.js", "start": "parcel", "start-local": "REACT_APP_SUBGRAPH_ENDPOINT=http://localhost:8000/subgraphs/name/kleros/kleros-v2-core-local parcel", "build": "yarn generate && yarn parcel build", diff --git a/web/src/assets/svgs/icons/appeal.png b/web/src/assets/svgs/icons/appeal.png new file mode 100644 index 000000000..d0faae682 Binary files /dev/null and b/web/src/assets/svgs/icons/appeal.png differ diff --git a/web/src/assets/svgs/icons/appeal.svg b/web/src/assets/svgs/icons/appeal.svg new file mode 100644 index 000000000..a5afa1e66 --- /dev/null +++ b/web/src/assets/svgs/icons/appeal.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/src/assets/svgs/icons/balance.svg b/web/src/assets/svgs/icons/balance.svg new file mode 100644 index 000000000..25c5a37f9 --- /dev/null +++ b/web/src/assets/svgs/icons/balance.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/assets/svgs/icons/info-circle.svg b/web/src/assets/svgs/icons/info-circle.svg new file mode 100644 index 000000000..bcb80ff1e --- /dev/null +++ b/web/src/assets/svgs/icons/info-circle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/src/assets/svgs/icons/kleros.svg b/web/src/assets/svgs/icons/kleros.svg index 532aaf497..e0dc1a0fd 100644 --- a/web/src/assets/svgs/icons/kleros.svg +++ b/web/src/assets/svgs/icons/kleros.svg @@ -1,3 +1,3 @@ - + diff --git a/web/src/assets/svgs/icons/voted.svg b/web/src/assets/svgs/icons/voted.svg new file mode 100644 index 000000000..d43f24ad6 --- /dev/null +++ b/web/src/assets/svgs/icons/voted.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/src/components/DisputeCard/index.tsx b/web/src/components/DisputeCard/index.tsx index c6ba8f6cb..5470894c3 100644 --- a/web/src/components/DisputeCard/index.tsx +++ b/web/src/components/DisputeCard/index.tsx @@ -34,7 +34,11 @@ const Container = styled.div` } `; -const getPeriodEndTimestamp = (lastPeriodChange: string, currentPeriodIndex: number, timesPerPeriod: string[]) => { +export const getPeriodEndTimestamp = ( + lastPeriodChange: string, + currentPeriodIndex: number, + timesPerPeriod: string[] +) => { const durationCurrentPeriod = parseInt(timesPerPeriod[currentPeriodIndex]); return parseInt(lastPeriodChange) + durationCurrentPeriod; }; diff --git a/web/src/components/GradientTokenIcons.tsx b/web/src/components/GradientTokenIcons.tsx new file mode 100644 index 000000000..f79454b32 --- /dev/null +++ b/web/src/components/GradientTokenIcons.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import styled, { css } from "styled-components"; + +const LinearGradientPath = styled.path<{ gradient: string }>` + ${({ gradient }) => + gradient && + css` + stroke: url(#${gradient}); + `} +`; + +interface IGradientTokenIcons { + icon: string; +} + +const GradientTokenIcons: React.FC = ({ icon }) => { + return ( + <> + {icon === "ETH" ? ( + + + + + + + + + + ) : ( + + + + + + + + + + )} + + ); +}; +export default GradientTokenIcons; diff --git a/web/src/components/Popup/Description/Appeal.tsx b/web/src/components/Popup/Description/Appeal.tsx new file mode 100644 index 000000000..e994b96c1 --- /dev/null +++ b/web/src/components/Popup/Description/Appeal.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledAmountFunded = styled.div` + display: flex; + margin-left: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; + text-align: center; +`; + +const StyledOptionFunded = styled.div` + display: flex; + margin-bottom: calc(16px + (32 - 16) * ((100vw - 300px) / (1250 - 300))); + margin-left: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; + text-align: center; +`; + +const AmountContainer = styled.div` + color: ${({ theme }) => theme.primaryText}; +`; + +const OptionContainer = styled.div` + color: ${({ theme }) => theme.primaryText}; +`; + +interface IAppeal { + amount: string; + option: string; +} + +const Appeal: React.FC = ({ amount, option }) => { + return ( + + + You have funded:  {amount} ETH + + + Option funded:  {option} + + + ); +}; +export default Appeal; diff --git a/web/src/components/Popup/Description/StakeWithdraw.tsx b/web/src/components/Popup/Description/StakeWithdraw.tsx new file mode 100644 index 000000000..960d817b8 --- /dev/null +++ b/web/src/components/Popup/Description/StakeWithdraw.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import styled from "styled-components"; +import { isUndefined } from "utils/index"; +import { useAccount } from "wagmi"; +import { useKlerosCoreGetJurorBalance } from "hooks/contracts/generated"; +import { format } from "src/pages/Dashboard/Courts/CourtCard"; +import KlerosLogo from "tsx:svgs/icons/kleros.svg"; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const StyledKlerosLogo = styled(KlerosLogo)` + width: 14px; + height: 14px; +`; + +const StyledTitle = styled.div` + display: flex; + margin-bottom: calc(16px + (32 - 16) * ((100vw - 300px) / (1250 - 300))); + margin-left: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; + text-align: center; +`; + +const AmountStakedOrWithdrawnContainer = styled.div` + font-size: 24px; + font-weight: 600; + color: ${({ theme }) => theme.secondaryPurple}; + margin-bottom: calc(0px + (4 - 0) * ((100vw - 300px) / (1250 - 300))); +`; + +const TotalStakeContainer = styled.div` + display: flex; + font-size: 14px; + align-items: center; + justify-content: center; + margin-bottom: calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); +`; + +const MyStakeContainer = styled.div` + display: flex; + margin: 0px calc(4px + (8 - 4) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; +`; + +const AmountContainer = styled.div` + font-weight: 600; + color: ${({ theme }) => theme.primaryText}; +`; + +interface IStakeWithdraw { + pnkStaked: string; + courtName: string; + isStake: boolean; + courtId: string; +} + +interface IAmountStakedOrWithdrawn { + pnkStaked: string; + isStake: boolean; +} + +const AmountStakedOrWithdrawn: React.FC = ({ pnkStaked, isStake }) => { + return isStake ?
+ {pnkStaked} PNK
:
- {pnkStaked} PNK
; +}; + +const StakeWithdraw: React.FC = ({ pnkStaked, courtName, isStake, courtId }) => { + const { address } = useAccount(); + + const { data: jurorBalance } = useKlerosCoreGetJurorBalance({ + enabled: !isUndefined(address) && !isUndefined(courtId), + args: [address, BigInt(courtId)], + watch: true, + }); + + return ( + + 🎉 Your stake in the {courtName} court was successful! 🎉 + + + + + + My Stake:{" "} + {`${format(jurorBalance?.[0])} PNK`} + + + ); +}; +export default StakeWithdraw; diff --git a/web/src/components/Popup/Description/VoteWithCommit.tsx b/web/src/components/Popup/Description/VoteWithCommit.tsx new file mode 100644 index 000000000..789840385 --- /dev/null +++ b/web/src/components/Popup/Description/VoteWithCommit.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledDescription = styled.div` + margin-bottom: calc(16px + (32 - 16) * ((100vw - 300px) / (1250 - 300))); + margin-left: calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; + text-align: center; + line-height: 21.8px; +`; + +const EmphasizedDate = styled.span` + font-size: 16px; + font-weight: 400; + line-height: 21.8px; + color: ${({ theme }) => theme.primaryText}; +`; + +interface IVoteWithCommit { + date: string; +} + +const VoteWithCommit: React.FC = ({ date }) => { + return ( + + + Your vote is confirmed. It's kept secret until all jurors have cast their votes. + You'll need to justify and reveal your vote on {date} + + + ); +}; +export default VoteWithCommit; diff --git a/web/src/components/Popup/Description/VoteWithoutCommit.tsx b/web/src/components/Popup/Description/VoteWithoutCommit.tsx new file mode 100644 index 000000000..a5b445130 --- /dev/null +++ b/web/src/components/Popup/Description/VoteWithoutCommit.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledDescription = styled.div` + margin-bottom: calc(16px + (32 - 16) * ((100vw - 300px) / (1250 - 300))); + margin-left: calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; + text-align: center; + line-height: 21.8px; +`; + +const EmphasizedDate = styled.span` + font-size: 16px; + font-weight: 400; + line-height: 21.8px; + color: ${({ theme }) => theme.primaryText}; +`; + +interface IVoteWithoutCommit { + date: string; +} + +const VoteWithoutCommit: React.FC = ({ date }) => { + return ( + + + The decision date is {date} with the possibility for appeals. After that time + you will be informed about the jury decision. + + + ); +}; +export default VoteWithoutCommit; diff --git a/web/src/components/Popup/ExtraInfo/StakeWithdrawExtraInfo.tsx b/web/src/components/Popup/ExtraInfo/StakeWithdrawExtraInfo.tsx new file mode 100644 index 000000000..5ca4b4169 --- /dev/null +++ b/web/src/components/Popup/ExtraInfo/StakeWithdrawExtraInfo.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + color: ${({ theme }) => theme.secondaryText}; + text-align: center; + margin-left: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-top: calc(8px + (24 - 8) * ((100vw - 300px) / (1250 - 300))); +`; + +const StakeWithdrawExtraInfo: React.FC = () => { + return ( + + { + "In order not to miss when you're drawn for cases, make sure to subscribe to notifications: Settings > Notifications" + } + + ); +}; +export default StakeWithdrawExtraInfo; diff --git a/web/src/components/Popup/ExtraInfo/VoteWithCommitExtraInfo.tsx b/web/src/components/Popup/ExtraInfo/VoteWithCommitExtraInfo.tsx new file mode 100644 index 000000000..199264b3d --- /dev/null +++ b/web/src/components/Popup/ExtraInfo/VoteWithCommitExtraInfo.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import styled from "styled-components"; +import InfoCircle from "tsx:svgs/icons/info-circle.svg"; + +const Container = styled.div` + display: flex; + gap: 8px; + text-align: center; + align-items: center; + margin: 0 calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-top: calc(8px + (24 - 8) * ((100vw - 300px) / (1250 - 300))); + small { + font-size: 14px; + font-weight: 400; + color: ${({ theme }) => theme.secondaryText}; + line-height: 19px; + } +`; + +const StyledInfoCircle = styled(InfoCircle)` + min-width: 16px; + min-height: 16px; +`; + +const VoteWithCommitExtraInfo: React.FC = () => { + return ( + + + Subscribe to receive notifications to be reminded when the reveal time comes. + + ); +}; +export default VoteWithCommitExtraInfo; diff --git a/web/src/components/Popup/index.tsx b/web/src/components/Popup/index.tsx new file mode 100644 index 000000000..f9433f1a2 --- /dev/null +++ b/web/src/components/Popup/index.tsx @@ -0,0 +1,171 @@ +import React, { useRef } from "react"; +import styled from "styled-components"; +import { Button } from "@kleros/ui-components-library"; +import { Overlay } from "components/Overlay"; +import StakeWithdraw from "./Description/StakeWithdraw"; +import VoteWithCommit from "./Description/VoteWithCommit"; +import VoteWithoutCommit from "./Description/VoteWithoutCommit"; +import Appeal from "./Description/Appeal"; +import VoteWithCommitExtraInfo from "./ExtraInfo/VoteWithCommitExtraInfo"; +import StakeWithdrawExtraInfo from "./ExtraInfo/StakeWithdrawExtraInfo"; + +export enum PopupType { + STAKE_WITHDRAW = "STAKE_WITHDRAW", + APPEAL = "APPEAL", + VOTE_WITHOUT_COMMIT = "VOTE_WITHOUT_COMMIT", + VOTE_WITH_COMMIT = "VOTE_WITH_COMMIT", +} + +interface IStakeWithdraw { + popupType: PopupType.STAKE_WITHDRAW; + pnkStaked: string; + courtName: string; + isStake: boolean; + courtId: string; +} + +interface IVoteWithoutCommit { + popupType: PopupType.VOTE_WITHOUT_COMMIT; + date: string; +} + +interface IVoteWithCommit { + popupType: PopupType.VOTE_WITH_COMMIT; + date: string; +} + +interface IAppeal { + popupType: PopupType.APPEAL; + amount: string; + option: string; +} +interface IPopup { + title: string; + icon: React.FC>; + popupType: PopupType; + setIsOpen: (val: boolean) => void; + setAmount?: (val: string) => void; + isCommit?: boolean; +} + +type PopupProps = IStakeWithdraw | IVoteWithoutCommit | IVoteWithCommit | IAppeal; + +const Header = styled.h1` + display: flex; + margin-top: calc(12px + (32 - 12) * ((100vw - 300px) / (1250 - 300))); + margin-bottom: calc(12px + (24 - 12) * ((100vw - 300px) / (1250 - 300))); + font-size: 24px; + font-weight: 600; + line-height: 32.68px; +`; + +const IconContainer = styled.div` + width: calc(150px + (350 - 150) * (100vw - 375px) / (1250 - 375)); + display: flex; + align-items: center; + justify-content: center; + + svg { + display: inline-block; + width: calc(150px + (350 - 150) * (100vw - 375px) / (1250 - 375)); + height: calc(150px + (350 - 150) * (100vw - 375px) / (1250 - 375)); + } +`; + +const StyledButton = styled(Button)` + margin: calc(16px + (32 - 16) * ((100vw - 300px) / (1250 - 300))); +`; + +const Container = styled.div` + display: flex; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + z-index: 10; + flex-direction: column; + align-items: center; + justify-content: center; + width: calc(300px + (600 - 300) * (100vw - 375px) / (1250 - 375)); + max-width: 600px; + border-radius: 3px; + border: 1px solid ${({ theme }) => theme.stroke}; + background-color: ${({ theme }) => theme.whiteBackground}; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.06); + + svg { + visibility: visible; + } +`; + +const Popup: React.FC = ({ + title, + icon: Icon, + popupType, + setIsOpen, + setAmount, + isCommit, + ...props +}) => { + const containerRef = useRef(null); + + const resetValue = () => { + if (setAmount) { + setAmount(""); + } + }; + + let PopupComponent: JSX.Element | null = null; + + switch (popupType) { + case PopupType.STAKE_WITHDRAW: { + const { pnkStaked, courtName, isStake, courtId } = props as IStakeWithdraw; + PopupComponent = ( + + ); + break; + } + case PopupType.VOTE_WITHOUT_COMMIT: { + const { date } = props as IVoteWithoutCommit; + PopupComponent = ; + break; + } + case PopupType.VOTE_WITH_COMMIT: { + const { date } = props as IVoteWithCommit; + PopupComponent = ; + break; + } + case PopupType.APPEAL: { + const { amount, option } = props as IAppeal; + PopupComponent = ; + break; + } + default: + break; + } + + return ( + <> + + +
{title}
+ {PopupComponent} + + + + {popupType === PopupType.STAKE_WITHDRAW && } + {popupType === PopupType.VOTE_WITH_COMMIT && } + { + setIsOpen(false); + resetValue(); + }} + /> +
+ + ); +}; +export default Popup; diff --git a/web/src/components/Verdict/DisputeTimeline.tsx b/web/src/components/Verdict/DisputeTimeline.tsx index 439abd8e2..d53b88520 100644 --- a/web/src/components/Verdict/DisputeTimeline.tsx +++ b/web/src/components/Verdict/DisputeTimeline.tsx @@ -3,12 +3,13 @@ import { useParams } from "react-router-dom"; import styled, { useTheme } from "styled-components"; import { _TimelineItem1, CustomTimeline } from "@kleros/ui-components-library"; import { Periods } from "consts/periods"; -import { useVotingHistory } from "queries/useVotingHistory"; -import { useDisputeTemplate } from "queries/useDisputeTemplate"; +import { ClassicRound } from "src/graphql/graphql"; import { DisputeDetailsQuery, useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery"; +import { useDisputeTemplate } from "queries/useDisputeTemplate"; +import { useVotingHistory } from "queries/useVotingHistory"; +import CalendarIcon from "assets/svgs/icons/calendar.svg"; import ClosedCaseIcon from "assets/svgs/icons/check-circle-outline.svg"; import AppealedCaseIcon from "assets/svgs/icons/close-circle.svg"; -import CalendarIcon from "assets/svgs/icons/calendar.svg"; const Container = styled.div` display: flex; @@ -59,31 +60,31 @@ const getCaseEventTimes = ( type TimelineItems = [_TimelineItem1, ..._TimelineItem1[]]; -const useItems = (disputeDetails?: DisputeDetailsQuery) => { - const { data: disputeTemplate } = useDisputeTemplate(); +const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string}`) => { const { id } = useParams(); const { data: votingHistory } = useVotingHistory(id); - const localRounds = votingHistory?.dispute?.disputeKitDispute?.localRounds; + const { data: disputeTemplate } = useDisputeTemplate(id, arbitrable); + const localRounds: ClassicRound[] = votingHistory?.dispute?.disputeKitDispute?.localRounds; const theme = useTheme(); return useMemo(() => { const dispute = disputeDetails?.dispute; if (dispute) { + const rulingOverride = dispute.overridden; + const parsedDisputeFinalRuling = parseInt(dispute.currentRuling); const currentPeriodIndex = Periods[dispute.period]; const lastPeriodChange = dispute.lastPeriodChange; const courtTimePeriods = dispute.court.timesPerPeriod; return localRounds?.reduce( (acc, { winningChoice }, index) => { - const parsedWinningChoice = parseInt(winningChoice); + const parsedRoundChoice = parseInt(winningChoice); const eventDate = getCaseEventTimes(lastPeriodChange, currentPeriodIndex, courtTimePeriods, false); - const icon = disputeDetails?.dispute?.ruled && index === localRounds.length - 1 ? ClosedCaseIcon : ""; + const icon = dispute.ruled && !rulingOverride && index === localRounds.length - 1 ? ClosedCaseIcon : ""; acc.push({ title: `Jury Decision - Round ${index + 1}`, party: - parsedWinningChoice !== 0 - ? disputeTemplate?.answers?.[parseInt(winningChoice) - 1].title - : "Refuse to Arbitrate", + parsedRoundChoice !== 0 ? disputeTemplate?.answers?.[parsedRoundChoice - 1].title : "Refuse to Arbitrate", subtitle: eventDate, rightSided: true, variant: theme.secondaryPurple, @@ -98,6 +99,17 @@ const useItems = (disputeDetails?: DisputeDetailsQuery) => { rightSided: true, Icon: AppealedCaseIcon, }); + } else if (rulingOverride && parsedDisputeFinalRuling !== parsedRoundChoice) { + acc.push({ + title: "Won by Appeal", + party: + parsedDisputeFinalRuling !== 0 + ? disputeTemplate?.answers?.[parsedDisputeFinalRuling - 1].title + : "Refuse to Arbitrate", + subtitle: eventDate, + rightSided: true, + Icon: ClosedCaseIcon, + }); } return acc; @@ -117,10 +129,14 @@ const useItems = (disputeDetails?: DisputeDetailsQuery) => { }, [disputeDetails, disputeTemplate, localRounds, theme]); }; -const DisputeTimeline: React.FC = () => { +interface IDisputeTimeline { + arbitrable?: `0x${string}`; +} + +const DisputeTimeline: React.FC = ({ arbitrable }) => { const { id } = useParams(); const { data: disputeDetails } = useDisputeDetailsQuery(id); - const items = useItems(disputeDetails); + const items = useItems(disputeDetails, arbitrable); return ( diff --git a/web/src/components/Verdict/FinalDecision.tsx b/web/src/components/Verdict/FinalDecision.tsx index eb0393fda..39a4bcb27 100644 --- a/web/src/components/Verdict/FinalDecision.tsx +++ b/web/src/components/Verdict/FinalDecision.tsx @@ -3,9 +3,9 @@ import { useNavigate, useParams } from "react-router-dom"; import styled from "styled-components"; import Identicon from "react-identicons"; import ArrowIcon from "assets/svgs/icons/arrow.svg"; -import { useDisputeTemplate } from "queries/useDisputeTemplate"; -import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery"; import { useKlerosCoreCurrentRuling } from "hooks/contracts/generated"; +import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery"; +import { useDisputeTemplate } from "queries/useDisputeTemplate"; import LightButton from "../LightButton"; import VerdictBanner from "./VerdictBanner"; @@ -75,9 +75,13 @@ const AnswerTitle = styled.h3` margin: 0; `; -const FinalDecision: React.FC = () => { +interface IFinalDecision { + arbitrable?: `0x${string}`; +} + +const FinalDecision: React.FC = ({ arbitrable }) => { const { id } = useParams(); - const { data: disputeTemplate } = useDisputeTemplate(id); + const { data: disputeTemplate } = useDisputeTemplate(id, arbitrable); const { data: disputeDetails } = useDisputeDetailsQuery(id); const ruled = disputeDetails?.dispute?.ruled ?? false; const navigate = useNavigate(); diff --git a/web/src/components/Verdict/index.tsx b/web/src/components/Verdict/index.tsx index 76f14bff4..565ec5837 100644 --- a/web/src/components/Verdict/index.tsx +++ b/web/src/components/Verdict/index.tsx @@ -9,11 +9,15 @@ const Container = styled.div` gap: 24px; `; -const Verdict: React.FC = () => { +interface IVerdict { + arbitrable?: `0x${string}`; +} + +const Verdict: React.FC = ({ arbitrable }) => { return ( - - + + ); }; diff --git a/web/src/context/Web3Provider.tsx b/web/src/context/Web3Provider.tsx index 665f2e9b0..bf3596cd3 100644 --- a/web/src/context/Web3Provider.tsx +++ b/web/src/context/Web3Provider.tsx @@ -22,7 +22,7 @@ const { publicClient, webSocketPublicClient } = configureChains(chains, [ ]); const wagmiConfig = createConfig({ - autoConnect: false, + autoConnect: true, connectors: w3mConnectors({ projectId, version: 2, chains }), publicClient, webSocketPublicClient, diff --git a/web/src/graphql/gql.ts b/web/src/graphql/gql.ts index 90c809e70..f7ce54f38 100644 --- a/web/src/graphql/gql.ts +++ b/web/src/graphql/gql.ts @@ -23,7 +23,7 @@ const documents = { types.CourtPolicyUriDocument, '\n query CourtTree {\n court(id: "1") {\n name\n id\n children(orderBy: name) {\n name\n id\n children {\n name\n id\n children {\n name\n id\n children {\n name\n id\n children {\n name\n id\n }\n }\n }\n }\n }\n }\n }\n': types.CourtTreeDocument, - "\n query DisputeDetails($disputeID: ID!) {\n dispute(id: $disputeID) {\n court {\n id\n timesPerPeriod\n hiddenVotes\n feeForJuror\n }\n arbitrated {\n id\n }\n period\n ruled\n lastPeriodChange\n currentRound {\n id\n }\n currentRoundIndex\n }\n }\n": + "\n query DisputeDetails($disputeID: ID!) {\n dispute(id: $disputeID) {\n court {\n id\n timesPerPeriod\n hiddenVotes\n feeForJuror\n }\n arbitrated {\n id\n }\n period\n ruled\n lastPeriodChange\n currentRuling\n overridden\n tied\n currentRound {\n id\n }\n currentRoundIndex\n }\n }\n": types.DisputeDetailsDocument, "\n query Draw($address: String, $disputeID: String, $roundID: String) {\n draws(where: { dispute: $disputeID, juror: $address, round: $roundID }) {\n voteID\n }\n }\n": types.DrawDocument, @@ -85,8 +85,8 @@ export function graphql( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: "\n query DisputeDetails($disputeID: ID!) {\n dispute(id: $disputeID) {\n court {\n id\n timesPerPeriod\n hiddenVotes\n feeForJuror\n }\n arbitrated {\n id\n }\n period\n ruled\n lastPeriodChange\n currentRound {\n id\n }\n currentRoundIndex\n }\n }\n" -): (typeof documents)["\n query DisputeDetails($disputeID: ID!) {\n dispute(id: $disputeID) {\n court {\n id\n timesPerPeriod\n hiddenVotes\n feeForJuror\n }\n arbitrated {\n id\n }\n period\n ruled\n lastPeriodChange\n currentRound {\n id\n }\n currentRoundIndex\n }\n }\n"]; + source: "\n query DisputeDetails($disputeID: ID!) {\n dispute(id: $disputeID) {\n court {\n id\n timesPerPeriod\n hiddenVotes\n feeForJuror\n }\n arbitrated {\n id\n }\n period\n ruled\n lastPeriodChange\n currentRuling\n overridden\n tied\n currentRound {\n id\n }\n currentRoundIndex\n }\n }\n" +): (typeof documents)["\n query DisputeDetails($disputeID: ID!) {\n dispute(id: $disputeID) {\n court {\n id\n timesPerPeriod\n hiddenVotes\n feeForJuror\n }\n arbitrated {\n id\n }\n period\n ruled\n lastPeriodChange\n currentRuling\n overridden\n tied\n currentRound {\n id\n }\n currentRoundIndex\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/web/src/graphql/graphql.ts b/web/src/graphql/graphql.ts index 879939a1f..c61ee4e48 100644 --- a/web/src/graphql/graphql.ts +++ b/web/src/graphql/graphql.ts @@ -201,10 +201,13 @@ export enum ClassicContribution_OrderBy { ContributorTotalStake = "contributor__totalStake", CoreDispute = "coreDispute", CoreDisputeCurrentRoundIndex = "coreDispute__currentRoundIndex", + CoreDisputeCurrentRuling = "coreDispute__currentRuling", CoreDisputeId = "coreDispute__id", CoreDisputeLastPeriodChange = "coreDispute__lastPeriodChange", + CoreDisputeOverridden = "coreDispute__overridden", CoreDisputePeriod = "coreDispute__period", CoreDisputeRuled = "coreDispute__ruled", + CoreDisputeTied = "coreDispute__tied", Id = "id", LocalRound = "localRound", LocalRoundFeeRewards = "localRound__feeRewards", @@ -305,10 +308,13 @@ export type ClassicDispute_Filter = { export enum ClassicDispute_OrderBy { CoreDispute = "coreDispute", CoreDisputeCurrentRoundIndex = "coreDispute__currentRoundIndex", + CoreDisputeCurrentRuling = "coreDispute__currentRuling", CoreDisputeId = "coreDispute__id", CoreDisputeLastPeriodChange = "coreDispute__lastPeriodChange", + CoreDisputeOverridden = "coreDispute__overridden", CoreDisputePeriod = "coreDispute__period", CoreDisputeRuled = "coreDispute__ruled", + CoreDisputeTied = "coreDispute__tied", CurrentLocalRoundIndex = "currentLocalRoundIndex", ExtraData = "extraData", Id = "id", @@ -724,10 +730,13 @@ export enum ClassicVote_OrderBy { Choice = "choice", CoreDispute = "coreDispute", CoreDisputeCurrentRoundIndex = "coreDispute__currentRoundIndex", + CoreDisputeCurrentRuling = "coreDispute__currentRuling", CoreDisputeId = "coreDispute__id", CoreDisputeLastPeriodChange = "coreDispute__lastPeriodChange", + CoreDisputeOverridden = "coreDispute__overridden", CoreDisputePeriod = "coreDispute__period", CoreDisputeRuled = "coreDispute__ruled", + CoreDisputeTied = "coreDispute__tied", Id = "id", Juror = "juror", JurorActiveDisputes = "juror__activeDisputes", @@ -816,10 +825,13 @@ export enum Contribution_OrderBy { ContributorTotalStake = "contributor__totalStake", CoreDispute = "coreDispute", CoreDisputeCurrentRoundIndex = "coreDispute__currentRoundIndex", + CoreDisputeCurrentRuling = "coreDispute__currentRuling", CoreDisputeId = "coreDispute__id", CoreDisputeLastPeriodChange = "coreDispute__lastPeriodChange", + CoreDisputeOverridden = "coreDispute__overridden", CoreDisputePeriod = "coreDispute__period", CoreDisputeRuled = "coreDispute__ruled", + CoreDisputeTied = "coreDispute__tied", Id = "id", } @@ -1192,13 +1204,16 @@ export type Dispute = { court: Court; currentRound: Round; currentRoundIndex: Scalars["BigInt"]["output"]; + currentRuling: Scalars["BigInt"]["output"]; disputeKitDispute?: Maybe; id: Scalars["ID"]["output"]; lastPeriodChange: Scalars["BigInt"]["output"]; + overridden: Scalars["Boolean"]["output"]; period: Period; rounds: Array; ruled: Scalars["Boolean"]["output"]; shifts: Array; + tied: Scalars["Boolean"]["output"]; }; export type DisputeRoundsArgs = { @@ -1316,10 +1331,13 @@ export type DisputeKitDispute_Filter = { export enum DisputeKitDispute_OrderBy { CoreDispute = "coreDispute", CoreDisputeCurrentRoundIndex = "coreDispute__currentRoundIndex", + CoreDisputeCurrentRuling = "coreDispute__currentRuling", CoreDisputeId = "coreDispute__id", CoreDisputeLastPeriodChange = "coreDispute__lastPeriodChange", + CoreDisputeOverridden = "coreDispute__overridden", CoreDisputePeriod = "coreDispute__period", CoreDisputeRuled = "coreDispute__ruled", + CoreDisputeTied = "coreDispute__tied", CurrentLocalRoundIndex = "currentLocalRoundIndex", Id = "id", LocalRounds = "localRounds", @@ -1535,6 +1553,14 @@ export type Dispute_Filter = { currentRound_not_starts_with_nocase?: InputMaybe; currentRound_starts_with?: InputMaybe; currentRound_starts_with_nocase?: InputMaybe; + currentRuling?: InputMaybe; + currentRuling_gt?: InputMaybe; + currentRuling_gte?: InputMaybe; + currentRuling_in?: InputMaybe>; + currentRuling_lt?: InputMaybe; + currentRuling_lte?: InputMaybe; + currentRuling_not?: InputMaybe; + currentRuling_not_in?: InputMaybe>; disputeKitDispute_?: InputMaybe; id?: InputMaybe; id_gt?: InputMaybe; @@ -1553,6 +1579,10 @@ export type Dispute_Filter = { lastPeriodChange_not?: InputMaybe; lastPeriodChange_not_in?: InputMaybe>; or?: InputMaybe>>; + overridden?: InputMaybe; + overridden_in?: InputMaybe>; + overridden_not?: InputMaybe; + overridden_not_in?: InputMaybe>; period?: InputMaybe; period_in?: InputMaybe>; period_not?: InputMaybe; @@ -1563,6 +1593,10 @@ export type Dispute_Filter = { ruled_not?: InputMaybe; ruled_not_in?: InputMaybe>; shifts_?: InputMaybe; + tied?: InputMaybe; + tied_in?: InputMaybe>; + tied_not?: InputMaybe; + tied_not_in?: InputMaybe>; }; export enum Dispute_OrderBy { @@ -1592,15 +1626,18 @@ export enum Dispute_OrderBy { CurrentRoundRepartitions = "currentRound__repartitions", CurrentRoundTokensAtStakePerJuror = "currentRound__tokensAtStakePerJuror", CurrentRoundTotalFeesForJurors = "currentRound__totalFeesForJurors", + CurrentRuling = "currentRuling", DisputeKitDispute = "disputeKitDispute", DisputeKitDisputeCurrentLocalRoundIndex = "disputeKitDispute__currentLocalRoundIndex", DisputeKitDisputeId = "disputeKitDispute__id", Id = "id", LastPeriodChange = "lastPeriodChange", + Overridden = "overridden", Period = "period", Rounds = "rounds", Ruled = "ruled", Shifts = "shifts", + Tied = "tied", } export type Draw = { @@ -1701,10 +1738,13 @@ export type Draw_Filter = { export enum Draw_OrderBy { Dispute = "dispute", DisputeCurrentRoundIndex = "dispute__currentRoundIndex", + DisputeCurrentRuling = "dispute__currentRuling", DisputeId = "dispute__id", DisputeLastPeriodChange = "dispute__lastPeriodChange", + DisputeOverridden = "dispute__overridden", DisputePeriod = "dispute__period", DisputeRuled = "dispute__ruled", + DisputeTied = "dispute__tied", Id = "id", Juror = "juror", JurorActiveDisputes = "juror__activeDisputes", @@ -2113,10 +2153,13 @@ export enum Penalty_OrderBy { DegreeOfCoherency = "degreeOfCoherency", Dispute = "dispute", DisputeCurrentRoundIndex = "dispute__currentRoundIndex", + DisputeCurrentRuling = "dispute__currentRuling", DisputeId = "dispute__id", DisputeLastPeriodChange = "dispute__lastPeriodChange", + DisputeOverridden = "dispute__overridden", DisputePeriod = "dispute__period", DisputeRuled = "dispute__ruled", + DisputeTied = "dispute__tied", Id = "id", Juror = "juror", JurorActiveDisputes = "juror__activeDisputes", @@ -2695,10 +2738,13 @@ export enum Round_OrderBy { DisputeKitId = "disputeKit__id", DisputeKitNeedsFreezing = "disputeKit__needsFreezing", DisputeCurrentRoundIndex = "dispute__currentRoundIndex", + DisputeCurrentRuling = "dispute__currentRuling", DisputeId = "dispute__id", DisputeLastPeriodChange = "dispute__lastPeriodChange", + DisputeOverridden = "dispute__overridden", DisputePeriod = "dispute__period", DisputeRuled = "dispute__ruled", + DisputeTied = "dispute__tied", DrawnJurors = "drawnJurors", Id = "id", NbVotes = "nbVotes", @@ -3217,10 +3263,13 @@ export type TokenAndEthShift_Filter = { export enum TokenAndEthShift_OrderBy { Dispute = "dispute", DisputeCurrentRoundIndex = "dispute__currentRoundIndex", + DisputeCurrentRuling = "dispute__currentRuling", DisputeId = "dispute__id", DisputeLastPeriodChange = "dispute__lastPeriodChange", + DisputeOverridden = "dispute__overridden", DisputePeriod = "dispute__period", DisputeRuled = "dispute__ruled", + DisputeTied = "dispute__tied", EthAmount = "ethAmount", Id = "id", Juror = "juror", @@ -3517,10 +3566,13 @@ export type Vote_Filter = { export enum Vote_OrderBy { CoreDispute = "coreDispute", CoreDisputeCurrentRoundIndex = "coreDispute__currentRoundIndex", + CoreDisputeCurrentRuling = "coreDispute__currentRuling", CoreDisputeId = "coreDispute__id", CoreDisputeLastPeriodChange = "coreDispute__lastPeriodChange", + CoreDisputeOverridden = "coreDispute__overridden", CoreDisputePeriod = "coreDispute__period", CoreDisputeRuled = "coreDispute__ruled", + CoreDisputeTied = "coreDispute__tied", Id = "id", Juror = "juror", JurorActiveDisputes = "juror__activeDisputes", @@ -3681,6 +3733,9 @@ export type DisputeDetailsQuery = { period: Period; ruled: boolean; lastPeriodChange: any; + currentRuling: any; + overridden: boolean; + tied: boolean; currentRoundIndex: any; court: { __typename?: "Court"; id: string; timesPerPeriod: Array; hiddenVotes: boolean; feeForJuror: any }; arbitrated: { __typename?: "Arbitrable"; id: string }; @@ -4185,6 +4240,9 @@ export const DisputeDetailsDocument = { { kind: "Field", name: { kind: "Name", value: "period" } }, { kind: "Field", name: { kind: "Name", value: "ruled" } }, { kind: "Field", name: { kind: "Name", value: "lastPeriodChange" } }, + { kind: "Field", name: { kind: "Name", value: "currentRuling" } }, + { kind: "Field", name: { kind: "Name", value: "overridden" } }, + { kind: "Field", name: { kind: "Name", value: "tied" } }, { kind: "Field", name: { kind: "Name", value: "currentRound" }, diff --git a/web/src/hooks/queries/useDisputeDetailsQuery.ts b/web/src/hooks/queries/useDisputeDetailsQuery.ts index db23752f6..221ef05b1 100644 --- a/web/src/hooks/queries/useDisputeDetailsQuery.ts +++ b/web/src/hooks/queries/useDisputeDetailsQuery.ts @@ -19,6 +19,9 @@ const disputeDetailsQuery = graphql(` period ruled lastPeriodChange + currentRuling + overridden + tied currentRound { id } diff --git a/web/src/hooks/queries/useDisputeTemplate.ts b/web/src/hooks/queries/useDisputeTemplate.ts index ea1319c05..c81c771f7 100644 --- a/web/src/hooks/queries/useDisputeTemplate.ts +++ b/web/src/hooks/queries/useDisputeTemplate.ts @@ -9,7 +9,7 @@ import { useIsCrossChainDispute } from "../useIsCrossChainDispute"; export const useDisputeTemplate = (disputeID?: string, arbitrableAddress?: `0x${string}`) => { const publicClient = usePublicClient(); const { data: crossChainData } = useIsCrossChainDispute(disputeID, arbitrableAddress); - const isEnabled = !isUndefined(arbitrableAddress) && !isUndefined(disputeID) && !isUndefined(crossChainData); + const isEnabled = !isUndefined(disputeID) && !isUndefined(crossChainData) && !isUndefined(arbitrableAddress); return useQuery({ queryKey: [`DisputeTemplate${disputeID}${arbitrableAddress}`], enabled: isEnabled, @@ -22,6 +22,7 @@ export const useDisputeTemplate = (disputeID?: string, arbitrableAddress?: `0x${ const templateId = isCrossChainDispute ? crossChainTemplateId : await getTemplateId(arbitrableAddress, disputeID, publicClient); + return await getDisputeTemplate( templateId, isCrossChainDispute ? crossChainArbitrableAddress : arbitrableAddress, diff --git a/web/src/hooks/queries/useJurorBalance.ts b/web/src/hooks/queries/useJurorBalance.ts deleted file mode 100644 index dad59bfab..000000000 --- a/web/src/hooks/queries/useJurorBalance.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getKlerosCore } from "hooks/contracts/generated"; - -export const useJurorBalance = (user?: `0x${string}` | null, courtId?: string | undefined) => { - const klerosCore = getKlerosCore({}); - - const isEnabled = !!(klerosCore && user && courtId); - - return useQuery({ - queryKey: [`JurorBalance${user}${courtId}`], - enabled: isEnabled, - queryFn: async () => { - if (isEnabled) { - return await klerosCore.read.getJurorBalance([user, BigInt(courtId)]); - } - return undefined; - }, - }); -}; diff --git a/web/src/hooks/queries/usePNKAllowance.ts b/web/src/hooks/queries/usePNKAllowance.ts deleted file mode 100644 index d3b555f5a..000000000 --- a/web/src/hooks/queries/usePNKAllowance.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getPnk, getKlerosCore } from "hooks/contracts/generated"; - -export const usePNKAllowance = (user?: `0x${string}` | null) => { - const pnkContract = getPnk({}); - const klerosCore = getKlerosCore({}); - const isEnabled = user !== undefined; - - return useQuery({ - queryKey: [`pnkAllowance${user}`], - enabled: isEnabled, - queryFn: async () => { - if (pnkContract && user && klerosCore) { - return await pnkContract.read.allowance([user, klerosCore.address]); - } else { - return undefined; - } - }, - }); -}; diff --git a/web/src/hooks/queries/usePNKBalance.ts b/web/src/hooks/queries/usePNKBalance.ts deleted file mode 100644 index 8dea187d1..000000000 --- a/web/src/hooks/queries/usePNKBalance.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getPnk } from "hooks/contracts/generated"; - -export const usePNKBalance = (user?: `0x${string}` | null) => { - const pnkContract = getPnk({}); - - const isEnabled = !!(pnkContract && user); - - return useQuery({ - queryKey: [`PNKBalance${user}`], - enabled: isEnabled, - queryFn: async () => { - if (isEnabled) { - const balance = await pnkContract.read.balanceOf([user]); - return balance; - } - return undefined; - }, - }); -}; diff --git a/web/src/hooks/queries/useVotingHistory.ts b/web/src/hooks/queries/useVotingHistory.ts index 621b1e167..7f912f38b 100644 --- a/web/src/hooks/queries/useVotingHistory.ts +++ b/web/src/hooks/queries/useVotingHistory.ts @@ -36,8 +36,8 @@ const votingHistoryQuery = graphql(` export const useVotingHistory = (disputeID?: string) => { const isEnabled = disputeID !== undefined; - return useQuery({ - queryKey: [`VotingHistory${disputeID}`], + return useQuery({ + queryKey: ["refetchOnBlock", `VotingHistory${disputeID}`], enabled: isEnabled, queryFn: async () => await graphqlQueryFnHelper(votingHistoryQuery, { disputeID }), }); diff --git a/web/src/hooks/useCountdown.ts b/web/src/hooks/useCountdown.ts index 85e102566..ca60fa33a 100644 --- a/web/src/hooks/useCountdown.ts +++ b/web/src/hooks/useCountdown.ts @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { getTimeLeft } from "utils/date"; +import { isUndefined } from "utils/index"; export function useCountdown(deadline?: number): number | undefined { const [counter, setCounter] = useState(); @@ -10,9 +11,10 @@ export function useCountdown(deadline?: number): number | undefined { } }, [deadline]); useEffect(() => { - typeof counter !== "undefined" && - counter > 0 && - setTimeout(() => setCounter(counter - 1), 1000); + if (!isUndefined(counter) && counter > 0) { + const timeout = setTimeout(() => setCounter(counter - 1), 1000); + return () => clearTimeout(timeout); + } else return; }, [counter]); return counter; } diff --git a/web/src/pages/Cases/CaseDetails/Appeal/Classic/Fund.tsx b/web/src/pages/Cases/CaseDetails/Appeal/Classic/Fund.tsx index 3888b2b25..12ab41015 100644 --- a/web/src/pages/Cases/CaseDetails/Appeal/Classic/Fund.tsx +++ b/web/src/pages/Cases/CaseDetails/Appeal/Classic/Fund.tsx @@ -42,7 +42,13 @@ const useFundAppeal = (parsedAmount) => { return fundAppeal; }; -const Fund: React.FC = () => { +interface IFund { + amount: string; + setAmount: (val: string) => void; + setIsOpen: (val: boolean) => void; +} + +const Fund: React.FC = ({ amount, setAmount, setIsOpen }) => { const needFund = useNeedFund(); const { address, isDisconnected } = useAccount(); const { data: balance } = useBalance({ @@ -51,7 +57,6 @@ const Fund: React.FC = () => { }); const publicClient = usePublicClient(); - const [amount, setAmount] = useState(""); const [debouncedAmount, setDebouncedAmount] = useState(""); useDebounce(() => setDebouncedAmount(amount), 500, [amount]); @@ -79,12 +84,12 @@ const Fund: React.FC = () => { onClick={() => { if (fundAppeal) { setIsSending(true); - wrapWithToast(async () => await fundAppeal().then((response) => response.hash), publicClient) - .then(() => { - setAmount(""); - close(); - }) - .finally(() => setIsSending(false)); + wrapWithToast(async () => await fundAppeal().then((response) => response.hash), publicClient).finally( + () => { + setIsSending(false); + setIsOpen(true); + } + ); } }} /> diff --git a/web/src/pages/Cases/CaseDetails/Appeal/Classic/index.tsx b/web/src/pages/Cases/CaseDetails/Appeal/Classic/index.tsx index 657f6748d..d5020a627 100644 --- a/web/src/pages/Cases/CaseDetails/Appeal/Classic/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Appeal/Classic/index.tsx @@ -1,15 +1,44 @@ -import React from "react"; -import { ClassicAppealProvider } from "hooks/useClassicAppealContext"; +import React, { useState } from "react"; +import { ClassicAppealProvider, useOptionsContext, useSelectedOptionContext } from "hooks/useClassicAppealContext"; import Options from "./Options"; import Fund from "./Fund"; +import Popup, { PopupType } from "components/Popup"; +import AppealIcon from "svgs/icons/appeal.svg"; +import { isUndefined } from "utils/index"; -const Classic: React.FC = () => ( - -

Appeal crowdfunding

- - - -
-); +const Classic: React.FC = () => { + const [isPopupOpen, setIsPopupOpen] = useState(false); + const [amount, setAmount] = useState(""); + const { selectedOption } = useSelectedOptionContext(); + const options = useOptionsContext(); -export default Classic; + return ( + <> + {isPopupOpen && ( + + )} +

Appeal crowdfunding

+ + + + + ); +}; + +const ClassicWrapper: React.FC = () => { + return ( + + + + ); +}; + +export default ClassicWrapper; diff --git a/web/src/pages/Cases/CaseDetails/Appeal/index.tsx b/web/src/pages/Cases/CaseDetails/Appeal/index.tsx index d81c37167..b30c6a24f 100644 --- a/web/src/pages/Cases/CaseDetails/Appeal/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Appeal/index.tsx @@ -1,8 +1,8 @@ import React from "react"; -import Classic from "./Classic"; +import ClassicWrapper from "./Classic"; import { Periods } from "consts/periods"; const Appeal: React.FC<{ currentPeriodIndex: number }> = ({ currentPeriodIndex }) => - Periods.appeal === currentPeriodIndex ? :

Not in appeal period

; + Periods.appeal === currentPeriodIndex ? :

Not in appeal period

; export default Appeal; diff --git a/web/src/pages/Cases/CaseDetails/Overview.tsx b/web/src/pages/Cases/CaseDetails/Overview.tsx index 3935ab568..43f96e61b 100644 --- a/web/src/pages/Cases/CaseDetails/Overview.tsx +++ b/web/src/pages/Cases/CaseDetails/Overview.tsx @@ -120,7 +120,7 @@ const Overview: React.FC = ({ arbitrable, courtID, currentPeriodIndex {currentPeriodIndex !== Periods.evidence && ( <>
- +
)} @@ -137,7 +137,11 @@ const Overview: React.FC = ({ arbitrable, courtID, currentPeriodIndex )} {courtPolicy && ( - + Court Policy diff --git a/web/src/pages/Cases/CaseDetails/Timeline.tsx b/web/src/pages/Cases/CaseDetails/Timeline.tsx index 1712573b6..f25ad94d5 100644 --- a/web/src/pages/Cases/CaseDetails/Timeline.tsx +++ b/web/src/pages/Cases/CaseDetails/Timeline.tsx @@ -10,42 +10,23 @@ const Timeline: React.FC<{ dispute: DisputeDetailsQuery["dispute"]; currentPeriodIndex: number; }> = ({ currentPeriodIndex, dispute }) => { - const currentItemIndex = currentPeriodToCurrentItem( - currentPeriodIndex, - dispute?.ruled - ); + const currentItemIndex = currentPeriodToCurrentItem(currentPeriodIndex, dispute?.ruled); const items = useTimeline(dispute, currentItemIndex, currentItemIndex); return ( - + ); }; -const currentPeriodToCurrentItem = ( - currentPeriodIndex: number, - ruled?: boolean -): number => { +const currentPeriodToCurrentItem = (currentPeriodIndex: number, ruled?: boolean): number => { if (currentPeriodIndex <= Periods.commit) return currentPeriodIndex; - else if (currentPeriodIndex < Periods.execution) - return currentPeriodIndex - 1; + else if (currentPeriodIndex < Periods.execution) return currentPeriodIndex - 1; else return ruled ? 5 : currentPeriodIndex - 1; }; -const useTimeline = ( - dispute: DisputeDetailsQuery["dispute"], - currentItemIndex: number, - currentPeriodIndex: number -) => { - const titles = [ - "Evidence Period", - "Voting Period", - "Appeal Period", - "Executed", - ]; +const useTimeline = (dispute: DisputeDetailsQuery["dispute"], currentItemIndex: number, currentPeriodIndex: number) => { + const titles = ["Evidence Period", "Voting Period", "Appeal Period", "Executed"]; const deadlineCurrentPeriod = getDeadline( currentPeriodIndex, dispute?.lastPeriodChange, @@ -77,15 +58,9 @@ const getDeadline = ( lastPeriodChange?: string, timesPerPeriod?: string[] ): number | undefined => { - if ( - lastPeriodChange && - timesPerPeriod && - currentPeriodIndex < timesPerPeriod.length - ) { + if (lastPeriodChange && timesPerPeriod && currentPeriodIndex < timesPerPeriod.length) { const parsedLastPeriodChange = parseInt(lastPeriodChange, 10); - const parsedTimeCurrentPeriod = parseInt( - timesPerPeriod[currentPeriodIndex] - ); + const parsedTimeCurrentPeriod = parseInt(timesPerPeriod[currentPeriodIndex]); return parsedLastPeriodChange + parsedTimeCurrentPeriod; } return 0; diff --git a/web/src/pages/Cases/CaseDetails/Voting/Binary.tsx b/web/src/pages/Cases/CaseDetails/Voting/Classic.tsx similarity index 87% rename from web/src/pages/Cases/CaseDetails/Voting/Binary.tsx rename to web/src/pages/Cases/CaseDetails/Voting/Classic.tsx index 3930271ea..1856da6b4 100644 --- a/web/src/pages/Cases/CaseDetails/Voting/Binary.tsx +++ b/web/src/pages/Cases/CaseDetails/Voting/Classic.tsx @@ -8,6 +8,7 @@ import { wrapWithToast } from "utils/wrapWithToast"; import { useDisputeTemplate } from "queries/useDisputeTemplate"; import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery"; import { EnsureChain } from "components/EnsureChain"; +import ReactMarkdown from "react-markdown"; const Container = styled.div` width: 100%; @@ -44,7 +45,13 @@ const RefuseToArbitrateContainer = styled.div` justify-content: center; `; -const Binary: React.FC<{ arbitrable: `0x${string}`; voteIDs: string[] }> = ({ arbitrable, voteIDs }) => { +interface IClassic { + arbitrable: `0x${string}`; + voteIDs: string[]; + setIsOpen: (val: boolean) => void; +} + +const Classic: React.FC = ({ arbitrable, voteIDs, setIsOpen }) => { const { id } = useParams(); const parsedDisputeID = BigInt(id ?? 0); const parsedVoteIDs = useMemo(() => voteIDs.map((voteID) => BigInt(voteID)), [voteIDs]); @@ -70,17 +77,21 @@ const Binary: React.FC<{ arbitrable: `0x${string}`; voteIDs: string[] }> = ({ ar ], }); if (walletClient) { - wrapWithToast(async () => await walletClient.writeContract(request), publicClient).finally(() => { - setChosenOption(-1); - setIsSending(false); - }); + wrapWithToast(async () => await walletClient.writeContract(request), publicClient) + .then(() => { + setIsOpen(true); + }) + .finally(() => { + setChosenOption(-1); + setIsSending(false); + }); } }; return id ? ( -

{disputeTemplate?.question}

+ {disputeTemplate.question} setJustification(e.target.value)} @@ -122,4 +133,4 @@ const Binary: React.FC<{ arbitrable: `0x${string}`; voteIDs: string[] }> = ({ ar ); }; -export default Binary; +export default Classic; diff --git a/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx b/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx index 4504b438c..3fc351993 100644 --- a/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx +++ b/web/src/pages/Cases/CaseDetails/Voting/VotingHistory.tsx @@ -112,7 +112,9 @@ const VotingHistory: React.FC<{ arbitrable?: `0x${string}` }> = ({ arbitrable })

{localRounds.at(currentTab)?.totalVoted === rounds.at(currentTab)?.nbVotes ? "All jurors voted" - : localRounds.at(currentTab)?.totalVoted + " jurors voted out of " + rounds.at(currentTab)?.nbVotes} + : localRounds.at(currentTab)?.totalVoted.toString() + + ` vote${localRounds.at(currentTab)?.totalVoted.toString() === "1" ? "" : "s"} cast out of ` + + rounds.at(currentTab)?.nbVotes}

0 && - !voted ? ( - draw.voteID)} /> - ) : ( - + const [isPopupOpen, setIsPopupOpen] = useState(false); + useLockBodyScroll(isPopupOpen); + const lastPeriodChange = disputeData?.dispute?.lastPeriodChange; + const timesPerPeriod = disputeData?.dispute?.court?.timesPerPeriod; + const finalDate = + !isUndefined(currentPeriodIndex) && + !isUndefined(timesPerPeriod) && + getPeriodEndTimestamp(lastPeriodChange, currentPeriodIndex, timesPerPeriod); + + return ( + <> + {isPopupOpen && ( + + )} + {drawData && + !isUndefined(arbitrable) && + currentPeriodIndex === Periods.vote && + drawData.draws?.length > 0 && + !voted ? ( + draw.voteID)} /> + ) : ( + + )} + ); }; diff --git a/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx b/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx index f969070ec..b43e5aa7e 100644 --- a/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx +++ b/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx @@ -5,10 +5,8 @@ import { formatEther } from "viem"; import { useDebounce } from "react-use"; import { useAccount } from "wagmi"; import { Field } from "@kleros/ui-components-library"; - import { useParsedAmount } from "hooks/useParsedAmount"; -import { usePNKBalance } from "queries/usePNKBalance"; -import { useKlerosCoreGetJurorBalance } from "hooks/contracts/generated"; +import { useKlerosCoreGetJurorBalance, usePnkBalanceOf } from "hooks/contracts/generated"; import StakeWithdrawButton, { ActionType } from "./StakeWithdrawButton"; import { isUndefined } from "utils/index"; import { EnsureChain } from "components/EnsureChain"; @@ -39,17 +37,30 @@ interface IInputDisplay { action: ActionType; isSending: boolean; setIsSending: (arg0: boolean) => void; + setIsPopupOpen: (arg0: boolean) => void; + amount: string; + setAmount: (arg0: string) => void; } -const InputDisplay: React.FC = ({ action, isSending, setIsSending }) => { - const [amount, setAmount] = useState(""); +const InputDisplay: React.FC = ({ + action, + isSending, + setIsSending, + setIsPopupOpen, + amount, + setAmount, +}) => { const [debouncedAmount, setDebouncedAmount] = useState(""); useDebounce(() => setDebouncedAmount(amount), 500, [amount]); const parsedAmount = useParsedAmount(debouncedAmount); const { id } = useParams(); const { address } = useAccount(); - const { data: balance } = usePNKBalance(address); + const { data: balance } = usePnkBalanceOf({ + enabled: !isUndefined(address), + args: [address ?? "0x"], + watch: true, + }); const parsedBalance = formatEther(balance ?? 0n); const { data: jurorBalance } = useKlerosCoreGetJurorBalance({ enabled: !isUndefined(address), @@ -93,6 +104,7 @@ const InputDisplay: React.FC = ({ action, isSending, setIsSending setAmount, isSending, setIsSending, + setIsPopupOpen, }} /> diff --git a/web/src/pages/Courts/CourtDetails/StakePanel/JurorStakeDisplay.tsx b/web/src/pages/Courts/CourtDetails/StakePanel/JurorStakeDisplay.tsx index 6e0ed5686..f047c3b23 100644 --- a/web/src/pages/Courts/CourtDetails/StakePanel/JurorStakeDisplay.tsx +++ b/web/src/pages/Courts/CourtDetails/StakePanel/JurorStakeDisplay.tsx @@ -9,7 +9,7 @@ import DiceIcon from "svgs/icons/dice.svg"; import LockerIcon from "svgs/icons/locker.svg"; import PNKIcon from "svgs/icons/pnk.svg"; import { useCourtDetails } from "queries/useCourtDetails"; -import { useJurorBalance } from "queries/useJurorBalance"; +import { useKlerosCoreGetJurorBalance } from "hooks/contracts/generated"; const format = (value: bigint | undefined): string => (value !== undefined ? formatEther(value) : "0"); @@ -32,7 +32,11 @@ const formatBigIntPercentage = (numerator: bigint, denominator: bigint): string const JurorBalanceDisplay = () => { const { id } = useParams(); const { address } = useAccount(); - const { data: jurorBalance } = useJurorBalance(address, id); + const { data: jurorBalance } = useKlerosCoreGetJurorBalance({ + enabled: !isUndefined(address), + args: [address ?? "0x", BigInt(id ?? 0)], + watch: true, + }); const { data: courtDetails } = useCourtDetails(id); const stakedByAllJurors = courtDetails?.court?.stake; diff --git a/web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawButton.tsx b/web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawButton.tsx index 3c94541d8..962c44184 100644 --- a/web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawButton.tsx +++ b/web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawButton.tsx @@ -10,8 +10,8 @@ import { usePnkIncreaseAllowance, usePreparePnkIncreaseAllowance, useKlerosCoreGetJurorBalance, + usePnkAllowance, } from "hooks/contracts/generated"; -import { usePNKAllowance } from "queries/usePNKAllowance"; import { wrapWithToast } from "utils/wrapWithToast"; import { isUndefined } from "utils/index"; import { EnsureChain } from "components/EnsureChain"; @@ -28,11 +28,20 @@ interface IActionButton { action: ActionType; setIsSending: (arg0: boolean) => void; setAmount: (arg0: string) => void; + setIsPopupOpen: (arg0: boolean) => void; } -const StakeWithdrawButton: React.FC = ({ parsedAmount, action, setAmount, isSending, setIsSending }) => { +const StakeWithdrawButton: React.FC = ({ + parsedAmount, + action, + setAmount, + isSending, + setIsSending, + setIsPopupOpen, +}) => { const { id } = useParams(); const { address } = useAccount(); + const klerosCore = getKlerosCore({}); const { data: balance } = usePnkBalanceOf({ enabled: !isUndefined(address), args: [address!], @@ -43,7 +52,11 @@ const StakeWithdrawButton: React.FC = ({ parsedAmount, action, se args: [address ?? "0x", BigInt(id ?? 0)], watch: true, }); - const { data: allowance } = usePNKAllowance(address); + const { data: allowance } = usePnkAllowance({ + enabled: !isUndefined(address), + args: [address ?? "0x", klerosCore.address], + watch: true, + }); const publicClient = usePublicClient(); const isStaking = action === ActionType.stake; @@ -62,7 +75,6 @@ const StakeWithdrawButton: React.FC = ({ parsedAmount, action, se return 0n; }, [jurorBalance, parsedAmount, isAllowance, isStaking]); - const klerosCore = getKlerosCore({}); const { config: increaseAllowanceConfig } = usePreparePnkIncreaseAllowance({ enabled: isAllowance && !isUndefined(klerosCore) && !isUndefined(targetStake) && !isUndefined(allowance), args: [klerosCore?.address, BigInt(targetStake ?? 0) - BigInt(allowance ?? 0)], @@ -87,11 +99,10 @@ const StakeWithdrawButton: React.FC = ({ parsedAmount, action, se const handleStake = () => { if (typeof setStake !== "undefined") { setIsSending(true); - wrapWithToast(async () => await setStake().then((response) => response.hash), publicClient) - .then(() => { - setAmount(""); - }) - .finally(() => setIsSending(false)); + wrapWithToast(async () => await setStake().then((response) => response.hash), publicClient).finally(() => { + setIsSending(false); + setIsPopupOpen(true); + }); } }; diff --git a/web/src/pages/Courts/CourtDetails/StakePanel/index.tsx b/web/src/pages/Courts/CourtDetails/StakePanel/index.tsx index 640d2c146..d814e9e56 100644 --- a/web/src/pages/Courts/CourtDetails/StakePanel/index.tsx +++ b/web/src/pages/Courts/CourtDetails/StakePanel/index.tsx @@ -1,18 +1,22 @@ import React, { useState } from "react"; import styled from "styled-components"; - +import { useLockBodyScroll } from "react-use"; import Tag from "components/Tag"; import JurorBalanceDisplay from "./JurorStakeDisplay"; import InputDisplay from "./InputDisplay"; import { ActionType } from "./StakeWithdrawButton"; +import Popup, { PopupType } from "components/Popup/index"; +import BalanceIcon from "assets/svgs/icons/balance.svg"; -const StakePanel: React.FC<{ courtName: string }> = ({ - courtName = "General Court", -}) => { +const StakePanel: React.FC<{ courtName: string; id: string }> = ({ courtName = "General Court", id }) => { + const [amount, setAmount] = useState(""); const [isSending, setIsSending] = useState(false); + const [isPopupOpen, setIsPopupOpen] = useState(false); const [isActive, setIsActive] = useState(true); const [action, setAction] = useState(ActionType.stake); + useLockBodyScroll(isPopupOpen); + const handleClick = (action: ActionType) => { setIsActive(action === ActionType.stake); setAction(action); @@ -22,25 +26,30 @@ const StakePanel: React.FC<{ courtName: string }> = ({ return ( - handleClick(ActionType.stake)} - /> - handleClick(ActionType.withdraw)} - /> + handleClick(ActionType.stake)} /> + handleClick(ActionType.withdraw)} /> - + + {isPopupOpen && ( + + )} ); }; @@ -48,6 +57,7 @@ const StakePanel: React.FC<{ courtName: string }> = ({ export default StakePanel; const Container = styled.div` + position: relative; width: 100%; margin-top: 32px; display: flex; diff --git a/web/src/pages/Courts/CourtDetails/index.tsx b/web/src/pages/Courts/CourtDetails/index.tsx index a0fb7e591..82abe751d 100644 --- a/web/src/pages/Courts/CourtDetails/index.tsx +++ b/web/src/pages/Courts/CourtDetails/index.tsx @@ -5,7 +5,7 @@ import { useParams } from "react-router-dom"; import { Card, Breadcrumb } from "@kleros/ui-components-library"; import { useCourtPolicy } from "queries/useCourtPolicy"; import { useCourtTree, CourtTreeQuery } from "queries/useCourtTree"; - +import { isUndefined } from "utils/index"; import Stats from "./Stats"; import Description from "./Description"; import StakePanel from "./StakePanel"; @@ -30,7 +30,7 @@ const CourtDetails: React.FC = () => {

- + @@ -60,11 +60,7 @@ interface IItem { id: string; } -const getCourtsPath = ( - node: CourtTreeQuery["court"], - id: string | undefined, - path: IItem[] = [] -): IItem[] | null => { +const getCourtsPath = (node: CourtTreeQuery["court"], id: string | undefined, path: IItem[] = []): IItem[] | null => { if (!node || !id) return null; if (node.id === id) { diff --git a/web/src/pages/Dashboard/Courts/CourtCard.tsx b/web/src/pages/Dashboard/Courts/CourtCard.tsx index 822d898a1..b7d385d57 100644 --- a/web/src/pages/Dashboard/Courts/CourtCard.tsx +++ b/web/src/pages/Dashboard/Courts/CourtCard.tsx @@ -33,7 +33,7 @@ const tooltipMsg = "The locked stake of incoherent jurors is redistributed as incentives for " + "the coherent jurors."; -const format = (value: bigint | undefined): string => (value !== undefined ? formatEther(value) : "0"); +export const format = (value: bigint | undefined): string => (value !== undefined ? formatEther(value) : "0"); interface ICourtCard { id: string; diff --git a/web/src/pages/Dashboard/JurorInfo/Coherency.tsx b/web/src/pages/Dashboard/JurorInfo/Coherency.tsx index 6554d9a3a..22b553844 100644 --- a/web/src/pages/Dashboard/JurorInfo/Coherency.tsx +++ b/web/src/pages/Dashboard/JurorInfo/Coherency.tsx @@ -18,22 +18,38 @@ const tooltipMsg = " using the number of times you have been coherent and the total cases you" + " have been in."; +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 Coherency: React.FC = () => { const { address } = useAccount(); const { data } = useUserQuery(address?.toLowerCase()); const totalCoherent = parseInt(data?.user?.totalCoherent) ?? 0; const totalResolvedDisputes = parseInt(data?.user?.totalResolvedDisputes) ?? 1; const coherencyScore = calculateCoherencyScore(totalCoherent, totalResolvedDisputes); + const roundedCoherencyScore = Math.round(coherencyScore * 100); + + const { level, title } = + levelTitles.find(({ scoreRange }) => { + return roundedCoherencyScore >= scoreRange[0] && roundedCoherencyScore < scoreRange[1]; + }) ?? levelTitles[0]; return ( - Aristotle - - + {title} + + diff --git a/web/src/pages/Dashboard/JurorInfo/TokenRewards.tsx b/web/src/pages/Dashboard/JurorInfo/TokenRewards.tsx index 19679c151..4edf67170 100644 --- a/web/src/pages/Dashboard/JurorInfo/TokenRewards.tsx +++ b/web/src/pages/Dashboard/JurorInfo/TokenRewards.tsx @@ -1,7 +1,6 @@ import React from "react"; import styled from "styled-components"; -import _ETH from "assets/svgs/styled/eth.svg"; -import _PNK from "assets/svgs/styled/pnk.svg"; +import GradientTokenIcons from "components/GradientTokenIcons"; const RewardContainer = styled.div` display: flex; @@ -10,14 +9,6 @@ const RewardContainer = styled.div` gap: 8px; `; -const ETH = styled(_ETH)` - stroke: ${({ theme }) => theme.secondaryBlue}; -`; - -const PNK = styled(_PNK)` - stroke: ${({ theme }) => theme.secondaryBlue}; -`; - const StyledH1 = styled.h1` margin: 0; `; @@ -31,7 +22,7 @@ interface ITokenRewards { const TokenRewards: React.FC = ({ token, amount, value }) => { return ( - {token === "ETH" ? : } + {token && } {amount} {token}