From 1ff55f8d4c01183fd245f518e10d7c9e7b3b3668 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 14 May 2025 17:47:28 +0100 Subject: [PATCH 01/19] docs: natspec for KlerosCore view functions --- contracts/src/arbitration/KlerosCoreBase.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 2bdebe157..6d45381e7 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -981,18 +981,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]; } From 036e89789509612eb59e7a4177c5da88cba06c33 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Mon, 19 May 2025 14:27:37 +0200 Subject: [PATCH 02/19] chore: style and button changes --- web/src/components/AllCasesButton.tsx | 45 ----------------------- web/src/components/LatestCases.tsx | 29 ++++++++------- web/src/components/SeeAllCasesButton.tsx | 24 ++++++++++++ web/src/components/SeeAllJurorsButton.tsx | 16 ++++++++ web/src/pages/Home/TopJurors/index.tsx | 28 +++++++------- 5 files changed, 71 insertions(+), 71 deletions(-) delete mode 100644 web/src/components/AllCasesButton.tsx create mode 100644 web/src/components/SeeAllCasesButton.tsx create mode 100644 web/src/components/SeeAllJurorsButton.tsx 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/LatestCases.tsx b/web/src/components/LatestCases.tsx index 89783a327..6bd1a5219 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,12 +39,6 @@ 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; @@ -49,15 +52,15 @@ const LatestCases: React.FC = ({ title = "Latest Cases", filters, 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/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) => )} )} - - - ); }; From f20c9d43609ac2e4443b0fd79705dcfb48cc413e Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Mon, 19 May 2025 17:08:21 +0200 Subject: [PATCH 03/19] chore: remove unused code --- web/src/components/LatestCases.tsx | 5 ++--- web/src/pages/Courts/CourtDetails/index.tsx | 6 +----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/web/src/components/LatestCases.tsx b/web/src/components/LatestCases.tsx index 6bd1a5219..9f4133607 100644 --- a/web/src/components/LatestCases.tsx +++ b/web/src/components/LatestCases.tsx @@ -42,10 +42,9 @@ const DisputeContainer = styled.div` 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; @@ -54,7 +53,7 @@ const LatestCases: React.FC = ({ title = "Latest Cases", filters, {title} - + {isUndefined(disputes) 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 = () => { - + From 54d83a7a977e691246fe1f4af719251a81211692 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 19 May 2025 16:21:57 +0100 Subject: [PATCH 04/19] fix: avoid unnecessary calls draw() when no juror is available, requires upgrading KlerosCore --- contracts/deploy/upgrade-all.ts | 2 +- contracts/scripts/keeperBot.ts | 15 ++++++++++++++- contracts/src/arbitration/KlerosCoreBase.sol | 4 +++- contracts/src/arbitration/KlerosCoreNeo.sol | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) 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/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/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 6d45381e7..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. diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 6edbede01..f24c2ba5f 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.0"; // ************************************* // // * Storage * // @@ -67,7 +67,7 @@ contract KlerosCoreNeo is KlerosCoreBase { jurorNft = _jurorNft; } - function initialize4() external reinitializer(4) { + function initialize5() external reinitializer(5) { // NOP } From 9e8aa9eeda36986da93f7cf35a0388d1c43445c8 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 19 May 2025 16:30:27 +0100 Subject: [PATCH 05/19] fix: reinitializer and version for KlerosCore --- contracts/src/arbitration/KlerosCore.sol | 4 ++-- contracts/src/arbitration/KlerosCoreNeo.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index f24c2ba5f..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.9.0"; + string public constant override version = "0.9.4"; // ************************************* // // * Storage * // From 7a5466b9e72f82d6d350c48ce49a98e51196dd7e Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Tue, 20 May 2025 15:28:44 +0200 Subject: [PATCH 06/19] chore: retrigger deploy preview --- web/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 29ef9a17dd0739d9d66597f22f1f1c78b2a0682c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 09:19:07 +0000 Subject: [PATCH 07/19] Fix: Update isCurrentRound for previous round on appeal The isCurrentRound field in the Round entity was not being set to false for the previous round when a new round was created due to an appeal. This commit modifies the handleAppealDecision handler in KlerosCore.ts to: 1. Load the round that was current before the appeal. 2. Set its isCurrentRound field to false and save it. 3. Proceed to create the new round, which will have isCurrentRound set to true by the createRoundFromRoundInfo function. A new test suite, kleros-core.test.ts, was added with a specific test case to verify this behavior. The test ensures that after an appeal: - The previous round's isCurrentRound is false. - The new round's isCurrentRound is true. - The dispute's currentRound and currentRoundIndex are correctly updated. - Court statistics are updated. --- subgraph/core/src/KlerosCore.ts | 13 +- subgraph/core/tests/kleros-core-utils.ts | 30 +++ subgraph/core/tests/kleros-core.test.ts | 225 +++++++++++++++++++++++ 3 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 subgraph/core/tests/kleros-core-utils.ts create mode 100644 subgraph/core/tests/kleros-core.test.ts 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/tests/kleros-core-utils.ts b/subgraph/core/tests/kleros-core-utils.ts new file mode 100644 index 000000000..170196c41 --- /dev/null +++ b/subgraph/core/tests/kleros-core-utils.ts @@ -0,0 +1,30 @@ +import { newMockEvent } from "matchstick-as"; +import { ethereum, BigInt, Address } from "@graphprotocol/graph-ts"; +import { AppealDecision } from "../generated/KlerosCore/KlerosCore"; + +export function createAppealDecisionEvent( + disputeID: BigInt, + arbitrable: Address, + blockNumber: BigInt, + timestamp: BigInt, + logIndex: BigInt, + transactionHash: string = "0x0000000000000000000000000000000000000000000000000000000000000000" // Default transaction hash +): AppealDecision { + let appealDecisionEvent = changetype(newMockEvent()); + + appealDecisionEvent.parameters = new Array(); + appealDecisionEvent.block.number = blockNumber; + appealDecisionEvent.block.timestamp = timestamp; + appealDecisionEvent.logIndex = logIndex; + appealDecisionEvent.transaction.hash = Address.fromString(transactionHash); + + + appealDecisionEvent.parameters.push( + new ethereum.EventParam("_disputeID", ethereum.Value.fromUnsignedBigInt(disputeID)) + ); + appealDecisionEvent.parameters.push( + new ethereum.EventParam("_arbitrable", ethereum.Value.fromAddress(arbitrable)) + ); + + return appealDecisionEvent; +} diff --git a/subgraph/core/tests/kleros-core.test.ts b/subgraph/core/tests/kleros-core.test.ts new file mode 100644 index 000000000..6eb135c2d --- /dev/null +++ b/subgraph/core/tests/kleros-core.test.ts @@ -0,0 +1,225 @@ +import { + assert, + describe, + test, + clearStore, + beforeAll, + afterAll, + createMockedFunction, + logStore, // Useful for debugging +} from "matchstick-as/assembly/index"; +import { Address, BigInt, ethereum } from "@graphprotocol/graph-ts"; +import { KlerosCore, AppealDecision } from "../generated/KlerosCore/KlerosCore"; +import { Court, Dispute, Round } from "../generated/schema"; +import { handleAppealDecision } from "../src/KlerosCore"; +import { createAppealDecisionEvent } from "./kleros-core-utils"; // Helper created in previous step +import { ZERO, ONE } from "../src/utils"; // Assuming these are exported from utils + +// Default contract address for KlerosCore mock +const KLEROS_CORE_ADDRESS = Address.fromString("0x0000000000000000000000000000000000000001"); +const ARBITRABLE_ADDRESS = Address.fromString("0x0000000000000000000000000000000000000002"); +const DISPUTE_ID = BigInt.fromI32(0); +const COURT_ID = "0"; // String ID for Court + +describe("KlerosCore - handleAppealDecision", () => { + beforeAll(() => { + clearStore(); // Ensure a clean state + + // 1. Create and save a Court entity + let court = new Court(COURT_ID); + court.hiddenVotes = false; + court.minStake = BigInt.fromI32(100); + court.alpha = BigInt.fromI32(1000); + court.feeForJuror = BigInt.fromI32(10); + court.jurorsForCourtJump = BigInt.fromI32(3); + court.timesPerPeriod = [ + BigInt.fromI32(100), + BigInt.fromI32(100), + BigInt.fromI32(100), + BigInt.fromI32(100), + BigInt.fromI32(100), + ]; + court.supportedDisputeKits = ["1"]; + court.numberDisputes = ZERO; + court.numberClosedDisputes = ZERO; + court.numberAppealingDisputes = ZERO; + court.numberVotingDisputes = ZERO; + court.frozenToken = ZERO; + court.activeJurors = ZERO; + court.inactiveJurors = ZERO; + court.drawnJurors = ZERO; + court.tokenStaked = ZERO; + court.totalCoherentJurors = ZERO; + court.totalResolvedDisputes = ZERO; + court.appealPeriod = [ZERO,ZERO,ZERO,ZERO]; + court.numberCoherentVotes = ZERO; + court.numberIncoherentVotes = ZERO; + court.disputesWithoutVotes = ZERO; + court.numberDelayedStakes = ZERO; + court.delayedStakeAmount = ZERO; + court.totalStake = ZERO; + court.numberVotes = ZERO; // Will be updated + court.save(); + + // 2. Create and save a Dispute entity + let dispute = new Dispute(DISPUTE_ID.toString()); + dispute.court = COURT_ID; + dispute.arbitrable = ARBITRABLE_ADDRESS.toHexString(); + dispute.numberOfChoices = BigInt.fromI32(2); + dispute.period = "evidence"; + dispute.lastPeriodChange = BigInt.fromI32(1000); + dispute.lastPeriodChangeBlockNumber = BigInt.fromI32(1); + dispute.periodNotificationIndex = ZERO; + dispute.periodDeadline = BigInt.fromI32(1100); + dispute.drawsInRound = ZERO; + dispute.commitsInRound = ZERO; + dispute.currentRuling = ZERO; + dispute.tied = true; + dispute.overridden = false; + dispute.currentRoundIndex = ZERO; // Starts at round 0 + dispute.currentRound = `${DISPUTE_ID.toString()}-0`; // Initial round ID + dispute.creator = Address.fromString("0x0000000000000000000000000000000000000003").toHexString(); + dispute.subcourtID = ZERO; + dispute.category = ZERO; + dispute.jurors = []; + dispute.rewardsFund = ZERO; + dispute.totalFeesForJurors = ZERO; + dispute.totalStaked = ZERO; + dispute.rounds = []; + dispute.ruled = false; + dispute.save(); + + // 3. Create and save an initial Round entity (round0) + let round0ID = `${DISPUTE_ID.toString()}-0`; + let round0 = new Round(round0ID); + round0.dispute = DISPUTE_ID.toString(); + round0.disputeKit = "1"; // Assume classic + round0.court = COURT_ID; + round0.pnkAtStake = BigInt.fromI32(1000); + round0.raisedSoFar = BigInt.fromI32(500); + round0.feeForJuror = BigInt.fromI32(10); + round0.drawnJurors = []; // Initialize as empty + round0.nbVotes = BigInt.fromI32(3); // Number of votes for this round + round0.repartitions = ZERO; + round0.totalAmount = BigInt.fromI32(1500); + round0.deadline = BigInt.fromI32(1200); + round0.isCurrentRound = true; // Crucial for the test + round0.jurorsDrawn = false; + round0.jurorRewardsDispersed = false; + round0.winningChoice = ZERO; + round0.tied = true; + round0.totalVoted = ZERO; + round0.fundedChoices = []; + round0.save(); + + // Link round0 to dispute's rounds array (if you store it like that) + dispute.rounds = dispute.rounds.concat([round0.id]); + dispute.save(); + + + // 4. Mock contract calls + // Mock KlerosCore.bind() + createMockedFunction(KLEROS_CORE_ADDRESS, "bind", "bind(address):(address)") + .withArgs([ethereum.Value.fromAddress(KLEROS_CORE_ADDRESS)]) + .returns([ethereum.Value.fromAddress(KLEROS_CORE_ADDRESS)]); + + // Mock contract.disputes(disputeID) + // disputes(uint256) returns (address,uint128,uint128,uint16,uint256,uint8,uint8) + // (courtID, firstRoundID, lastRoundID, numberOfAppeals, jurorsNumber, jurorsForJump, arbitrableChainID) + // We only care about courtID for this handler, which is disputeStorage.value0 + let disputeTuple = new ethereum.Tuple(); + disputeTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromString(COURT_ID))); // courtID (value0) + disputeTuple.push(ethereum.Value.fromUnsignedBigInt(ZERO)); // firstRoundID + disputeTuple.push(ethereum.Value.fromUnsignedBigInt(ZERO)); // lastRoundID + disputeTuple.push(ethereum.Value.fromUnsignedBigInt(ZERO)); // numberOfAppeals + disputeTuple.push(ethereum.Value.fromUnsignedBigInt(ZERO)); // jurorsNumber + disputeTuple.push(ethereum.Value.fromI32(0)); // jurorsForJump (uint8) + disputeTuple.push(ethereum.Value.fromI32(0)); // arbitrableChainID (uint8) + + createMockedFunction(KLEROS_CORE_ADDRESS, "disputes", "disputes(uint256):(uint256,uint128,uint128,uint16,uint256,uint8,uint8)") + .withArgs([ethereum.Value.fromUnsignedBigInt(DISPUTE_ID)]) + .returns([ethereum.Value.fromTuple(disputeTuple)]); + + // Mock contract.getRoundInfo(disputeID, newRoundIndex) for the *new* round (round 1) + // getRoundInfo(uint256,uint256) returns (uint256,uint256,uint256,address[],uint256,uint256,uint256,uint256) + // (pnkAtStake, raisedSoFar, feeForJuror, drawnJurors, nbVotes, repartitions, totalAmount, deadline) + let newRoundIndex = ONE; + let roundInfoTuple = new ethereum.Tuple(); + roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(2000))); // pnkAtStake for round 1 + roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(1000))); // raisedSoFar for round 1 + roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(20))); // feeForJuror for round 1 + roundInfoTuple.push(ethereum.Value.fromAddressArray([])); // drawnJurors for round 1 + roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(5))); // nbVotes for round 1 (IMPORTANT for court.numberVotes update) + roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(ZERO)); // repartitions for round 1 + roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(3000))); // totalAmount for round 1 + roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(2000))); // deadline for round 1 + + createMockedFunction(KLEROS_CORE_ADDRESS, "getRoundInfo", "getRoundInfo(uint256,uint256):(uint256,uint256,uint256,address[],uint256,uint256,uint256,uint256)") + .withArgs([ + ethereum.Value.fromUnsignedBigInt(DISPUTE_ID), + ethereum.Value.fromUnsignedBigInt(newRoundIndex) // newRoundIndex will be 1 + ]) + .returns([ethereum.Value.fromTuple(roundInfoTuple)]); + }); + + afterAll(() => { + clearStore(); + }); + + test("Should correctly update rounds on appeal decision", () => { + // 1. Create AppealDecision event + let appealTime = BigInt.fromI32(1500); + let appealBlock = BigInt.fromI32(10); + let appealLogIndex = BigInt.fromI32(1); + let appealEvent = createAppealDecisionEvent( + DISPUTE_ID, + ARBITRABLE_ADDRESS, + appealBlock, + appealTime, + appealLogIndex + ); + appealEvent.address = KLEROS_CORE_ADDRESS; // Set the event address to match mocked contract + + // Store initial court votes + let court = Court.load(COURT_ID)!; + let initialCourtVotes = court.numberVotes; + + // 2. Call handleAppealDecision + handleAppealDecision(appealEvent); + + // 3. Assertions + // Assert round0.isCurrentRound is false + let round0ID = `${DISPUTE_ID.toString()}-0`; + assert.fieldEquals("Round", round0ID, "isCurrentRound", "false"); + + // Load the Dispute + let dispute = Dispute.load(DISPUTE_ID.toString())!; + assert.assertNotNull(dispute); + + // Assert dispute.currentRoundIndex == ONE + assert.bigIntEquals(ONE, dispute.currentRoundIndex); + + // Assert a new Round (round1) is created and isCurrentRound is true + let round1ID = `${DISPUTE_ID.toString()}-1`; + assert.entityCount("Round", 2); // round0 and round1 + assert.fieldEquals("Round", round1ID, "isCurrentRound", "true"); + assert.fieldEquals("Round", round1ID, "dispute", DISPUTE_ID.toString()); + assert.fieldEquals("Round", round1ID, "court", COURT_ID); + // Check some values from mocked getRoundInfo for round1 + assert.fieldEquals("Round", round1ID, "pnkAtStake", "2000"); + assert.fieldEquals("Round", round1ID, "raisedSoFar", "1000"); + assert.fieldEquals("Round", round1ID, "feeForJuror", "20"); + assert.fieldEquals("Round", round1ID, "nbVotes", "5"); // From mocked getRoundInfo for new round + + // Assert dispute.currentRound points to round1 + assert.stringEquals(round1ID, dispute.currentRound); + + // Assert court.numberVotes has been updated + // Initial court votes + nbVotes from new round (mocked as 5) + let expectedCourtVotes = initialCourtVotes.plus(BigInt.fromI32(5)); + assert.fieldEquals("Court", COURT_ID, "numberVotes", expectedCourtVotes.toString()); + + // Optional: Log store to see entities - useful for debugging + // logStore(); + }); +}); From 3925b6f8cf6bee36011480836f11c1108377eed5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 10:46:01 +0000 Subject: [PATCH 08/19] Fix: Update isCurrentRound for previous round on appeal The isCurrentRound field in the Round entity was not being set to false for the previous round when a new round was created due to an appeal. This commit modifies the handleAppealDecision handler in KlerosCore.ts to: 1. Load the round that was current before the appeal. 2. Set its isCurrentRound field to false and save it. 3. Proceed to create the new round, which will have isCurrentRound set to true by the createRoundFromRoundInfo function. Newly added test files (kleros-core.test.ts and kleros-core-utils.ts) were removed from this commit due to feedback regarding outdated testing tooling. The core logic fix remains. --- subgraph/core/tests/kleros-core-utils.ts | 30 --- subgraph/core/tests/kleros-core.test.ts | 225 ----------------------- 2 files changed, 255 deletions(-) delete mode 100644 subgraph/core/tests/kleros-core-utils.ts delete mode 100644 subgraph/core/tests/kleros-core.test.ts diff --git a/subgraph/core/tests/kleros-core-utils.ts b/subgraph/core/tests/kleros-core-utils.ts deleted file mode 100644 index 170196c41..000000000 --- a/subgraph/core/tests/kleros-core-utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { newMockEvent } from "matchstick-as"; -import { ethereum, BigInt, Address } from "@graphprotocol/graph-ts"; -import { AppealDecision } from "../generated/KlerosCore/KlerosCore"; - -export function createAppealDecisionEvent( - disputeID: BigInt, - arbitrable: Address, - blockNumber: BigInt, - timestamp: BigInt, - logIndex: BigInt, - transactionHash: string = "0x0000000000000000000000000000000000000000000000000000000000000000" // Default transaction hash -): AppealDecision { - let appealDecisionEvent = changetype(newMockEvent()); - - appealDecisionEvent.parameters = new Array(); - appealDecisionEvent.block.number = blockNumber; - appealDecisionEvent.block.timestamp = timestamp; - appealDecisionEvent.logIndex = logIndex; - appealDecisionEvent.transaction.hash = Address.fromString(transactionHash); - - - appealDecisionEvent.parameters.push( - new ethereum.EventParam("_disputeID", ethereum.Value.fromUnsignedBigInt(disputeID)) - ); - appealDecisionEvent.parameters.push( - new ethereum.EventParam("_arbitrable", ethereum.Value.fromAddress(arbitrable)) - ); - - return appealDecisionEvent; -} diff --git a/subgraph/core/tests/kleros-core.test.ts b/subgraph/core/tests/kleros-core.test.ts deleted file mode 100644 index 6eb135c2d..000000000 --- a/subgraph/core/tests/kleros-core.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { - assert, - describe, - test, - clearStore, - beforeAll, - afterAll, - createMockedFunction, - logStore, // Useful for debugging -} from "matchstick-as/assembly/index"; -import { Address, BigInt, ethereum } from "@graphprotocol/graph-ts"; -import { KlerosCore, AppealDecision } from "../generated/KlerosCore/KlerosCore"; -import { Court, Dispute, Round } from "../generated/schema"; -import { handleAppealDecision } from "../src/KlerosCore"; -import { createAppealDecisionEvent } from "./kleros-core-utils"; // Helper created in previous step -import { ZERO, ONE } from "../src/utils"; // Assuming these are exported from utils - -// Default contract address for KlerosCore mock -const KLEROS_CORE_ADDRESS = Address.fromString("0x0000000000000000000000000000000000000001"); -const ARBITRABLE_ADDRESS = Address.fromString("0x0000000000000000000000000000000000000002"); -const DISPUTE_ID = BigInt.fromI32(0); -const COURT_ID = "0"; // String ID for Court - -describe("KlerosCore - handleAppealDecision", () => { - beforeAll(() => { - clearStore(); // Ensure a clean state - - // 1. Create and save a Court entity - let court = new Court(COURT_ID); - court.hiddenVotes = false; - court.minStake = BigInt.fromI32(100); - court.alpha = BigInt.fromI32(1000); - court.feeForJuror = BigInt.fromI32(10); - court.jurorsForCourtJump = BigInt.fromI32(3); - court.timesPerPeriod = [ - BigInt.fromI32(100), - BigInt.fromI32(100), - BigInt.fromI32(100), - BigInt.fromI32(100), - BigInt.fromI32(100), - ]; - court.supportedDisputeKits = ["1"]; - court.numberDisputes = ZERO; - court.numberClosedDisputes = ZERO; - court.numberAppealingDisputes = ZERO; - court.numberVotingDisputes = ZERO; - court.frozenToken = ZERO; - court.activeJurors = ZERO; - court.inactiveJurors = ZERO; - court.drawnJurors = ZERO; - court.tokenStaked = ZERO; - court.totalCoherentJurors = ZERO; - court.totalResolvedDisputes = ZERO; - court.appealPeriod = [ZERO,ZERO,ZERO,ZERO]; - court.numberCoherentVotes = ZERO; - court.numberIncoherentVotes = ZERO; - court.disputesWithoutVotes = ZERO; - court.numberDelayedStakes = ZERO; - court.delayedStakeAmount = ZERO; - court.totalStake = ZERO; - court.numberVotes = ZERO; // Will be updated - court.save(); - - // 2. Create and save a Dispute entity - let dispute = new Dispute(DISPUTE_ID.toString()); - dispute.court = COURT_ID; - dispute.arbitrable = ARBITRABLE_ADDRESS.toHexString(); - dispute.numberOfChoices = BigInt.fromI32(2); - dispute.period = "evidence"; - dispute.lastPeriodChange = BigInt.fromI32(1000); - dispute.lastPeriodChangeBlockNumber = BigInt.fromI32(1); - dispute.periodNotificationIndex = ZERO; - dispute.periodDeadline = BigInt.fromI32(1100); - dispute.drawsInRound = ZERO; - dispute.commitsInRound = ZERO; - dispute.currentRuling = ZERO; - dispute.tied = true; - dispute.overridden = false; - dispute.currentRoundIndex = ZERO; // Starts at round 0 - dispute.currentRound = `${DISPUTE_ID.toString()}-0`; // Initial round ID - dispute.creator = Address.fromString("0x0000000000000000000000000000000000000003").toHexString(); - dispute.subcourtID = ZERO; - dispute.category = ZERO; - dispute.jurors = []; - dispute.rewardsFund = ZERO; - dispute.totalFeesForJurors = ZERO; - dispute.totalStaked = ZERO; - dispute.rounds = []; - dispute.ruled = false; - dispute.save(); - - // 3. Create and save an initial Round entity (round0) - let round0ID = `${DISPUTE_ID.toString()}-0`; - let round0 = new Round(round0ID); - round0.dispute = DISPUTE_ID.toString(); - round0.disputeKit = "1"; // Assume classic - round0.court = COURT_ID; - round0.pnkAtStake = BigInt.fromI32(1000); - round0.raisedSoFar = BigInt.fromI32(500); - round0.feeForJuror = BigInt.fromI32(10); - round0.drawnJurors = []; // Initialize as empty - round0.nbVotes = BigInt.fromI32(3); // Number of votes for this round - round0.repartitions = ZERO; - round0.totalAmount = BigInt.fromI32(1500); - round0.deadline = BigInt.fromI32(1200); - round0.isCurrentRound = true; // Crucial for the test - round0.jurorsDrawn = false; - round0.jurorRewardsDispersed = false; - round0.winningChoice = ZERO; - round0.tied = true; - round0.totalVoted = ZERO; - round0.fundedChoices = []; - round0.save(); - - // Link round0 to dispute's rounds array (if you store it like that) - dispute.rounds = dispute.rounds.concat([round0.id]); - dispute.save(); - - - // 4. Mock contract calls - // Mock KlerosCore.bind() - createMockedFunction(KLEROS_CORE_ADDRESS, "bind", "bind(address):(address)") - .withArgs([ethereum.Value.fromAddress(KLEROS_CORE_ADDRESS)]) - .returns([ethereum.Value.fromAddress(KLEROS_CORE_ADDRESS)]); - - // Mock contract.disputes(disputeID) - // disputes(uint256) returns (address,uint128,uint128,uint16,uint256,uint8,uint8) - // (courtID, firstRoundID, lastRoundID, numberOfAppeals, jurorsNumber, jurorsForJump, arbitrableChainID) - // We only care about courtID for this handler, which is disputeStorage.value0 - let disputeTuple = new ethereum.Tuple(); - disputeTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromString(COURT_ID))); // courtID (value0) - disputeTuple.push(ethereum.Value.fromUnsignedBigInt(ZERO)); // firstRoundID - disputeTuple.push(ethereum.Value.fromUnsignedBigInt(ZERO)); // lastRoundID - disputeTuple.push(ethereum.Value.fromUnsignedBigInt(ZERO)); // numberOfAppeals - disputeTuple.push(ethereum.Value.fromUnsignedBigInt(ZERO)); // jurorsNumber - disputeTuple.push(ethereum.Value.fromI32(0)); // jurorsForJump (uint8) - disputeTuple.push(ethereum.Value.fromI32(0)); // arbitrableChainID (uint8) - - createMockedFunction(KLEROS_CORE_ADDRESS, "disputes", "disputes(uint256):(uint256,uint128,uint128,uint16,uint256,uint8,uint8)") - .withArgs([ethereum.Value.fromUnsignedBigInt(DISPUTE_ID)]) - .returns([ethereum.Value.fromTuple(disputeTuple)]); - - // Mock contract.getRoundInfo(disputeID, newRoundIndex) for the *new* round (round 1) - // getRoundInfo(uint256,uint256) returns (uint256,uint256,uint256,address[],uint256,uint256,uint256,uint256) - // (pnkAtStake, raisedSoFar, feeForJuror, drawnJurors, nbVotes, repartitions, totalAmount, deadline) - let newRoundIndex = ONE; - let roundInfoTuple = new ethereum.Tuple(); - roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(2000))); // pnkAtStake for round 1 - roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(1000))); // raisedSoFar for round 1 - roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(20))); // feeForJuror for round 1 - roundInfoTuple.push(ethereum.Value.fromAddressArray([])); // drawnJurors for round 1 - roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(5))); // nbVotes for round 1 (IMPORTANT for court.numberVotes update) - roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(ZERO)); // repartitions for round 1 - roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(3000))); // totalAmount for round 1 - roundInfoTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(2000))); // deadline for round 1 - - createMockedFunction(KLEROS_CORE_ADDRESS, "getRoundInfo", "getRoundInfo(uint256,uint256):(uint256,uint256,uint256,address[],uint256,uint256,uint256,uint256)") - .withArgs([ - ethereum.Value.fromUnsignedBigInt(DISPUTE_ID), - ethereum.Value.fromUnsignedBigInt(newRoundIndex) // newRoundIndex will be 1 - ]) - .returns([ethereum.Value.fromTuple(roundInfoTuple)]); - }); - - afterAll(() => { - clearStore(); - }); - - test("Should correctly update rounds on appeal decision", () => { - // 1. Create AppealDecision event - let appealTime = BigInt.fromI32(1500); - let appealBlock = BigInt.fromI32(10); - let appealLogIndex = BigInt.fromI32(1); - let appealEvent = createAppealDecisionEvent( - DISPUTE_ID, - ARBITRABLE_ADDRESS, - appealBlock, - appealTime, - appealLogIndex - ); - appealEvent.address = KLEROS_CORE_ADDRESS; // Set the event address to match mocked contract - - // Store initial court votes - let court = Court.load(COURT_ID)!; - let initialCourtVotes = court.numberVotes; - - // 2. Call handleAppealDecision - handleAppealDecision(appealEvent); - - // 3. Assertions - // Assert round0.isCurrentRound is false - let round0ID = `${DISPUTE_ID.toString()}-0`; - assert.fieldEquals("Round", round0ID, "isCurrentRound", "false"); - - // Load the Dispute - let dispute = Dispute.load(DISPUTE_ID.toString())!; - assert.assertNotNull(dispute); - - // Assert dispute.currentRoundIndex == ONE - assert.bigIntEquals(ONE, dispute.currentRoundIndex); - - // Assert a new Round (round1) is created and isCurrentRound is true - let round1ID = `${DISPUTE_ID.toString()}-1`; - assert.entityCount("Round", 2); // round0 and round1 - assert.fieldEquals("Round", round1ID, "isCurrentRound", "true"); - assert.fieldEquals("Round", round1ID, "dispute", DISPUTE_ID.toString()); - assert.fieldEquals("Round", round1ID, "court", COURT_ID); - // Check some values from mocked getRoundInfo for round1 - assert.fieldEquals("Round", round1ID, "pnkAtStake", "2000"); - assert.fieldEquals("Round", round1ID, "raisedSoFar", "1000"); - assert.fieldEquals("Round", round1ID, "feeForJuror", "20"); - assert.fieldEquals("Round", round1ID, "nbVotes", "5"); // From mocked getRoundInfo for new round - - // Assert dispute.currentRound points to round1 - assert.stringEquals(round1ID, dispute.currentRound); - - // Assert court.numberVotes has been updated - // Initial court votes + nbVotes from new round (mocked as 5) - let expectedCourtVotes = initialCourtVotes.plus(BigInt.fromI32(5)); - assert.fieldEquals("Court", COURT_ID, "numberVotes", expectedCourtVotes.toString()); - - // Optional: Log store to see entities - useful for debugging - // logStore(); - }); -}); From ad47783fd13ddd657b025bf5c937db7abc9fd69e Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Mon, 26 May 2025 14:46:14 +0200 Subject: [PATCH 09/19] chore: timeline clarify remaining time --- web/src/pages/Cases/CaseDetails/Timeline.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/src/pages/Cases/CaseDetails/Timeline.tsx b/web/src/pages/Cases/CaseDetails/Timeline.tsx index 7a90b25b8..dfaed7e82 100644 --- a/web/src/pages/Cases/CaseDetails/Timeline.tsx +++ b/web/src/pages/Cases/CaseDetails/Timeline.tsx @@ -83,11 +83,10 @@ const AppealBanner: React.FC = () => { 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]); From f370669c57740f884bd438696c5b85a74707dd22 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Tue, 27 May 2025 05:29:46 +0530 Subject: [PATCH 10/19] fix(subgraph): handle-batched-disputes-request-events --- subgraph/core/src/entities/Dispute.ts | 30 +++++++++++++++++++++++++-- subgraph/package.json | 2 +- 2 files changed, 29 insertions(+), 3 deletions(-) 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..e63da74b7 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.3", "drtVersion": "0.12.0", "license": "MIT", "scripts": { From 20ee602882b74bab919bfe55a654b9bc7016645d Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Wed, 28 May 2025 01:25:55 +0200 Subject: [PATCH 11/19] fix: bug in color loading text in file viewer --- web/src/components/FileViewer/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) 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}; + } `; /** From 367549c78e67ae972c0ef89fd2dfdf69fe4b1ee8 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 28 May 2025 11:11:33 +0100 Subject: [PATCH 12/19] chore: subgraph version bump --- subgraph/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subgraph/package.json b/subgraph/package.json index e63da74b7..05ab7235a 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-subgraph", - "version": "0.15.3", + "version": "0.15.4", "drtVersion": "0.12.0", "license": "MIT", "scripts": { From e7a0759d24886cf875ccec610d371e664e63752b Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Wed, 28 May 2025 17:40:04 +0200 Subject: [PATCH 13/19] fix: allow return in case evidence was opened in new tab --- web/src/components/EvidenceCard.tsx | 4 +++- web/src/pages/AttachmentDisplay/Header.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) 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/pages/AttachmentDisplay/Header.tsx b/web/src/pages/AttachmentDisplay/Header.tsx index 2283e0a0e..ae622af24 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,11 @@ const StyledButton = styled(Button)` const Header: React.FC<{ title: string }> = ({ title }) => { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const disputeId = searchParams.get("disputeId"); const handleReturn = () => { - navigate(-1); + navigate(`/cases/${disputeId}/evidence`); }; return ( From dde0dc7844383695259a1a4f2f3b581219019b3e Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Wed, 28 May 2025 18:18:07 +0200 Subject: [PATCH 14/19] fix: few more links handling --- web/src/components/DisputePreview/Policies.tsx | 12 ++++++++++-- web/src/pages/AttachmentDisplay/Header.tsx | 11 ++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) 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/pages/AttachmentDisplay/Header.tsx b/web/src/pages/AttachmentDisplay/Header.tsx index ae622af24..18dd6d9af 100644 --- a/web/src/pages/AttachmentDisplay/Header.tsx +++ b/web/src/pages/AttachmentDisplay/Header.tsx @@ -68,9 +68,18 @@ 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(`/cases/${disputeId}/evidence`); + 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 ( From 2b51d5e0990c302b0b940318e14f2ec35c23700f Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 28 May 2025 19:54:45 +0100 Subject: [PATCH 15/19] chore: find-initializer-versions utility --- .../scripts/find-initializer-versions.sh | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 contracts/scripts/find-initializer-versions.sh diff --git a/contracts/scripts/find-initializer-versions.sh b/contracts/scripts/find-initializer-versions.sh new file mode 100755 index 000000000..c8f7aacc6 --- /dev/null +++ b/contracts/scripts/find-initializer-versions.sh @@ -0,0 +1,23 @@ +#! /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) + +for c in arbitrum arbitrumSepolia arbitrumSepoliaDevnet; do + echo "$c" + 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 0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2 --address "$address" --rpc-url "${rpcUrls[$c]}" --json | jq -r .[].data) + for result in $results; do + cast --to-dec "$result" + done + echo + done + echo "--------------------------------" +done From 4adedb5f9cc083c82c9eedcd71c953a2da4dd1a2 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 28 May 2025 20:56:00 +0100 Subject: [PATCH 16/19] chore: find-initializer-versions utility --- contracts/scripts/find-initializer-versions.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/scripts/find-initializer-versions.sh b/contracts/scripts/find-initializer-versions.sh index c8f7aacc6..322929812 100755 --- a/contracts/scripts/find-initializer-versions.sh +++ b/contracts/scripts/find-initializer-versions.sh @@ -7,17 +7,21 @@ 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 0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2 --address "$address" --rpc-url "${rpcUrls[$c]}" --json | jq -r .[].data) - for result in $results; do - cast --to-dec "$result" - done + 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 - echo "--------------------------------" done From 47aaca4d12f93859573dc764a11144819a55ad35 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Wed, 11 Jun 2025 01:31:25 +0200 Subject: [PATCH 17/19] feat: new overview design --- web/src/assets/svgs/icons/gavel-executed.svg | 11 + .../DisputePreview/DisputeContext.tsx | 40 +++- .../components/Verdict/DisputeTimeline.tsx | 194 ++++++++---------- web/src/components/Verdict/FinalDecision.tsx | 38 ++-- web/src/components/Verdict/index.tsx | 9 +- .../Cases/CaseDetails/Overview/index.tsx | 4 +- 6 files changed, 152 insertions(+), 144 deletions(-) create mode 100644 web/src/assets/svgs/icons/gavel-executed.svg 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/DisputePreview/DisputeContext.tsx b/web/src/components/DisputePreview/DisputeContext.tsx index d5e7130d1..022d69518 100644 --- a/web/src/components/DisputePreview/DisputeContext.tsx +++ b/web/src/components/DisputePreview/DisputeContext.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import styled from "styled-components"; import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; @@ -9,6 +9,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 +18,21 @@ import { Divider } from "../Divider"; import { ExternalLink } from "../ExternalLink"; import AliasDisplay from "./Alias"; +import RulingAndRewardsIndicators from "../Verdict/RulingAndRewardsIndicators"; 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; @@ -68,17 +77,36 @@ const AliasesContainer = styled.div` interface IDisputeContext { disputeDetails?: DisputeDetails; + dispute: DisputeDetailsQuery | undefined; isRpcError?: boolean; + votingHistory: VotingHistoryQuery | undefined; } -export const DisputeContext: React.FC = ({ disputeDetails, isRpcError = false }) => { +export const DisputeContext: React.FC = ({ + disputeDetails, + dispute, + isRpcError = false, + votingHistory, +}) => { 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(Boolean(dispute?.dispute?.ruled)) || jurorRewardsDispersed ? ( + + ) : null} + + {disputeDetails?.question?.trim() || disputeDetails?.description?.trim() ? (
{disputeDetails?.question?.trim() ? ( 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/Cases/CaseDetails/Overview/index.tsx b/web/src/pages/Cases/CaseDetails/Overview/index.tsx index 32e9cd0f6..817129d9e 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 ( <> - + - + Date: Wed, 11 Jun 2025 02:02:24 +0200 Subject: [PATCH 18/19] feat: add card labels to the overview --- .../DisputePreview/DisputeContext.tsx | 29 +++++++++++++++---- .../DisputeView/CardLabels/index.tsx | 17 +++++++++-- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/web/src/components/DisputePreview/DisputeContext.tsx b/web/src/components/DisputePreview/DisputeContext.tsx index 022d69518..101f4e2e2 100644 --- a/web/src/components/DisputePreview/DisputeContext.tsx +++ b/web/src/components/DisputePreview/DisputeContext.tsx @@ -2,6 +2,8 @@ import React, { useMemo } from "react"; import styled from "styled-components"; import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; +import { useParams } from "react-router-dom"; +import { useAccount } from "wagmi"; import { INVALID_DISPUTE_DATA_ERROR, RPC_ERROR } from "consts/index"; import { Answer as IAnswer } from "context/NewDisputeContext"; @@ -19,6 +21,7 @@ import { ExternalLink } from "../ExternalLink"; import AliasDisplay from "./Alias"; import RulingAndRewardsIndicators from "../Verdict/RulingAndRewardsIndicators"; +import CardLabel from "../DisputeView/CardLabels"; const StyledH1 = styled.h1` margin: 0; @@ -75,6 +78,13 @@ 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; dispute: DisputeDetailsQuery | undefined; @@ -88,6 +98,8 @@ export const DisputeContext: React.FC = ({ isRpcError = false, votingHistory, }) => { + const { id } = useParams(); + 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]); @@ -99,12 +111,17 @@ export const DisputeContext: React.FC = ({ {isUndefined(disputeDetails) ? : (disputeDetails?.title ?? errMsg)} - {!isUndefined(Boolean(dispute?.dispute?.ruled)) || jurorRewardsDispersed ? ( - - ) : null} + + {!isUndefined(Boolean(dispute?.dispute?.ruled)) || jurorRewardsDispersed ? ( + + ) : null} + {!isDisconnected ? ( + + ) : null} + {disputeDetails?.question?.trim() || disputeDetails?.description?.trim() ? ( 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 ? ( ) : ( From f5f25303d7caf5fd0b7a7153fd45e0888121e68c Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Wed, 11 Jun 2025 02:34:47 +0200 Subject: [PATCH 19/19] chore: tweak --- .../DisputePreview/DisputeContext.tsx | 38 +++++++++++-------- .../DisputeInfo/DisputeInfoCard.tsx | 2 +- .../Cases/CaseDetails/Overview/index.tsx | 2 +- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/web/src/components/DisputePreview/DisputeContext.tsx b/web/src/components/DisputePreview/DisputeContext.tsx index 101f4e2e2..435bc47a8 100644 --- a/web/src/components/DisputePreview/DisputeContext.tsx +++ b/web/src/components/DisputePreview/DisputeContext.tsx @@ -2,7 +2,6 @@ import React, { useMemo } from "react"; import styled from "styled-components"; import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; -import { useParams } from "react-router-dom"; import { useAccount } from "wagmi"; import { INVALID_DISPUTE_DATA_ERROR, RPC_ERROR } from "consts/index"; @@ -87,18 +86,20 @@ const RulingAndRewardsAndLabels = styled.div` interface IDisputeContext { disputeDetails?: DisputeDetails; - dispute: DisputeDetailsQuery | undefined; isRpcError?: boolean; - votingHistory: VotingHistoryQuery | undefined; + dispute?: DisputeDetailsQuery | undefined; + + disputeId?: string; + votingHistory?: VotingHistoryQuery | undefined; } export const DisputeContext: React.FC = ({ disputeDetails, - dispute, isRpcError = false, + dispute, + disputeId, votingHistory, }) => { - const { id } = useParams(); const { isDisconnected } = useAccount(); const errMsg = isRpcError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR; const rounds = votingHistory?.dispute?.rounds; @@ -111,17 +112,22 @@ export const DisputeContext: React.FC = ({ {isUndefined(disputeDetails) ? : (disputeDetails?.title ?? errMsg)} - - {!isUndefined(Boolean(dispute?.dispute?.ruled)) || jurorRewardsDispersed ? ( - - ) : null} - {!isDisconnected ? ( - - ) : null} - + {!isUndefined(disputeDetails) && + !isUndefined(dispute) && + !isUndefined(disputeId) && + !isUndefined(votingHistory) ? ( + + {!isUndefined(Boolean(dispute?.dispute?.ruled)) || jurorRewardsDispersed ? ( + + ) : null} + {!isDisconnected ? ( + + ) : null} + + ) : null} {disputeDetails?.question?.trim() || disputeDetails?.description?.trim() ? ( 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/pages/Cases/CaseDetails/Overview/index.tsx b/web/src/pages/Cases/CaseDetails/Overview/index.tsx index 817129d9e..0180342c0 100644 --- a/web/src/pages/Cases/CaseDetails/Overview/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Overview/index.tsx @@ -56,7 +56,7 @@ const Overview: React.FC = ({ arbitrable, courtID, currentPeriodIndex return ( <> - +