From f862532db59510cade828ff4da920bd5358ee248 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Mon, 22 Dec 2025 19:15:34 +0530 Subject: [PATCH 1/2] feat(kleros-sdk): extra-evidences-schema --- .../utils/disputeDetailsSchema.ts | 17 ++++++++++ kleros-sdk/test/disputeDetailsSchema.test.ts | 33 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts b/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts index 78c4697a7..77cd78bc5 100644 --- a/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts +++ b/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts @@ -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", @@ -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({ @@ -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(), }); diff --git a/kleros-sdk/test/disputeDetailsSchema.test.ts b/kleros-sdk/test/disputeDetailsSchema.test.ts index d9f132e73..6f49f971e 100644 --- a/kleros-sdk/test/disputeDetailsSchema.test.ts +++ b/kleros-sdk/test/disputeDetailsSchema.test.ts @@ -3,6 +3,7 @@ import { ethAddressSchema, ensNameSchema, ethAddressOrEnsNameSchema, + TxHashSchema, } from "../src/dataMappings/utils/disputeDetailsSchema"; describe("Dispute Details Schema", () => { @@ -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) => { @@ -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) => { From 05ebe69f3af75f95bdd674be1433e26a0308ca2b Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Mon, 22 Dec 2025 19:35:38 +0530 Subject: [PATCH 2/2] feat(web): naive-arbitrable-side-evidence-display --- web/src/components/EvidenceCard.tsx | 26 ++++++++++------- .../Cases/CaseDetails/Evidence/index.tsx | 29 +++++++++++++++++-- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/web/src/components/EvidenceCard.tsx b/web/src/components/EvidenceCard.tsx index 90ea42f00..3fa4226af 100644 --- a/web/src/components/EvidenceCard.tsx +++ b/web/src/components/EvidenceCard.tsx @@ -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"; @@ -186,9 +186,9 @@ const AttachedFileText: React.FC = () => ( ); interface IEvidenceCard extends Pick { - sender: string; - index: number; - transactionHash: string; + sender?: string; + index?: number; + transactionHash?: string; } const EvidenceCard: React.FC = ({ @@ -212,7 +212,7 @@ const EvidenceCard: React.FC = ({ - #{index}. + {isUndefined(index) ? null : #{index}. }

{name}

{name && description ? ( @@ -227,12 +227,16 @@ const EvidenceCard: React.FC = ({
- - - - - - + {isUndefined(sender) ? null : ( + + + + )} + {isUndefined(timestamp) || isUndefined(transactionExplorerLink) ? null : ( + + + + )} {fileURI && fileURI !== "-" ? ( diff --git a/web/src/pages/Cases/CaseDetails/Evidence/index.tsx b/web/src/pages/Cases/CaseDetails/Evidence/index.tsx index 31d16a121..3fc21127c 100644 --- a/web/src/pages/Cases/CaseDetails/Evidence/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Evidence/index.tsx @@ -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"; @@ -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"; @@ -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; @@ -77,14 +85,17 @@ const SpamLabel = styled.label` cursor: pointer; `; -const Evidence: React.FC = () => { +interface IEvidence { + arbitrable?: Address; +} +const Evidence: React.FC = ({ arbitrable }) => { const { id } = useParams(); const ref = useRef(null); const [search, setSearch] = useState(); const [debouncedSearch, setDebouncedSearch] = useState(); 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]); @@ -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)); @@ -116,6 +128,19 @@ const Evidence: React.FC = () => { + {!isUndefined(arbitrableEvidences) && arbitrableEvidences.length > 0 ? ( + <> + Evidence provided by arbitrable + {arbitrableEvidences.map(({ name, description, fileURI, sender, timestamp, transactionHash }, index) => ( + + ))} + + + ) : null} {evidences?.realEvidences ? ( <> {evidences?.realEvidences.map(