Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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://docs.kleros.io/developer/arbitration-development/erc-1497-evidence-standard#evidence
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
29 changes: 27 additions & 2 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,6 +12,9 @@ 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";

Expand Down Expand Up @@ -41,6 +45,10 @@ const StyledLabel = styled.label`
font-size: 16px;
`;

const ArbitrableEvidenceHeading = styled.h2`
font-weight: 600;
font-size: 24px;
`;
const ScrollButton = styled(Button)`
align-self: flex-end;
background-color: transparent;
Expand Down Expand Up @@ -77,14 +85,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 +116,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,6 +128,19 @@ const Evidence: React.FC = () => {
<Container ref={ref}>
<EvidenceSearch {...{ search, setSearch }} />
<ScrollButton small Icon={DownArrow} text="Scroll to latest" onClick={scrollToLatest} />
{!isUndefined(arbitrableEvidences) && arbitrableEvidences.length > 0 ? (
<>
<ArbitrableEvidenceHeading>Evidence provided by arbitrable</ArbitrableEvidenceHeading>
{arbitrableEvidences.map(({ name, description, fileURI, sender, timestamp, transactionHash }, index) => (
<EvidenceCard
key={index}
evidence=""
{...{ sender, timestamp, transactionHash, name, description, fileURI }}
/>
))}
<Divider />
</>
) : null}
{evidences?.realEvidences ? (
<>
{evidences?.realEvidences.map(
Expand Down
Loading