Skip to content

Commit 35f2581

Browse files
feat(web): dispute-txn-hashes-and-timestamps
1 parent 49be36e commit 35f2581

File tree

9 files changed

+121
-31
lines changed

9 files changed

+121
-31
lines changed

web/src/components/EvidenceCard.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import React, { useMemo } from "react";
22
import styled, { css } from "styled-components";
33

4-
import { landscapeStyle } from "styles/landscapeStyle";
5-
import { responsiveSize } from "styles/responsiveSize";
6-
import { hoverShortTransitionTiming } from "styles/commonStyles";
7-
84
import Identicon from "react-identicons";
95
import ReactMarkdown from "react-markdown";
106

@@ -18,6 +14,11 @@ import { getIpfsUrl } from "utils/getIpfsUrl";
1814
import { shortenAddress } from "utils/shortenAddress";
1915

2016
import { type Evidence } from "src/graphql/graphql";
17+
import { getTxnExplorerLink } from "src/utils";
18+
19+
import { hoverShortTransitionTiming } from "styles/commonStyles";
20+
import { landscapeStyle } from "styles/landscapeStyle";
21+
import { responsiveSize } from "styles/responsiveSize";
2122

2223
import { ExternalLink } from "./ExternalLink";
2324
import { InternalLink } from "./InternalLink";
@@ -224,7 +225,7 @@ const EvidenceCard: React.FC<IEvidenceCard> = ({
224225
}, [sender]);
225226

226227
const transactionExplorerLink = useMemo(() => {
227-
return `${getChain(DEFAULT_CHAIN)?.blockExplorers?.default.url}/tx/${transactionHash}`;
228+
return getTxnExplorerLink(transactionHash ?? "");
228229
}, [transactionHash]);
229230

