diff --git a/contracts/deploy/upgrade-all.ts b/contracts/deploy/upgrade-all.ts index e545cac8d..ddb6d440b 100644 --- a/contracts/deploy/upgrade-all.ts +++ b/contracts/deploy/upgrade-all.ts @@ -81,7 +81,7 @@ const deployUpgradeAll: DeployFunction = async (hre: HardhatRuntimeEnvironment) await upgrade(disputeKitClassic, "initialize6", []); await upgrade(disputeTemplateRegistry, "initialize2", []); await upgrade(evidence, "initialize2", []); - await upgrade(core, "initialize4", []); + await upgrade(core, "initialize5", []); await upgrade(policyRegistry, "initialize2", []); await upgrade(sortition, "initialize3", []); }; diff --git a/contracts/scripts/find-initializer-versions.sh b/contracts/scripts/find-initializer-versions.sh new file mode 100755 index 000000000..322929812 --- /dev/null +++ b/contracts/scripts/find-initializer-versions.sh @@ -0,0 +1,27 @@ +#! /usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +declare -A rpcUrls +rpcUrls["arbitrum"]=$(mesc url arbitrum_alchemy) +rpcUrls["arbitrumSepolia"]=$(mesc url arbitrumSepolia_alchemy) +rpcUrls["arbitrumSepoliaDevnet"]=$(mesc url arbitrumSepolia_alchemy) + +# event Initialized(uint64 version); +eventTopic=0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2 + +for c in arbitrum arbitrumSepolia arbitrumSepoliaDevnet; do + echo "--------------------------------" + echo "$c" + echo "--------------------------------" + for f in "$SCRIPT_DIR"/../deployments/"$c"/*_Proxy.json; do + address=$(jq -r .address "$f") + block=$(jq -r .receipt.blockNumber "$f") + basename "$f" + results=$(cast logs --from-block "$block" --to-block latest $eventTopic --address "$address" --rpc-url "${rpcUrls[$c]}" --json | jq -r .[].data) + initializer=$(cast --to-dec "$(echo "$results" | tail -n1)") + version=$(cast call --rpc-url "${rpcUrls[$c]}" "$address" "version()(string)" --json 2>/dev/null | jq -r '.[0]') + echo "$initializer" @v"$version" + echo + done +done diff --git a/contracts/scripts/keeperBot.ts b/contracts/scripts/keeperBot.ts index bd4ebcf35..c211f171b 100644 --- a/contracts/scripts/keeperBot.ts +++ b/contracts/scripts/keeperBot.ts @@ -7,6 +7,7 @@ import { Cores, getContracts as getContractsForCoreType } from "./utils/contract let request: (url: string, query: string) => Promise; // Workaround graphql-request ESM import const { ethers } = hre; +const MAX_DRAW_CALLS_WITHOUT_JURORS = 10; const MAX_DRAW_ITERATIONS = 30; const MAX_EXECUTE_ITERATIONS = 20; const MAX_DELAYED_STAKES_ITERATIONS = 50; @@ -248,7 +249,19 @@ const drawJurors = async (dispute: { id: string; currentRoundIndex: string }, it const { core } = await getContracts(); let success = false; try { - await core.draw.staticCall(dispute.id, iterations, HIGH_GAS_LIMIT); + const simulatedIterations = iterations * MAX_DRAW_CALLS_WITHOUT_JURORS; // Drawing will be skipped as long as no juror is available in the next MAX_DRAW_CALLS_WITHOUT_JURORS calls to draw() given this nb of iterations. + const { drawnJurors: drawnJurorsBefore } = await core.getRoundInfo(dispute.id, dispute.currentRoundIndex); + const nbDrawnJurors = (await core.draw.staticCall(dispute.id, simulatedIterations, HIGH_GAS_LIMIT)) as bigint; + const extraJurors = nbDrawnJurors - BigInt(drawnJurorsBefore.length); + logger.debug( + `Draw: ${extraJurors} jurors available in the next ${simulatedIterations} iterations for dispute ${dispute.id}` + ); + if (extraJurors <= 0n) { + logger.warn( + `Draw: skipping, no jurors available in the next ${simulatedIterations} iterations for dispute ${dispute.id}` + ); + return success; + } } catch (e) { logger.error(`Draw: will fail for ${dispute.id}, skipping`); return success; diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index 88272f22b..287e4f685 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -8,7 +8,7 @@ import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20} from "./KlerosCor /// Core arbitrator contract for Kleros v2. /// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. contract KlerosCore is KlerosCoreBase { - string public constant override version = "0.9.3"; + string public constant override version = "0.9.4"; // ************************************* // // * Constructor * // @@ -56,7 +56,7 @@ contract KlerosCore is KlerosCoreBase { ); } - function initialize4() external reinitializer(4) { + function initialize5() external reinitializer(5) { // NOP } diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 2bdebe157..6a435489b 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -593,7 +593,8 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @dev Draws jurors for the dispute. Can be called in parts. /// @param _disputeID The ID of the dispute. /// @param _iterations The number of iterations to run. - function draw(uint256 _disputeID, uint256 _iterations) external { + /// @return nbDrawnJurors The total number of jurors drawn in the round. + function draw(uint256 _disputeID, uint256 _iterations) external returns (uint256 nbDrawnJurors) { Dispute storage dispute = disputes[_disputeID]; uint256 currentRound = dispute.rounds.length - 1; Round storage round = dispute.rounds[currentRound]; @@ -616,6 +617,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable } } round.drawIterations += i; + return round.drawnJurors.length; } /// @dev Appeals the ruling of a specified dispute. @@ -981,18 +983,34 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable (ruling, tied, overridden) = disputeKit.currentRuling(_disputeID); } + /// @dev Gets the round info for a specified dispute and round. + /// @dev This function must not be called from a non-view function because it returns a dynamic array which might be very large, theoretically exceeding the block gas limit. + /// @param _disputeID The ID of the dispute. + /// @param _round The round to get the info for. + /// @return round The round info. function getRoundInfo(uint256 _disputeID, uint256 _round) external view returns (Round memory) { return disputes[_disputeID].rounds[_round]; } + /// @dev Gets the PNK at stake per juror for a specified dispute and round. + /// @param _disputeID The ID of the dispute. + /// @param _round The round to get the info for. + /// @return pnkAtStakePerJuror The PNK at stake per juror. function getPnkAtStakePerJuror(uint256 _disputeID, uint256 _round) external view returns (uint256) { return disputes[_disputeID].rounds[_round].pnkAtStakePerJuror; } + /// @dev Gets the number of rounds for a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return The number of rounds. function getNumberOfRounds(uint256 _disputeID) external view returns (uint256) { return disputes[_disputeID].rounds.length; } + /// @dev Checks if a given dispute kit is supported by a given court. + /// @param _courtID The ID of the court to check the support for. + /// @param _disputeKitID The ID of the dispute kit to check the support for. + /// @return Whether the dispute kit is supported or not. function isSupported(uint96 _courtID, uint256 _disputeKitID) external view returns (bool) { return courts[_courtID].supportedDisputeKits[_disputeKitID]; } diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 6edbede01..988e42a53 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -9,7 +9,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /// Core arbitrator contract for Kleros v2. /// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. contract KlerosCoreNeo is KlerosCoreBase { - string public constant override version = "0.8.0"; + string public constant override version = "0.9.4"; // ************************************* // // * Storage * // @@ -67,7 +67,7 @@ contract KlerosCoreNeo is KlerosCoreBase { jurorNft = _jurorNft; } - function initialize4() external reinitializer(4) { + function initialize5() external reinitializer(5) { // NOP } diff --git a/subgraph/core/src/KlerosCore.ts b/subgraph/core/src/KlerosCore.ts index 42a54644f..519ee51c4 100644 --- a/subgraph/core/src/KlerosCore.ts +++ b/subgraph/core/src/KlerosCore.ts @@ -215,10 +215,19 @@ export function handleAppealDecision(event: AppealDecision): void { const disputeID = event.params._disputeID; const dispute = Dispute.load(disputeID.toString()); if (!dispute) return; + + // Load the current (previous) round + const previousRoundID = dispute.currentRound; + const previousRound = Round.load(previousRoundID); + if (previousRound) { + previousRound.isCurrentRound = false; + previousRound.save(); + } + const newRoundIndex = dispute.currentRoundIndex.plus(ONE); - const roundID = `${disputeID}-${newRoundIndex.toString()}`; + const newRoundID = `${disputeID}-${newRoundIndex.toString()}`; dispute.currentRoundIndex = newRoundIndex; - dispute.currentRound = roundID; + dispute.currentRound = newRoundID; dispute.save(); const roundInfo = contract.getRoundInfo(disputeID, newRoundIndex); diff --git a/subgraph/core/src/entities/Dispute.ts b/subgraph/core/src/entities/Dispute.ts index 3e03eb1f8..f3bbf221e 100644 --- a/subgraph/core/src/entities/Dispute.ts +++ b/subgraph/core/src/entities/Dispute.ts @@ -57,10 +57,36 @@ export const updateDisputeRequestData = (event: DisputeCreation): void => { if (!receipt) return; const logs = receipt.logs; + const coreDisputeId = event.params._disputeID; // note that the topic at 0th index is always the event signature - const disputeRequestEventIndex = logs.findIndex((log) => log.topics[0] == DisputeRequestSignature); - const crossChainDisputeEventIndex = logs.findIndex((log) => log.topics[0] == CrossChainDisputeIncomingSignature); + // For DisputeRequestSignature + let disputeRequestEventIndex = -1; + for (let i = 0; i < logs.length; i++) { + let log = logs[i]; + if (log.topics.length > 2 && log.topics[0] == DisputeRequestSignature) { + // 3rd indexed argument in event is _arbitratorDisputeId + let decodedId = ethereum.decode("uint256", log.topics[2]); + if (decodedId != null && coreDisputeId.equals(decodedId.toBigInt())) { + disputeRequestEventIndex = i; + break; + } + } + } + + // For CrossChainDisputeIncomingSignature + let crossChainDisputeEventIndex = -1; + for (let i = 0; i < logs.length; i++) { + let log = logs[i]; + if (log.topics.length > 3 && log.topics[0] == CrossChainDisputeIncomingSignature) { + // 4th indexed argument in event is _arbitratorDisputeId + let decodedId = ethereum.decode("uint256", log.topics[3]); + if (decodedId != null && coreDisputeId.equals(decodedId.toBigInt())) { + crossChainDisputeEventIndex = i; + break; + } + } + } if (crossChainDisputeEventIndex !== -1) { const crossChainDisputeEvent = logs[crossChainDisputeEventIndex]; diff --git a/subgraph/package.json b/subgraph/package.json index 0a125ee5e..05ab7235a 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-subgraph", - "version": "0.15.2", + "version": "0.15.4", "drtVersion": "0.12.0", "license": "MIT", "scripts": { diff --git a/web/README.md b/web/README.md index 61fe8c865..e6b643337 100644 --- a/web/README.md +++ b/web/README.md @@ -23,7 +23,7 @@ ### Pre-Requisites -If you haven't already, you need to follow all the previous steps of the **Contributing** section of the repo's [Contribution Guidelines](../CONTRIBUTING.md). +If you haven't already, you need to follow all the previous steps of the **Contributing** section of the repo's [Contribution Guidelines](../CONTRIBUTING.md) ### Getting Started diff --git a/web/src/assets/svgs/icons/gavel-executed.svg b/web/src/assets/svgs/icons/gavel-executed.svg new file mode 100644 index 000000000..5e9b5777f --- /dev/null +++ b/web/src/assets/svgs/icons/gavel-executed.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/components/AllCasesButton.tsx b/web/src/components/AllCasesButton.tsx deleted file mode 100644 index 07bdb1115..000000000 --- a/web/src/components/AllCasesButton.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react"; - -import styled from "styled-components"; - -import DocIcon from "svgs/icons/doc.svg"; - -import { encodeURIFilter } from "utils/uri"; -import { getDescriptiveCourtName } from "utils/getDescriptiveCourtName"; - -import { BlueIconTextButtonContainer } from "./BlueIconTextButtonContainer"; -import { InternalLink } from "./InternalLink"; - -const StyledDocIcon = styled(DocIcon)` - width: 16px; - height: 16px; - margin-right: 8px; -`; - -const IconAndTextContainer = styled.div` - display: inline-block; -`; - -interface IAllCasesButton { - courtId?: string; - courtName?: string; -} - -const AllCasesButton: React.FC = ({ courtId, courtName }) => { - const filter = courtId ? { court: courtId } : {}; - const link = `/cases/display/1/desc/${encodeURIFilter(filter)}`; - const labelText = courtId ? `All Cases in ${getDescriptiveCourtName(courtName)}` : "All Cases"; - - return ( - - - - - {labelText} - - - - ); -}; - -export default AllCasesButton; diff --git a/web/src/components/DisputePreview/DisputeContext.tsx b/web/src/components/DisputePreview/DisputeContext.tsx index d5e7130d1..435bc47a8 100644 --- a/web/src/components/DisputePreview/DisputeContext.tsx +++ b/web/src/components/DisputePreview/DisputeContext.tsx @@ -1,7 +1,8 @@ -import React from "react"; +import React, { useMemo } from "react"; import styled from "styled-components"; import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; +import { useAccount } from "wagmi"; import { INVALID_DISPUTE_DATA_ERROR, RPC_ERROR } from "consts/index"; import { Answer as IAnswer } from "context/NewDisputeContext"; @@ -9,6 +10,8 @@ import { isUndefined } from "utils/index"; import { responsiveSize } from "styles/responsiveSize"; +import { DisputeDetailsQuery, VotingHistoryQuery } from "src/graphql/graphql"; + import ReactMarkdown from "components/ReactMarkdown"; import { StyledSkeleton } from "components/StyledSkeleton"; @@ -16,14 +19,22 @@ import { Divider } from "../Divider"; import { ExternalLink } from "../ExternalLink"; import AliasDisplay from "./Alias"; +import RulingAndRewardsIndicators from "../Verdict/RulingAndRewardsIndicators"; +import CardLabel from "../DisputeView/CardLabels"; const StyledH1 = styled.h1` margin: 0; word-wrap: break-word; - font-size: ${responsiveSize(18, 24)}; + font-size: ${responsiveSize(20, 26)}; line-height: 24px; `; +const TitleSection = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + const ReactMarkdownWrapper = styled.div` & p:first-of-type { margin: 0; @@ -66,19 +77,59 @@ const AliasesContainer = styled.div` gap: ${responsiveSize(8, 20)}; `; +const RulingAndRewardsAndLabels = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; +`; + interface IDisputeContext { disputeDetails?: DisputeDetails; isRpcError?: boolean; + dispute?: DisputeDetailsQuery | undefined; + + disputeId?: string; + votingHistory?: VotingHistoryQuery | undefined; } -export const DisputeContext: React.FC = ({ disputeDetails, isRpcError = false }) => { +export const DisputeContext: React.FC = ({ + disputeDetails, + isRpcError = false, + dispute, + disputeId, + votingHistory, +}) => { + const { isDisconnected } = useAccount(); const errMsg = isRpcError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR; + const rounds = votingHistory?.dispute?.rounds; + const jurorRewardsDispersed = useMemo(() => Boolean(rounds?.every((round) => round.jurorRewardsDispersed)), [rounds]); + console.log({ jurorRewardsDispersed }, disputeDetails); return ( <> - - {isUndefined(disputeDetails) ? : (disputeDetails?.title ?? errMsg)} - + + + {isUndefined(disputeDetails) ? : (disputeDetails?.title ?? errMsg)} + + {!isUndefined(disputeDetails) && + !isUndefined(dispute) && + !isUndefined(disputeId) && + !isUndefined(votingHistory) ? ( + + {!isUndefined(Boolean(dispute?.dispute?.ruled)) || jurorRewardsDispersed ? ( + + ) : null} + {!isDisconnected ? ( + + ) : null} + + ) : null} + + {disputeDetails?.question?.trim() || disputeDetails?.description?.trim() ? (
{disputeDetails?.question?.trim() ? ( diff --git a/web/src/components/DisputePreview/Policies.tsx b/web/src/components/DisputePreview/Policies.tsx index ad19094e0..013e2208a 100644 --- a/web/src/components/DisputePreview/Policies.tsx +++ b/web/src/components/DisputePreview/Policies.tsx @@ -1,6 +1,8 @@ import React from "react"; import styled, { css } from "styled-components"; +import { useParams } from "react-router-dom"; + import PaperclipIcon from "svgs/icons/paperclip.svg"; import PolicyIcon from "svgs/icons/policy.svg"; @@ -67,17 +69,23 @@ interface IPolicies { } export const Policies: React.FC = ({ disputePolicyURI, courtId, attachment }) => { + const { id } = useParams(); + return ( Policy documents: {!isUndefined(attachment) && !isUndefined(attachment.uri) ? ( - + {attachment.label ?? "Attachment"} ) : null} {isUndefined(disputePolicyURI) ? null : ( - + Dispute Policy diff --git a/web/src/components/DisputeView/CardLabels/index.tsx b/web/src/components/DisputeView/CardLabels/index.tsx index 523539ddf..d6ca94a56 100644 --- a/web/src/components/DisputeView/CardLabels/index.tsx +++ b/web/src/components/DisputeView/CardLabels/index.tsx @@ -22,11 +22,12 @@ import { ClassicContribution } from "src/graphql/graphql"; import Label, { IColors } from "./Label"; import RewardsAndFundLabel from "./RewardsAndFundLabel"; -const Container = styled.div<{ isList: boolean }>` +const Container = styled.div<{ isList: boolean; isOverview: boolean }>` display: flex; gap: 8px; flex-direction: column; align-items: end; + ${({ isList }) => !isList && css` @@ -36,7 +37,16 @@ const Container = styled.div<{ isList: boolean }>` flex-direction: row; align-items: center; `} + + ${({ isOverview }) => + isOverview && + css` + margin-top: 0; + flex-direction: row; + width: auto; + `} `; + const RewardsContainer = styled.div` display: flex; gap: 4px 8px; @@ -47,6 +57,7 @@ interface ICardLabels { disputeId: string; round: number; isList: boolean; + isOverview?: boolean; } const LabelArgs: Record>; color: IColors }> = { @@ -73,7 +84,7 @@ const getFundingRewards = (contributions: ClassicContribution[], closed: boolean return Number(formatUnits(BigInt(contribution), 18)); }; -const CardLabel: React.FC = ({ disputeId, round, isList }) => { +const CardLabel: React.FC = ({ disputeId, round, isList, isOverview = false }) => { const { address } = useAccount(); const { data: labelInfo, isLoading } = useLabelInfoQuery(address?.toLowerCase(), disputeId); const localRounds = getLocalRounds(labelInfo?.dispute?.disputeKitDispute); @@ -139,7 +150,7 @@ const CardLabel: React.FC = ({ disputeId, round, isList }) => { }, [contributionRewards, shifts]); return ( - + {isLoading ? ( ) : ( diff --git a/web/src/components/DisputeView/DisputeInfo/DisputeInfoCard.tsx b/web/src/components/DisputeView/DisputeInfo/DisputeInfoCard.tsx index 6bc83242d..0f1818fda 100644 --- a/web/src/components/DisputeView/DisputeInfo/DisputeInfoCard.tsx +++ b/web/src/components/DisputeView/DisputeInfo/DisputeInfoCard.tsx @@ -60,7 +60,7 @@ const DisputeInfoCard: React.FC = ({ isOverview, showLabels, f item.display ? : null )} - {showLabels ? : null} + {showLabels ? : null} ); }; diff --git a/web/src/components/EvidenceCard.tsx b/web/src/components/EvidenceCard.tsx index e25340907..f5f5c72d0 100644 --- a/web/src/components/EvidenceCard.tsx +++ b/web/src/components/EvidenceCard.tsx @@ -3,6 +3,7 @@ import styled, { css } from "styled-components"; import Identicon from "react-identicons"; import ReactMarkdown from "react-markdown"; +import { useParams } from "react-router-dom"; import { Card } from "@kleros/ui-components-library"; @@ -224,6 +225,7 @@ const EvidenceCard: React.FC = ({ fileURI, }) => { const profileLink = `/profile/1/desc/all?address=${sender}`; + const { id } = useParams(); const transactionExplorerLink = useMemo(() => { return getTxnExplorerLink(transactionHash ?? ""); @@ -258,7 +260,7 @@ const EvidenceCard: React.FC = ({ {fileURI && fileURI !== "-" ? ( - + diff --git a/web/src/components/FileViewer/index.tsx b/web/src/components/FileViewer/index.tsx index 39b025a53..e67c0787f 100644 --- a/web/src/components/FileViewer/index.tsx +++ b/web/src/components/FileViewer/index.tsx @@ -24,6 +24,10 @@ const StyledDocViewer = styled(DocViewer)` #pdf-controls { z-index: 3; } + + [class*="--loading"] { + color: ${({ theme }) => theme.secondaryText}; + } `; /** diff --git a/web/src/components/LatestCases.tsx b/web/src/components/LatestCases.tsx index 89783a327..9f4133607 100644 --- a/web/src/components/LatestCases.tsx +++ b/web/src/components/LatestCases.tsx @@ -11,14 +11,23 @@ import DisputeView from "components/DisputeView"; import { SkeletonDisputeCard } from "components/StyledSkeleton"; import { Dispute_Filter } from "../graphql/graphql"; -import AllCasesButton from "./AllCasesButton"; +import SeeAllCasesButton from "./SeeAllCasesButton"; const Container = styled.div` - margin-top: ${responsiveSize(28, 48)}; + margin-top: ${responsiveSize(32, 48)}; `; -const Title = styled.h1` +const TitleAndButtonContainer = styled.div` + display: flex; + flex-wrap: wrap; + flex-direction: row; + align-items: center; + gap: 4px 12px; margin-bottom: ${responsiveSize(12, 24)}; +`; + +const Title = styled.h1` + margin-bottom: 0; font-size: ${responsiveSize(20, 24)}; `; @@ -30,34 +39,27 @@ const DisputeContainer = styled.div` gap: var(--gap); `; -const ButtonContainer = styled.div` - display: flex; - margin-top: 16px; - justify-content: center; -`; - interface ILatestCases { title?: string; filters?: Dispute_Filter; - courtName?: string; } -const LatestCases: React.FC = ({ title = "Latest Cases", filters, courtName }) => { +const LatestCases: React.FC = ({ title = "Latest Cases", filters }) => { const { data } = useCasesQuery(0, 3, filters); const disputes: DisputeDetailsFragment[] = useMemo(() => data?.disputes as DisputeDetailsFragment[], [data]); const courtId = typeof filters?.court === "string" ? filters?.court : undefined; return isUndefined(disputes) || disputes.length > 0 ? ( - {title} + + {title} + + {isUndefined(disputes) ? Array.from({ length: 3 }).map((_, index) => ) : disputes.map((dispute) => )} - - - ) : null; }; diff --git a/web/src/components/SeeAllCasesButton.tsx b/web/src/components/SeeAllCasesButton.tsx new file mode 100644 index 000000000..8515619ab --- /dev/null +++ b/web/src/components/SeeAllCasesButton.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import { encodeURIFilter } from "utils/uri"; + +import { BlueIconTextButtonContainer } from "./BlueIconTextButtonContainer"; +import { InternalLink } from "./InternalLink"; + +interface ISeeAllCasesButton { + courtId?: string; +} + +const SeeAllCasesButton: React.FC = ({ courtId }) => { + const filter = courtId ? { court: courtId } : {}; + const link = `/cases/display/1/desc/${encodeURIFilter(filter)}`; + const labelText = "See all"; + + return ( + + {labelText} + + ); +}; + +export default SeeAllCasesButton; diff --git a/web/src/components/SeeAllJurorsButton.tsx b/web/src/components/SeeAllJurorsButton.tsx new file mode 100644 index 000000000..e18e77109 --- /dev/null +++ b/web/src/components/SeeAllJurorsButton.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +import { BlueIconTextButtonContainer } from "./BlueIconTextButtonContainer"; +import { InternalLink } from "./InternalLink"; + +const SeeAllJurorsButton: React.FC = () => { + return ( + + + + + + ); +}; + +export default SeeAllJurorsButton; diff --git a/web/src/components/Verdict/DisputeTimeline.tsx b/web/src/components/Verdict/DisputeTimeline.tsx index 2bbc53879..754f1dadf 100644 --- a/web/src/components/Verdict/DisputeTimeline.tsx +++ b/web/src/components/Verdict/DisputeTimeline.tsx @@ -1,14 +1,13 @@ import React, { useMemo } from "react"; import styled, { useTheme } from "styled-components"; -import Skeleton from "react-loading-skeleton"; import { useParams } from "react-router-dom"; import { _TimelineItem1, CustomTimeline } from "@kleros/ui-components-library"; -import CalendarIcon from "svgs/icons/calendar.svg"; import ClosedCaseIcon from "svgs/icons/check-circle-outline.svg"; import NewTabIcon from "svgs/icons/new-tab.svg"; +import GavelExecutedIcon from "svgs/icons/gavel-executed.svg"; import { Periods } from "consts/periods"; import { usePopulatedDisputeData } from "hooks/queries/usePopulatedDisputeData"; @@ -21,8 +20,6 @@ import { useVotingHistory } from "queries/useVotingHistory"; import { ClassicRound } from "src/graphql/graphql"; import { getTxnExplorerLink } from "src/utils"; -import { responsiveSize } from "styles/responsiveSize"; - import { StyledClosedCircle } from "components/StyledIcons/ClosedCircleIcon"; import { ExternalLink } from "../ExternalLink"; @@ -37,24 +34,6 @@ const StyledTimeline = styled(CustomTimeline)` width: 100%; `; -const EnforcementContainer = styled.div` - display: flex; - gap: 8px; - margin-top: ${responsiveSize(12, 24)}; - fill: ${({ theme }) => theme.secondaryText}; - - small { - font-weight: 400; - line-height: 19px; - color: ${({ theme }) => theme.secondaryText}; - } -`; - -const StyledCalendarIcon = styled(CalendarIcon)` - width: 14px; - height: 14px; -`; - const StyledNewTabIcon = styled(NewTabIcon)` margin-bottom: 2px; path { @@ -84,73 +63,95 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string const localRounds: ClassicRound[] = getLocalRounds(votingHistory?.dispute?.disputeKitDispute) as ClassicRound[]; const rounds = votingHistory?.dispute?.rounds; const theme = useTheme(); - const txnExplorerLink = useMemo(() => { + const txnDisputeCreatedLink = useMemo(() => { return getTxnExplorerLink(votingHistory?.dispute?.transactionHash ?? ""); }, [votingHistory]); + const txnEnforcementLink = useMemo(() => { + return getTxnExplorerLink(disputeDetails?.dispute?.rulingTransactionHash ?? ""); + }, [disputeDetails]); return useMemo(() => { const dispute = disputeDetails?.dispute; - if (dispute) { - const rulingOverride = dispute.overridden; - const currentPeriodIndex = Periods[dispute.period]; - - return localRounds?.reduce( - (acc, { winningChoice }, index) => { - const isOngoing = index === localRounds.length - 1 && currentPeriodIndex < 3; - const roundTimeline = rounds?.[index].timeline; - - const icon = dispute.ruled && !rulingOverride && index === localRounds.length - 1 ? ClosedCaseIcon : ""; - const answers = disputeData?.answers; - acc.push({ - title: `Jury Decision - Round ${index + 1}`, - party: isOngoing ? "Voting is ongoing" : getVoteChoice(winningChoice, answers), - subtitle: isOngoing - ? "" - : `${formatDate(roundTimeline?.[Periods.vote])} / ${ - votingHistory?.dispute?.rounds.at(index)?.court.name - }`, - rightSided: true, - variant: theme.secondaryPurple, - Icon: icon !== "" ? icon : undefined, - }); - - if (index < localRounds.length - 1) { - acc.push({ - title: "Appealed", - party: "", - subtitle: formatDate(roundTimeline?.[Periods.appeal]), - rightSided: true, - Icon: StyledClosedCircle, - }); - } else if (rulingOverride && dispute.currentRuling !== winningChoice) { - acc.push({ - title: "Won by Appeal", - party: getVoteChoice(dispute.currentRuling, answers), - subtitle: formatDate(roundTimeline?.[Periods.appeal]), - rightSided: true, - Icon: ClosedCaseIcon, - }); - } - - return acc; - }, - [ - { - title: "Dispute created", - party: ( - - - - ), - subtitle: formatDate(votingHistory?.dispute?.createdAt), - rightSided: true, - variant: theme.secondaryPurple, - }, - ] - ); + if (!dispute) return; + + const rulingOverride = dispute.overridden; + const currentPeriodIndex = Periods[dispute.period]; + + const base: TimelineItems = [ + { + title: "Dispute created", + party: ( + + + + ), + subtitle: formatDate(votingHistory?.dispute?.createdAt), + rightSided: true, + variant: theme.secondaryPurple, + }, + ]; + + const items = localRounds?.reduce<_TimelineItem1[]>((acc, { winningChoice }, index) => { + const isOngoing = index === localRounds.length - 1 && currentPeriodIndex < 3; + const roundTimeline = rounds?.[index].timeline; + const icon = dispute.ruled && !rulingOverride && index === localRounds.length - 1 ? ClosedCaseIcon : undefined; + const answers = disputeData?.answers; + + acc.push({ + title: `Jury Decision - Round ${index + 1}`, + party: isOngoing ? "Voting is ongoing" : getVoteChoice(winningChoice, answers), + subtitle: isOngoing ? "" : `${formatDate(roundTimeline?.[Periods.vote])} / ${rounds?.[index]?.court.name}`, + rightSided: true, + variant: theme.secondaryPurple, + Icon: icon, + }); + + if (index < localRounds.length - 1) { + acc.push({ + title: "Appealed", + party: "", + subtitle: formatDate(roundTimeline?.[Periods.appeal]), + rightSided: true, + Icon: StyledClosedCircle, + }); + } else if (rulingOverride && dispute.currentRuling !== winningChoice) { + acc.push({ + title: "Won by Appeal", + party: getVoteChoice(dispute.currentRuling, answers), + subtitle: formatDate(roundTimeline?.[Periods.appeal]), + rightSided: true, + Icon: ClosedCaseIcon, + }); + } + + return acc; + }, []); + + if (dispute.ruled) { + items.push({ + title: "Enforcement", + party: ( + + + + ), + subtitle: `${formatDate(dispute.rulingTimestamp)} / ${rounds?.at(-1)?.court.name}`, + rightSided: true, + Icon: GavelExecutedIcon, + }); } - return; - }, [disputeDetails, disputeData, localRounds, theme, rounds, votingHistory, txnExplorerLink]); + + return [...base, ...items] as TimelineItems; + }, [ + disputeDetails, + disputeData, + localRounds, + theme, + rounds, + votingHistory, + txnDisputeCreatedLink, + txnEnforcementLink, + ]); }; interface IDisputeTimeline { @@ -160,33 +161,8 @@ interface IDisputeTimeline { const DisputeTimeline: React.FC = ({ arbitrable }) => { const { id } = useParams(); const { data: disputeDetails } = useDisputeDetailsQuery(id); - const { data: votingHistory } = useVotingHistory(id); const items = useItems(disputeDetails, arbitrable); - const transactionExplorerLink = useMemo(() => { - return getTxnExplorerLink(disputeDetails?.dispute?.rulingTransactionHash ?? ""); - }, [disputeDetails]); - - return ( - - {items && } - {disputeDetails?.dispute?.ruled && ( - - - - Enforcement:{" "} - {disputeDetails.dispute.rulingTimestamp ? ( - - {formatDate(disputeDetails.dispute.rulingTimestamp)} - - ) : ( - - )}{" "} - / {votingHistory?.dispute?.rounds.at(-1)?.court.name} - - - )} - - ); + return {items && }; }; export default DisputeTimeline; diff --git a/web/src/components/Verdict/FinalDecision.tsx b/web/src/components/Verdict/FinalDecision.tsx index d58a216ce..7c1bb4864 100644 --- a/web/src/components/Verdict/FinalDecision.tsx +++ b/web/src/components/Verdict/FinalDecision.tsx @@ -12,10 +12,9 @@ import { REFETCH_INTERVAL } from "consts/index"; import { Periods } from "consts/periods"; import { useReadKlerosCoreCurrentRuling } from "hooks/contracts/generated"; import { usePopulatedDisputeData } from "hooks/queries/usePopulatedDisputeData"; -import { useVotingHistory } from "hooks/queries/useVotingHistory"; +import { VotingHistoryQuery } from "hooks/queries/useVotingHistory"; import { useVotingContext } from "hooks/useVotingContext"; import { getLocalRounds } from "utils/getLocalRounds"; -import { isUndefined } from "utils/index"; import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery"; @@ -25,7 +24,6 @@ import { Divider } from "../Divider"; import { StyledArrowLink } from "../StyledArrowLink"; import AnswerDisplay from "./Answer"; -import RulingAndRewardsIndicators from "./RulingAndRewardsIndicators"; const Container = styled.div` width: 100%; @@ -61,11 +59,11 @@ const JuryDecisionTag = styled.small` `; const StyledDivider = styled(Divider)` - margin: 16px 0px; + margin: 16px 0 0; ${landscapeStyle( () => css` - margin: 24px 0px; + margin: 24px 0 0; ` )} `; @@ -81,15 +79,15 @@ const ReStyledArrowLink = styled(StyledArrowLink)` interface IFinalDecision { arbitrable?: `0x${string}`; + votingHistory: VotingHistoryQuery | undefined; } -const FinalDecision: React.FC = ({ arbitrable }) => { +const FinalDecision: React.FC = ({ arbitrable, votingHistory }) => { const { id } = useParams(); const { isDisconnected } = useAccount(); const { data: populatedDisputeData } = usePopulatedDisputeData(id, arbitrable); const { data: disputeDetails } = useDisputeDetailsQuery(id); const { wasDrawn, hasVoted, isLoading, isCommitPeriod, isVotingPeriod, commited, isHiddenVotes } = useVotingContext(); - const { data: votingHistory } = useVotingHistory(id); const localRounds = getLocalRounds(votingHistory?.dispute?.disputeKitDispute); const ruled = disputeDetails?.dispute?.ruled ?? false; const periodIndex = Periods[disputeDetails?.dispute?.period ?? "evidence"]; @@ -101,25 +99,17 @@ const FinalDecision: React.FC = ({ arbitrable }) => { const currentRuling = Number(currentRulingArray?.[0] ?? 0); const answer = populatedDisputeData?.answers?.find((answer) => BigInt(answer.id) === BigInt(currentRuling)); - const rounds = votingHistory?.dispute?.rounds; - const jurorRewardsDispersed = useMemo(() => Boolean(rounds?.every((round) => round.jurorRewardsDispersed)), [rounds]); const buttonText = useMemo(() => { - if (!wasDrawn || isDisconnected) return "Check how the jury voted"; + if (!wasDrawn || isDisconnected) return "Check votes"; if (isCommitPeriod && !commited) return "Commit your vote"; if (isVotingPeriod && isHiddenVotes && commited && !hasVoted) return "Reveal your vote"; if (isVotingPeriod && !isHiddenVotes && !hasVoted) return "Cast your vote"; - return "Check how the jury voted"; + return "Check votes"; }, [wasDrawn, hasVoted, isCommitPeriod, isVotingPeriod, commited, isHiddenVotes, isDisconnected]); return ( - {!isUndefined(Boolean(disputeDetails?.dispute?.ruled)) || jurorRewardsDispersed ? ( - - ) : null} {ruled && ( The jury decided in favor of: @@ -140,15 +130,15 @@ const FinalDecision: React.FC = ({ arbitrable }) => { )} )} + {isLoading && !isDisconnected ? ( + + ) : ( + + {buttonText} + + )} - {isLoading && !isDisconnected ? ( - - ) : ( - - {buttonText} - - )} ); }; diff --git a/web/src/components/Verdict/index.tsx b/web/src/components/Verdict/index.tsx index b3ee84d7e..09c179f81 100644 --- a/web/src/components/Verdict/index.tsx +++ b/web/src/components/Verdict/index.tsx @@ -3,6 +3,8 @@ import styled from "styled-components"; import { responsiveSize } from "styles/responsiveSize"; +import { VotingHistoryQuery } from "src/graphql/graphql"; + import DisputeTimeline from "./DisputeTimeline"; import FinalDecision from "./FinalDecision"; @@ -14,13 +16,14 @@ const Container = styled.div` interface IVerdict { arbitrable?: `0x${string}`; + votingHistory: VotingHistoryQuery | undefined; } -const Verdict: React.FC = ({ arbitrable }) => { +const Verdict: React.FC = ({ arbitrable, votingHistory }) => { return ( - - + + ); }; diff --git a/web/src/pages/AttachmentDisplay/Header.tsx b/web/src/pages/AttachmentDisplay/Header.tsx index 2283e0a0e..18dd6d9af 100644 --- a/web/src/pages/AttachmentDisplay/Header.tsx +++ b/web/src/pages/AttachmentDisplay/Header.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "styled-components"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "@kleros/ui-components-library"; @@ -66,9 +66,20 @@ const StyledButton = styled(Button)` const Header: React.FC<{ title: string }> = ({ title }) => { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const disputeId = searchParams.get("disputeId"); + const attachmentTitle = searchParams.get("title"); const handleReturn = () => { - navigate(-1); + if (attachmentTitle === "Evidence File") { + navigate(`/cases/${disputeId}/evidence`); + } else if (attachmentTitle === "Case Policy" || attachmentTitle === "Dispute Policy") { + navigate(`/cases/${disputeId}/overview`); + } else if (attachmentTitle === "Policy File") { + navigate(`/resolver/policy`); + } else { + navigate("/"); + } }; return ( diff --git a/web/src/pages/Cases/CaseDetails/Overview/index.tsx b/web/src/pages/Cases/CaseDetails/Overview/index.tsx index 32e9cd0f6..0180342c0 100644 --- a/web/src/pages/Cases/CaseDetails/Overview/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Overview/index.tsx @@ -56,10 +56,10 @@ const Overview: React.FC = ({ arbitrable, courtID, currentPeriodIndex return ( <> - + - + { const { fundedChoices } = useFundingContext(); const text = useMemo(() => { - if (loserSideCountdown) - return `${secondsToDayHourMinute(loserSideCountdown)} left until losing options can be funded`; + if (loserSideCountdown) return `${secondsToDayHourMinute(loserSideCountdown)} remaining to fund losing options`; // only show if loosing option was funded and winner needs funding, else no action is needed from user if (winnerSideCountdown && !isUndefined(fundedChoices) && fundedChoices.length > 0) - return `${secondsToDayHourMinute(winnerSideCountdown)} left until winning option can be funded`; + return `${secondsToDayHourMinute(winnerSideCountdown)} remaining to fund winning option`; return; }, [loserSideCountdown, winnerSideCountdown, fundedChoices]); diff --git a/web/src/pages/Courts/CourtDetails/index.tsx b/web/src/pages/Courts/CourtDetails/index.tsx index 29d0070fc..448988711 100644 --- a/web/src/pages/Courts/CourtDetails/index.tsx +++ b/web/src/pages/Courts/CourtDetails/index.tsx @@ -149,11 +149,7 @@ const CourtDetails: React.FC = () => { - + diff --git a/web/src/pages/Home/TopJurors/index.tsx b/web/src/pages/Home/TopJurors/index.tsx index 7e56c86a5..ff8bd798c 100644 --- a/web/src/pages/Home/TopJurors/index.tsx +++ b/web/src/pages/Home/TopJurors/index.tsx @@ -12,14 +12,22 @@ import { SkeletonDisputeListItem } from "components/StyledSkeleton"; import Header from "./Header"; import JurorCard from "./JurorCard"; -import JurorsLeaderboardButton from "components/JurorsLeaderboardButton"; +import SeeAllJurorsButton from "components/SeeAllJurorsButton"; const Container = styled.div` - margin-top: ${responsiveSize(24, 48)}; + margin-top: ${responsiveSize(28, 48)}; `; -const Title = styled.h1` +const TitleAndButtonContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; margin-bottom: ${responsiveSize(12, 24)}; +`; + +const Title = styled.h1` + margin-bottom: 0; font-size: ${responsiveSize(20, 24)}; `; @@ -40,12 +48,6 @@ export const StyledLabel = styled.label` font-size: 16px; `; -const ButtonContainer = styled.div` - display: flex; - margin-top: 16px; - justify-content: center; -`; - const TopJurors: React.FC = () => { const { data: queryJurors } = useJurorsByCoherenceScore(0, 5, "coherenceScore", "desc"); @@ -56,7 +58,10 @@ const TopJurors: React.FC = () => { return ( - Top Jurors + + Top Jurors + + {!isUndefined(topJurors) && topJurors.length === 0 ? ( No jurors found ) : ( @@ -67,9 +72,6 @@ const TopJurors: React.FC = () => { : [...Array(5)].map((_, i) => )} )} - - - ); };