Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export const ethAddressOrEnsNameSchema = z.union([ethAddressSchema, ensNameSchem
errorMap: () => ({ message: "Provided address or ENS name is invalid." }),
});

export const TxHashSchema = z.string().refine((value) => isHexId(value) && value.length === 66, {
message: "Provided transaction hash is invalid.",
});

export enum QuestionType {
Bool = "bool",
Datetime = "datetime",
Expand Down Expand Up @@ -53,6 +57,18 @@ export const AttachmentSchema = z.object({

export const AliasSchema = z.record(ethAddressOrEnsNameSchema);

// https://github.com/kleros/kleros-v2/blob/dev/contracts/specifications/evidence-format.md
export const EvidenceSchema = z.object({
name: z.string(),
description: z.string(),
fileURI: z.string().optional(),
fileTypeExtension: z.string().optional(),
// court UI specific
transactionHash: TxHashSchema.optional(),
sender: ethAddressOrEnsNameSchema.optional(),
timestamp: z.number().optional(),
});

const MetadataSchema = z.record(z.unknown());

const DisputeDetailsSchema = z.object({
Expand All @@ -72,6 +88,7 @@ const DisputeDetailsSchema = z.object({
lang: z.string().optional(),
specification: z.string().optional(),
aliases: AliasSchema.optional(),
extraEvidences: z.array(EvidenceSchema).default([]),
version: z.string(),
});

Expand Down
33 changes: 33 additions & 0 deletions kleros-sdk/test/disputeDetailsSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ethAddressSchema,
ensNameSchema,
ethAddressOrEnsNameSchema,
TxHashSchema,
} from "../src/dataMappings/utils/disputeDetailsSchema";

describe("Dispute Details Schema", () => {
Expand All @@ -29,6 +30,23 @@ describe("Dispute Details Schema", () => {

const invalidEnsNamesNoAddress = ["", "vitalik", "vitalik.ether", "vitalik.sol", "eth.vitalik"];

const validTxnHashes = [
"0x274fbd8f08f1d2f76a49a7fa062c0590b00d400b1429a9f7a6c21e22b65c82d8",
"0x274FBD8F08F1D2F76A49A7FA062C0590B00D400B1429A9F7A6C21E22B65C82D8",
"0xa9d24e6c40c26c64b5fe96a3ef050f9a916ce4a362d123ab85a607055e9f99ec",
];

const invalidTxnHashes = [
"0x1234",
"0x1234567890abcdef",
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcde",
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"0X1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeg",
"0xZZZ4567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef00",
];

describe("ethAddressSchema", () => {
it("Should accept a valid address", async () => {
validAddresses.forEach((address) => {
Expand All @@ -46,6 +64,21 @@ describe("Dispute Details Schema", () => {
});
});

describe("txHashSchema", () => {
it("Should accept a valid transaction hash", async () => {
validTxnHashes.forEach((hash) => {
expect(() => TxHashSchema.parse(hash)).not.toThrow();
});
});

it("Should refuse an invalid transaction hash", async () => {
const invalidTransaction = "Provided transaction hash is invalid.";
invalidTxnHashes.forEach((hash) => {
expect(() => TxHashSchema.parse(hash)).toThrowError(invalidTransaction);
});
});
});

describe("ensNameSchema", () => {
it("Should accept a valid ENS name", async () => {
validEnsNames.forEach((ensName) => {
Expand Down
26 changes: 15 additions & 11 deletions web/src/components/EvidenceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { formatDate } from "utils/date";
import { getIpfsUrl } from "utils/getIpfsUrl";

import { type Evidence } from "src/graphql/graphql";
import { getTxnExplorerLink } from "src/utils";
import { getTxnExplorerLink, isUndefined } from "src/utils";

import { hoverShortTransitionTiming } from "styles/commonStyles";
import { landscapeStyle } from "styles/landscapeStyle";
Expand Down Expand Up @@ -186,9 +186,9 @@ const AttachedFileText: React.FC = () => (
);

interface IEvidenceCard extends Pick<Evidence, "evidence" | "timestamp" | "name" | "description" | "fileURI"> {
sender: string;
index: number;
transactionHash: string;
sender?: string;
index?: number;
transactionHash?: string;
}

const EvidenceCard: React.FC<IEvidenceCard> = ({
Expand All @@ -212,7 +212,7 @@ const EvidenceCard: React.FC<IEvidenceCard> = ({
<StyledCard>
<TopContent dir="auto">
<IndexAndName>
<Index>#{index}. </Index>
{isUndefined(index) ? null : <Index>#{index}. </Index>}
<h3>{name}</h3>
</IndexAndName>
{name && description ? (
Expand All @@ -227,12 +227,16 @@ const EvidenceCard: React.FC<IEvidenceCard> = ({
</TopContent>
<BottomShade>
<BottomLeftContent>
<StyledJurorInternalLink to={profileLink}>
<JurorTitle address={sender} />
</StyledJurorInternalLink>
<StyledExternalLink to={transactionExplorerLink} rel="noopener noreferrer" target="_blank">
<label>{formatDate(Number(timestamp), true)}</label>
</StyledExternalLink>
{isUndefined(sender) ? null : (
<StyledJurorInternalLink to={profileLink}>
<JurorTitle address={sender} />
</StyledJurorInternalLink>
)}
{isUndefined(timestamp) || isUndefined(transactionExplorerLink) ? null : (
<StyledExternalLink to={transactionExplorerLink} rel="noopener noreferrer" target="_blank">
<label>{formatDate(Number(timestamp), true)}</label>
</StyledExternalLink>
)}
</BottomLeftContent>
{fileURI && fileURI !== "-" ? (
<FileLinkContainer>
Expand Down
149 changes: 112 additions & 37 deletions web/src/pages/Cases/CaseDetails/Evidence/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styled, { css } from "styled-components";

import { useParams } from "react-router-dom";
import { useDebounce } from "react-use";
import { Address } from "viem";

import { Button } from "@kleros/ui-components-library";

Expand All @@ -11,20 +12,23 @@ import DownArrow from "svgs/icons/arrow-down.svg";
import { useSpamEvidence } from "hooks/useSpamEvidence";

import { useEvidences } from "queries/useEvidences";
import { usePopulatedDisputeData } from "queries/usePopulatedDisputeData";

import { isUndefined } from "src/utils";

import { landscapeStyle } from "styles/landscapeStyle";

import { Divider } from "components/Divider";
import EvidenceCard from "components/EvidenceCard";
import { SkeletonEvidenceCard } from "components/StyledSkeleton";
import WithHelpTooltip from "components/WithHelpTooltip";

import EvidenceSearch from "./EvidenceSearch";

const Container = styled.div`
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
padding: 20px 16px 16px;

Expand All @@ -34,19 +38,52 @@ const Container = styled.div`
`
)}
`;
const EvidenceContainer = styled.div`
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
gap: 24px;
margin-top: 32px;
`;

const JustificationContainer = styled(EvidenceContainer)`
margin-top: 0px;
margin-bottom: 32px;
`;

const EvidenceHeading = styled.h2`
font-size: 24px;
color: ${({ theme }) => theme.primaryText};
font-weight: 600;
margin-bottom: 0;
`;

const EvidenceCardContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
`;

const StyledLabel = styled.label`
display: flex;
margin-top: 16px;
font-size: 16px;
`;

const ArbitrableEvidenceHeading = styled.label`
font-size: 16px;
color: ${({ theme }) => theme.primaryText};
`;

const ScrollButton = styled(Button)`
align-self: flex-end;
background-color: transparent;
padding: 0;
flex-direction: row-reverse;
gap: 8px;
margin-top: 18px;
margin-bottom: 24px;
.button-text {
color: ${({ theme }) => theme.primaryBlue};
font-weight: 400;
Expand Down Expand Up @@ -77,14 +114,17 @@ const SpamLabel = styled.label`
cursor: pointer;
`;

const Evidence: React.FC = () => {
interface IEvidence {
arbitrable?: Address;
}
const Evidence: React.FC<IEvidence> = ({ arbitrable }) => {
const { id } = useParams();
const ref = useRef<HTMLDivElement>(null);
const [search, setSearch] = useState<string>();
const [debouncedSearch, setDebouncedSearch] = useState<string>();
const [showSpam, setShowSpam] = useState(false);
const { data: spamEvidences } = useSpamEvidence(id!);

const { data: disputeData } = usePopulatedDisputeData(id, arbitrable);
const { data } = useEvidences(id!, debouncedSearch);

useDebounce(() => setDebouncedSearch(search), 500, [search]);
Expand All @@ -105,6 +145,7 @@ const Evidence: React.FC = () => {
[spamEvidences]
);

const arbitrableEvidences = disputeData?.extraEvidences;
const evidences = useMemo(() => {
if (!data?.evidences) return;
const spamEvidences = data.evidences.filter((evidence) => isSpam(evidence.id));
Expand All @@ -116,46 +157,80 @@ const Evidence: React.FC = () => {
<Container ref={ref}>
<EvidenceSearch {...{ search, setSearch }} />
<ScrollButton small Icon={DownArrow} text="Scroll to latest" onClick={scrollToLatest} />
{evidences?.realEvidences ? (
{!isUndefined(arbitrableEvidences) && arbitrableEvidences.length > 0 ? (
<>
{evidences?.realEvidences.map(
({ evidence, sender, timestamp, transactionHash, name, description, fileURI, evidenceIndex }) => (
<EvidenceCard
key={timestamp}
index={parseInt(evidenceIndex)}
sender={sender?.id}
{...{ evidence, timestamp, transactionHash, name, description, fileURI }}
/>
)
)}
{spamEvidences && evidences?.spamEvidences.length !== 0 ? (
<>
<Divider />
{showSpam ? (
<JustificationContainer>
<WithHelpTooltip tooltipMsg="Justifications are submitted by one party before the case is created and explain their initial position or reasons for initiating the dispute.">
<ArbitrableEvidenceHeading>Justifications</ArbitrableEvidenceHeading>
</WithHelpTooltip>
<EvidenceCardContainer>
{arbitrableEvidences.map(({ name, description, fileURI, sender, timestamp, transactionHash }, index) => (
<EvidenceCard
key={index}
evidence=""
{...{ sender, timestamp, transactionHash, name, description, fileURI }}
/>
))}
</EvidenceCardContainer>
</JustificationContainer>
</>
) : null}
<Divider />

{evidences?.realEvidences ? (
<EvidenceContainer>
<EvidenceHeading>Evidence</EvidenceHeading>
{data?.evidences.length !== 0 ? (
<EvidenceCardContainer>
{evidences?.realEvidences.map(
({ evidence, sender, timestamp, transactionHash, name, description, fileURI, evidenceIndex }) => (
<EvidenceCard
key={timestamp}
index={parseInt(evidenceIndex)}
sender={sender?.id}
{...{ evidence, timestamp, transactionHash, name, description, fileURI }}
/>
)
)}
{spamEvidences && evidences?.spamEvidences.length !== 0 ? (
<>
<SpamLabel onClick={() => setShowSpam(false)}>Hide spam</SpamLabel>
{evidences?.spamEvidences.map(
({ evidence, sender, timestamp, transactionHash, name, description, fileURI, evidenceIndex }) => (
<EvidenceCard
key={timestamp}
index={parseInt(evidenceIndex)}
sender={sender?.id}
{...{ evidence, timestamp, transactionHash, name, description, fileURI }}
/>
)
<Divider />
{showSpam ? (
<>
<SpamLabel onClick={() => setShowSpam(false)}>Hide spam</SpamLabel>
{evidences?.spamEvidences.map(
({
evidence,
sender,
timestamp,
transactionHash,
name,
description,
fileURI,
evidenceIndex,
}) => (
<EvidenceCard
key={timestamp}
index={parseInt(evidenceIndex)}
sender={sender?.id}
{...{ evidence, timestamp, transactionHash, name, description, fileURI }}
/>
)
)}
</>
) : (
<SpamLabel onClick={() => setShowSpam(true)}>Show likely spam</SpamLabel>
)}
</>
) : (
<SpamLabel onClick={() => setShowSpam(true)}>Show likely spam</SpamLabel>
)}
</>
) : null}
</>
) : null}
</EvidenceCardContainer>
) : (
<StyledLabel>There is no evidence submitted yet</StyledLabel>
)}
</EvidenceContainer>
) : (
<SkeletonEvidenceCard />
)}

{data && data.evidences.length === 0 ? <StyledLabel>There is no evidence submitted yet</StyledLabel> : null}
</Container>
);
};
Expand Down
Loading