230231
return (

web/src/components/TxnHash.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import styled from "styled-components";
33

44
import NewTabIcon from "svgs/icons/new-tab.svg";
55

6-
import { DEFAULT_CHAIN, getChain } from "consts/chains";
6+
import { getTxnExplorerLink } from "src/utils";
77

88
import { ExternalLink } from "./ExternalLink";
99

@@ -23,7 +23,7 @@ interface ITxnHash {
2323
}
2424
const TxnHash: React.FC<ITxnHash> = ({ hash, variant }) => {
2525
const transactionExplorerLink = useMemo(() => {
26-
return `${getChain(DEFAULT_CHAIN)?.blockExplorers?.default.url}/tx/${hash}`;
26+
return getTxnExplorerLink(hash);
2727
}, [hash]);
2828

2929
return (

web/src/components/Verdict/DisputeTimeline.tsx

+50-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import React, { useMemo } from "react";
22
import styled, { useTheme } from "styled-components";
33

4-
import { responsiveSize } from "styles/responsiveSize";
5-
4+
import Skeleton from "react-loading-skeleton";
65
import { useParams } from "react-router-dom";
76

87
import { _TimelineItem1, CustomTimeline } from "@kleros/ui-components-library";
@@ -14,14 +13,20 @@ import { Periods } from "consts/periods";
1413
import { usePopulatedDisputeData } from "hooks/queries/usePopulatedDisputeData";
1514
import { getLocalRounds } from "utils/getLocalRounds";
1615
import { getVoteChoice } from "utils/getVoteChoice";
16+
import { shortenTxnHash } from "utils/shortenAddress";
1717

1818
import { DisputeDetailsQuery, useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
1919
import { useVotingHistory } from "queries/useVotingHistory";
2020

2121
import { ClassicRound } from "src/graphql/graphql";
22+
import { getTxnExplorerLink } from "src/utils";
23+
24+
import { responsiveSize } from "styles/responsiveSize";
2225

2326
import { StyledClosedCircle } from "components/StyledIcons/ClosedCircleIcon";
2427

28+
import { ExternalLink } from "../ExternalLink";
29+
2530
const Container = styled.div`
2631
display: flex;
2732
position: relative;
@@ -50,6 +55,16 @@ const StyledCalendarIcon = styled(CalendarIcon)`
5055
height: 14px;
5156
`;
5257

58+
const LinkContainer = styled.div`
59+
display: flex;
60+
gap: 4px;
61+
align-items: center;
62+
span {
63+
font-size: 14px;
64+
color: ${({ theme }) => theme.primaryText};
65+
}
66+
`;
67+
5368
const formatDate = (date: string) => {
5469
const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
5570
const startingDate = new Date(parseInt(date) * 1000);
@@ -67,6 +82,9 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string
6782
const localRounds: ClassicRound[] = getLocalRounds(votingHistory?.dispute?.disputeKitDispute) as ClassicRound[];
6883
const rounds = votingHistory?.dispute?.rounds;
6984
const theme = useTheme();
85+
const txnExplorerLink = useMemo(() => {
86+
return getTxnExplorerLink(votingHistory?.dispute?.transactionHash ?? "");
87+
}, [votingHistory]);
7088

7189
return useMemo<TimelineItems | undefined>(() => {
7290
const dispute = disputeDetails?.dispute;
@@ -119,7 +137,18 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string
119137
[
120138
{
121139
title: "Dispute created",
122-
party: "",
140+
party: (
141+
<LinkContainer>
142+
<span>at</span>
143+
<ExternalLink to={txnExplorerLink} rel="noopener noreferrer" target="_blank">
144+
{votingHistory?.dispute?.transactionHash ? (
145+
shortenTxnHash(votingHistory?.dispute?.transactionHash)
146+
) : (
147+
<Skeleton height={16} width={56} />
148+
)}
149+
</ExternalLink>
150+
</LinkContainer>
151+
),
123152
subtitle: formatDate(votingHistory?.dispute?.createdAt),
124153
rightSided: true,
125154
variant: theme.secondaryPurple,
@@ -128,7 +157,7 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string
128157
);
129158
}
130159
return;
131-
}, [disputeDetails, disputeData, localRounds, theme]);
160+
}, [disputeDetails, disputeData, localRounds, theme, rounds, votingHistory, txnExplorerLink]);
132161
};
133162

134163
interface IDisputeTimeline {
@@ -138,15 +167,30 @@ interface IDisputeTimeline {
138167
const DisputeTimeline: React.FC<IDisputeTimeline> = ({ arbitrable }) => {
139168
const { id } = useParams();
140169
const { data: disputeDetails } = useDisputeDetailsQuery(id);
170+
const { data: votingHistory } = useVotingHistory(id);
141171
const items = useItems(disputeDetails, arbitrable);
142172

173+
const transactionExplorerLink = useMemo(() => {
174+
return getTxnExplorerLink(disputeDetails?.dispute?.rulingTransactionHash ?? "");
175+
}, [disputeDetails]);
176+
143177
return (
144178
<Container>
145179
{items && <StyledTimeline {...{ items }} />}
146-
{disputeDetails?.dispute?.ruled && items && (
180+
{disputeDetails?.dispute?.ruled && (
147181
<EnforcementContainer>
148182
<StyledCalendarIcon />
149-
<small>Enforcement: {items.at(-1)?.subtitle}</small>
183+
<small>
184+
Enforcement:{" "}
185+
{disputeDetails.dispute.rulingTimestamp ? (
186+
<ExternalLink to={transactionExplorerLink} rel="noopener noreferrer" target="_blank">
187+
{formatDate(disputeDetails.dispute.rulingTimestamp)}
188+
</ExternalLink>
189+
) : (
190+
<Skeleton height={16} width={56} />
191+
)}{" "}
192+
/ {votingHistory?.dispute?.rounds.at(-1)?.court.name}
193+
</small>
150194
</EnforcementContainer>
151195
)}
152196
</Container>

web/src/hooks/queries/useDisputeDetailsQuery.ts

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const disputeDetailsQuery = graphql(`
3434
arbitrableChainId
3535
externalDisputeId
3636
templateId
37+
rulingTimestamp
38+
rulingTransactionHash
3739
}
3840
}
3941
`);

web/src/hooks/queries/useVotingHistory.ts

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const votingHistoryQuery = graphql(`
1212
dispute(id: $disputeID) {
1313
id
1414
createdAt
15+
transactionHash
1516
ruled
1617
rounds {
1718
nbVotes
@@ -29,6 +30,8 @@ const votingHistoryQuery = graphql(`
2930
... on ClassicVote {
3031
commited
3132
justification {
33+
transactionHash
34+
timestamp
3235
choice
3336
reference
3437
}

web/src/pages/Cases/CaseDetails/Voting/VotesDetails/index.tsx

+37-17
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import styled, { css } from "styled-components";
44
import { Card, CustomAccordion } from "@kleros/ui-components-library";
55

66
import { Answer } from "context/NewDisputeContext";
7+
import { formatDate } from "utils/date";
78
import { DrawnJuror } from "utils/getDrawnJurorsWithCount";
89
import { getVoteChoice } from "utils/getVoteChoice";
9-
import { isUndefined } from "utils/index";
10+
import { getTxnExplorerLink, isUndefined } from "utils/index";
1011

1112
import { hoverShortTransitionTiming } from "styles/commonStyles";
1213
import { landscapeStyle } from "styles/landscapeStyle";
1314

15+
import { ExternalLink } from "components/ExternalLink";
1416
import InfoCard from "components/InfoCard";
1517

1618
import AccordionTitle from "./AccordionTitle";
@@ -76,7 +78,7 @@ const AccordionContentContainer = styled.div`
7678
const JustificationText = styled.div`
7779
color: ${({ theme }) => theme.secondaryText};
7880
font-size: 16px;
79-
line-height: 1.2;
81+
line-height: 1.25;
8082
&:before {
8183
content: "Justification: ";
8284
color: ${({ theme }) => theme.primaryText};
@@ -97,21 +99,37 @@ const AccordionContent: React.FC<{
9799
choice?: string;
98100
answers: Answer[];
99101
justification: string;
100-
}> = ({ justification, choice, answers }) => (
101-
<AccordionContentContainer>
102-
{!isUndefined(choice) && (
103-
<div>
104-
<StyledLabel>Voted:&nbsp;</StyledLabel>
105-
<SecondaryTextLabel>{getVoteChoice(parseInt(choice), answers)}</SecondaryTextLabel>
106-
</div>
107-
)}
108-
{justification ? (
109-
<JustificationText>{justification}</JustificationText>
110-
) : (
111-
<SecondaryTextLabel>No justification provided</SecondaryTextLabel>
112-
)}
113-
</AccordionContentContainer>
114-
);
102+
timestamp?: string;
103+
transactionHash?: string;
104+
}> = ({ justification, choice, answers, timestamp, transactionHash }) => {
105+
const transactionExplorerLink = useMemo(() => {
106+
return getTxnExplorerLink(transactionHash ?? "");
107+
}, [transactionHash]);
108+
109+
return (
110+
<AccordionContentContainer>
111+
{!isUndefined(choice) && (
112+
<div>
113+
<StyledLabel>Voted:&nbsp;</StyledLabel>
114+
<SecondaryTextLabel>{getVoteChoice(parseInt(choice), answers)}</SecondaryTextLabel>
115+
</div>
116+
)}
117+
{!isUndefined(timestamp) && (
118+
<div>
119+
<StyledLabel>Date:&nbsp;</StyledLabel>
120+
<ExternalLink to={transactionExplorerLink} rel="noopener noreferrer" target="_blank">
121+
{formatDate(Number(timestamp), true)}
122+
</ExternalLink>
123+
</div>
124+
)}
125+
{justification ? (
126+
<JustificationText>{justification}</JustificationText>
127+
) : (
128+
<SecondaryTextLabel>No justification provided</SecondaryTextLabel>
129+
)}
130+
</AccordionContentContainer>
131+
);
132+
};
115133

116134
interface IVotesAccordion {
117135
drawnJurors: DrawnJuror[];
@@ -144,6 +162,8 @@ const VotesAccordion: React.FC<IVotesAccordion> = ({ drawnJurors, period, answer
144162
justification={drawnJuror?.vote?.justification.reference ?? ""}
145163
choice={drawnJuror.vote?.justification?.choice}
146164
answers={answers}
165+
transactionHash={drawnJuror.transactionHash}
166+
timestamp={drawnJuror.timestamp}
147167
/>
148168
),
149169
}
+8-1
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
import { VotingHistoryQuery } from "src/graphql/graphql";
22

33
type IVotingHistoryRounds = NonNullable<NonNullable<VotingHistoryQuery["dispute"]>["rounds"][number]["drawnJurors"]>;
4-
export type DrawnJuror = IVotingHistoryRounds[number] & { voteCount: number };
4+
export type DrawnJuror = IVotingHistoryRounds[number] & {
5+
voteCount: number;
6+
transactionHash?: string;
7+
timestamp?: string;
8+
};
59

610
export const getDrawnJurorsWithCount = (drawnJurors: IVotingHistoryRounds) =>
711
drawnJurors?.reduce<DrawnJuror[]>((acc, current) => {
812
const jurorId = current.juror.id;
913

1014
const existingJuror = acc.find((item) => item.juror.id === jurorId);
15+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
1116
existingJuror
1217
? existingJuror.voteCount++
1318
: acc.push({
1419
juror: { id: jurorId },
1520
voteCount: 1,
1621
vote: current.vote,
22+
transactionHash: current.vote?.justification?.transactionHash,
23+
timestamp: current.vote?.justification?.timestamp,
1724
});
1825
return acc;
1926
}, []);

web/src/utils/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import { DEFAULT_CHAIN, getChain } from "consts/chains";
2+
13
export const isUndefined = (maybeObject: any): maybeObject is undefined | null =>
24
typeof maybeObject === "undefined" || maybeObject === null;
35

46
/**
57
* Checks if a string is empty or contains only whitespace.
68
*/
79
export const isEmpty = (str: string): boolean => str.trim() === "";
10+
11+
export const getTxnExplorerLink = (hash: string) =>
12+
`${getChain(DEFAULT_CHAIN)?.blockExplorers?.default.url}/tx/${hash}`;

web/src/utils/shortenAddress.ts

+8
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@ export function shortenAddress(address: string): string {
88
throw new TypeError("Invalid input, address can't be parsed");
99
}
1010
}
11+
12+
export function shortenTxnHash(hash: string): string {
13+
try {
14+
return hash.substring(0, 6) + "..." + hash.substring(hash.length - 4);
15+
} catch {
16+
throw new TypeError("Invalid input, address can't be parsed");
17+
}
18+
}

0 commit comments

Comments
 (0)