diff --git a/subgraph/core-neo/subgraph.yaml b/subgraph/core-neo/subgraph.yaml index 7ea2979cc..37feb1f7d 100644 --- a/subgraph/core-neo/subgraph.yaml +++ b/subgraph/core-neo/subgraph.yaml @@ -8,9 +8,9 @@ dataSources: name: KlerosCore network: arbitrum-one source: - address: "0xCd415C03dfa85B02646C7e2977F22a480c4354F1" + address: "0x991d2df165670b9cac3B022f4B68D65b664222ea" abi: KlerosCore - startBlock: 190274596 + startBlock: 272063254 mapping: kind: ethereum/events apiVersion: 0.0.7 @@ -64,9 +64,9 @@ dataSources: name: PolicyRegistry network: arbitrum-one source: - address: "0x26c1980120F1C82cF611D666CE81D2b54d018547" + address: "0x553dcbF6aB3aE06a1064b5200Df1B5A9fB403d3c" abi: PolicyRegistry - startBlock: 190274403 + startBlock: 272063037 mapping: kind: ethereum/events apiVersion: 0.0.7 @@ -84,9 +84,9 @@ dataSources: name: DisputeKitClassic network: arbitrum-one source: - address: "0xb7c292cD9Fd3d20De84a71AE1caF054eEB6374A9" + address: "0x70B464be85A547144C72485eBa2577E5D3A45421" abi: DisputeKitClassic - startBlock: 190274518 + startBlock: 272063168 mapping: kind: ethereum/events apiVersion: 0.0.7 @@ -119,9 +119,9 @@ dataSources: name: EvidenceModule network: arbitrum-one source: - address: "0xe62B776498F48061ef9425fCEf30F3d1370DB005" + address: "0x48e052B4A6dC4F30e90930F1CeaAFd83b3981EB3" abi: EvidenceModule - startBlock: 190274441 + startBlock: 272063086 mapping: kind: ethereum/events apiVersion: 0.0.7 @@ -140,9 +140,9 @@ dataSources: name: SortitionModule network: arbitrum-one source: - address: "0x614498118850184c62f82d08261109334bFB050f" + address: "0x21A9402aDb818744B296e1d1BE58C804118DC03D" abi: SortitionModule - startBlock: 190274557 + startBlock: 272063201 mapping: kind: ethereum/events apiVersion: 0.0.7 diff --git a/subgraph/core/schema.graphql b/subgraph/core/schema.graphql index e2700a5f3..dc34b33b3 100644 --- a/subgraph/core/schema.graphql +++ b/subgraph/core/schema.graphql @@ -160,6 +160,7 @@ type Dispute @entity { disputeID: BigInt! court: Court! createdAt: BigInt + transactionHash: String! arbitrated: Arbitrable! period: Period! ruled: Boolean! @@ -180,6 +181,8 @@ type Dispute @entity { arbitrableChainId:BigInt externalDisputeId:BigInt templateId:BigInt + rulingTimestamp:BigInt + rulingTransactionHash:String } type PeriodIndexCounter @entity { @@ -303,6 +306,8 @@ type ClassicJustification @entity { choice: BigInt! votes: [ClassicVote!]! @derivedFrom(field: "justification") reference: String! + transactionHash: String! + timestamp: BigInt! } type ClassicEvidenceGroup implements EvidenceGroup @entity { diff --git a/subgraph/core/src/DisputeKitClassic.ts b/subgraph/core/src/DisputeKitClassic.ts index 4c8c37729..a8b9b3361 100644 --- a/subgraph/core/src/DisputeKitClassic.ts +++ b/subgraph/core/src/DisputeKitClassic.ts @@ -66,6 +66,8 @@ export function handleVoteCast(event: VoteCast): void { justification.localRound = currentLocalRoundID; justification.choice = choice; justification.reference = event.params._justification; + justification.transactionHash = event.transaction.hash.toHexString(); + justification.timestamp = event.block.timestamp; justification.save(); const currentRulingInfo = updateCountsAndGetCurrentRuling( currentLocalRoundID, diff --git a/subgraph/core/src/KlerosCore.ts b/subgraph/core/src/KlerosCore.ts index 8f968f83d..e6c06a354 100644 --- a/subgraph/core/src/KlerosCore.ts +++ b/subgraph/core/src/KlerosCore.ts @@ -184,6 +184,8 @@ export function handleRuling(event: Ruling): void { const dispute = Dispute.load(disputeID.toString()); if (!dispute) return; dispute.ruled = true; + dispute.rulingTransactionHash = event.transaction.hash.toHexString(); + dispute.rulingTimestamp = event.block.timestamp; dispute.save(); const court = Court.load(dispute.court); if (!court) return; diff --git a/subgraph/core/src/entities/Dispute.ts b/subgraph/core/src/entities/Dispute.ts index 49bea55ad..3e03eb1f8 100644 --- a/subgraph/core/src/entities/Dispute.ts +++ b/subgraph/core/src/entities/Dispute.ts @@ -21,6 +21,7 @@ export function createDisputeFromEvent(event: DisputeCreation): void { dispute.lastPeriodChange = event.block.timestamp; dispute.lastPeriodChangeBlockNumber = event.block.number; dispute.periodNotificationIndex = getAndIncrementPeriodCounter(dispute.period); + dispute.transactionHash = event.transaction.hash.toHexString(); const court = Court.load(courtID); if (!court) return; dispute.periodDeadline = event.block.timestamp.plus(court.timesPerPeriod[0]); diff --git a/subgraph/package.json b/subgraph/package.json index 21c2d258a..c6f1294ad 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-subgraph", - "version": "0.9.1", + "version": "0.10.1", "license": "MIT", "scripts": { "update:core:arbitrum-sepolia-devnet": "./scripts/update.sh arbitrumSepoliaDevnet arbitrum-sepolia core/subgraph.yaml", diff --git a/web/src/components/EvidenceCard.tsx b/web/src/components/EvidenceCard.tsx index 106daaa11..7bb466804 100644 --- a/web/src/components/EvidenceCard.tsx +++ b/web/src/components/EvidenceCard.tsx @@ -14,6 +14,7 @@ import { getIpfsUrl } from "utils/getIpfsUrl"; import { shortenAddress } from "utils/shortenAddress"; import { type Evidence } from "src/graphql/graphql"; +import { getTxnExplorerLink } from "src/utils"; import { hoverShortTransitionTiming } from "styles/commonStyles"; import { landscapeStyle } from "styles/landscapeStyle"; @@ -225,7 +226,7 @@ const EvidenceCard: React.FC = ({ }, [sender]); const transactionExplorerLink = useMemo(() => { - return `${getChain(DEFAULT_CHAIN)?.blockExplorers?.default.url}/tx/${transactionHash}`; + return getTxnExplorerLink(transactionHash ?? ""); }, [transactionHash]); return ( diff --git a/web/src/components/TxnHash.tsx b/web/src/components/TxnHash.tsx index bb25ce7c1..41e35f4d9 100644 --- a/web/src/components/TxnHash.tsx +++ b/web/src/components/TxnHash.tsx @@ -3,7 +3,7 @@ import styled from "styled-components"; import NewTabIcon from "svgs/icons/new-tab.svg"; -import { DEFAULT_CHAIN, getChain } from "consts/chains"; +import { getTxnExplorerLink } from "src/utils"; import { ExternalLink } from "./ExternalLink"; @@ -23,7 +23,7 @@ interface ITxnHash { } const TxnHash: React.FC = ({ hash, variant }) => { const transactionExplorerLink = useMemo(() => { - return `${getChain(DEFAULT_CHAIN)?.blockExplorers?.default.url}/tx/${hash}`; + return getTxnExplorerLink(hash); }, [hash]); return ( diff --git a/web/src/components/Verdict/DisputeTimeline.tsx b/web/src/components/Verdict/DisputeTimeline.tsx index a120bf9fa..0412bbb0d 100644 --- a/web/src/components/Verdict/DisputeTimeline.tsx +++ b/web/src/components/Verdict/DisputeTimeline.tsx @@ -1,14 +1,14 @@ import React, { useMemo } from "react"; import styled, { useTheme } from "styled-components"; -import { responsiveSize } from "styles/responsiveSize"; - +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 { Periods } from "consts/periods"; import { usePopulatedDisputeData } from "hooks/queries/usePopulatedDisputeData"; @@ -19,9 +19,14 @@ import { DisputeDetailsQuery, useDisputeDetailsQuery } from "queries/useDisputeD 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"; + const Container = styled.div` display: flex; position: relative; @@ -50,6 +55,18 @@ const StyledCalendarIcon = styled(CalendarIcon)` height: 14px; `; +const StyledNewTabIcon = styled(NewTabIcon)` + margin-bottom: 2px; + path { + fill: ${({ theme }) => theme.primaryBlue}; + } + :hover { + path { + fill: ${({ theme }) => theme.secondaryBlue}; + } + } +`; + const formatDate = (date: string) => { const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" }; const startingDate = new Date(parseInt(date) * 1000); @@ -67,6 +84,9 @@ 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(() => { + return getTxnExplorerLink(votingHistory?.dispute?.transactionHash ?? ""); + }, [votingHistory]); return useMemo(() => { const dispute = disputeDetails?.dispute; @@ -119,7 +139,11 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string [ { title: "Dispute created", - party: "", + party: ( + + + + ), subtitle: formatDate(votingHistory?.dispute?.createdAt), rightSided: true, variant: theme.secondaryPurple, @@ -128,7 +152,7 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string ); } return; - }, [disputeDetails, disputeData, localRounds, theme]); + }, [disputeDetails, disputeData, localRounds, theme, rounds, votingHistory, txnExplorerLink]); }; interface IDisputeTimeline { @@ -138,15 +162,30 @@ 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 && items && ( + {disputeDetails?.dispute?.ruled && ( - Enforcement: {items.at(-1)?.subtitle} + + Enforcement:{" "} + {disputeDetails.dispute.rulingTimestamp ? ( + + {formatDate(disputeDetails.dispute.rulingTimestamp)} + + ) : ( + + )}{" "} + / {votingHistory?.dispute?.rounds.at(-1)?.court.name} + )} diff --git a/web/src/hooks/queries/useDisputeDetailsQuery.ts b/web/src/hooks/queries/useDisputeDetailsQuery.ts index c5b359d08..71a417904 100644 --- a/web/src/hooks/queries/useDisputeDetailsQuery.ts +++ b/web/src/hooks/queries/useDisputeDetailsQuery.ts @@ -34,6 +34,8 @@ const disputeDetailsQuery = graphql(` arbitrableChainId externalDisputeId templateId + rulingTimestamp + rulingTransactionHash } } `); diff --git a/web/src/hooks/queries/useVotingHistory.ts b/web/src/hooks/queries/useVotingHistory.ts index 4991412dd..e1210ddaa 100644 --- a/web/src/hooks/queries/useVotingHistory.ts +++ b/web/src/hooks/queries/useVotingHistory.ts @@ -12,6 +12,7 @@ const votingHistoryQuery = graphql(` dispute(id: $disputeID) { id createdAt + transactionHash ruled rounds { nbVotes @@ -29,6 +30,8 @@ const votingHistoryQuery = graphql(` ... on ClassicVote { commited justification { + transactionHash + timestamp choice reference } diff --git a/web/src/pages/Cases/CaseDetails/Voting/VotesDetails/index.tsx b/web/src/pages/Cases/CaseDetails/Voting/VotesDetails/index.tsx index b78197f8a..d78cc055f 100644 --- a/web/src/pages/Cases/CaseDetails/Voting/VotesDetails/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Voting/VotesDetails/index.tsx @@ -4,13 +4,15 @@ import styled, { css } from "styled-components"; import { Card, CustomAccordion } from "@kleros/ui-components-library"; import { Answer } from "context/NewDisputeContext"; +import { formatDate } from "utils/date"; import { DrawnJuror } from "utils/getDrawnJurorsWithCount"; import { getVoteChoice } from "utils/getVoteChoice"; -import { isUndefined } from "utils/index"; +import { getTxnExplorerLink, isUndefined } from "utils/index"; import { hoverShortTransitionTiming } from "styles/commonStyles"; import { landscapeStyle } from "styles/landscapeStyle"; +import { ExternalLink } from "components/ExternalLink"; import InfoCard from "components/InfoCard"; import AccordionTitle from "./AccordionTitle"; @@ -73,21 +75,20 @@ const AccordionContentContainer = styled.div` gap: 12px; `; -const LabelWrapper = styled.div` - display: flex; - gap: 4px; -`; - -const JustificationText = styled.label` +const VotedText = styled.label` color: ${({ theme }) => theme.secondaryText}; font-size: 16px; - line-height: 1.2; - flex: 1; + ::before { + content: "Voted: "; + color: ${({ theme }) => theme.primaryText}; + } `; -const StyledLabel = styled.label` - color: ${({ theme }) => theme.primaryText}; - font-size: 16px; +const JustificationText = styled(VotedText)` + line-height: 1.25; + ::before { + content: "Justification: "; + } `; const SecondaryTextLabel = styled.label` @@ -96,28 +97,38 @@ const SecondaryTextLabel = styled.label` flex: 1; `; +const StyledInfoCard = styled(InfoCard)` + margin-top: 18.5px; +`; + const AccordionContent: React.FC<{ choice?: string; answers: Answer[]; justification: string; -}> = ({ justification, choice, answers }) => ( - - {!isUndefined(choice) && ( - - Voted:  - {getVoteChoice(parseInt(choice), answers)} - - )} - {justification ? ( - - Justification:  + timestamp?: string; + transactionHash?: string; +}> = ({ justification, choice, answers, timestamp, transactionHash }) => { + const transactionExplorerLink = useMemo(() => { + return getTxnExplorerLink(transactionHash ?? ""); + }, [transactionHash]); + + return ( + + {!isUndefined(choice) && {getVoteChoice(parseInt(choice), answers)}} + + {justification ? ( {justification} - - ) : ( - No justification provided - )} - -); + ) : ( + No justification provided + )} + {!isUndefined(timestamp) && ( + + {formatDate(Number(timestamp), true)} + + )} + + ); +}; interface IVotesAccordion { drawnJurors: DrawnJuror[]; @@ -150,6 +161,8 @@ const VotesAccordion: React.FC = ({ drawnJurors, period, answer justification={drawnJuror?.vote?.justification.reference ?? ""} choice={drawnJuror.vote?.justification?.choice} answers={answers} + transactionHash={drawnJuror.transactionHash} + timestamp={drawnJuror.timestamp} /> ), } @@ -160,12 +173,7 @@ const VotesAccordion: React.FC = ({ drawnJurors, period, answer return ( <> - {drawnJurors.length === 0 ? ( - <> -
- - - ) : null} + {drawnJurors.length === 0 ? : null} {accordionItems.length > 0 ? : null} {drawnJurors.map( diff --git a/web/src/utils/getDrawnJurorsWithCount.ts b/web/src/utils/getDrawnJurorsWithCount.ts index a2a86f980..408a5c561 100644 --- a/web/src/utils/getDrawnJurorsWithCount.ts +++ b/web/src/utils/getDrawnJurorsWithCount.ts @@ -1,19 +1,26 @@ import { VotingHistoryQuery } from "src/graphql/graphql"; type IVotingHistoryRounds = NonNullable["rounds"][number]["drawnJurors"]>; -export type DrawnJuror = IVotingHistoryRounds[number] & { voteCount: number }; +export type DrawnJuror = IVotingHistoryRounds[number] & { + voteCount: number; + transactionHash?: string; + timestamp?: string; +}; export const getDrawnJurorsWithCount = (drawnJurors: IVotingHistoryRounds) => drawnJurors?.reduce((acc, current) => { const jurorId = current.juror.id; const existingJuror = acc.find((item) => item.juror.id === jurorId); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions existingJuror ? existingJuror.voteCount++ : acc.push({ juror: { id: jurorId }, voteCount: 1, vote: current.vote, + transactionHash: current.vote?.justification?.transactionHash, + timestamp: current.vote?.justification?.timestamp, }); return acc; }, []); diff --git a/web/src/utils/index.ts b/web/src/utils/index.ts index 460283d00..0bd973663 100644 --- a/web/src/utils/index.ts +++ b/web/src/utils/index.ts @@ -1,3 +1,5 @@ +import { DEFAULT_CHAIN, getChain } from "consts/chains"; + export const isUndefined = (maybeObject: any): maybeObject is undefined | null => typeof maybeObject === "undefined" || maybeObject === null; @@ -5,3 +7,6 @@ export const isUndefined = (maybeObject: any): maybeObject is undefined | null = * Checks if a string is empty or contains only whitespace. */ export const isEmpty = (str: string): boolean => str.trim() === ""; + +export const getTxnExplorerLink = (hash: string) => + `${getChain(DEFAULT_CHAIN)?.blockExplorers?.default.url}/tx/${hash}`;