diff --git a/kleros-sdk/CHANGELOG.md b/kleros-sdk/CHANGELOG.md new file mode 100644 index 000000000..fff35cf72 --- /dev/null +++ b/kleros-sdk/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this package will be documented in this file. + +The format is based on [Common Changelog](https://common-changelog.org/). + +## [2.4.0] - 2026-01-09 + +### Added + +- Add optional field `extraEvidences` to the dispute details model to allow some arbitrable events to be rendered as evidence. ([#2210](https://github.com/kleros/kleros-v2/pull/2210)) + +[2.4.0]: https://github.com/kleros/kleros-v2/releases/tag/%40kleros%2Fkleros-sdk%402.4.0 diff --git a/kleros-sdk/package.json b/kleros-sdk/package.json index 52681af20..92a937c99 100644 --- a/kleros-sdk/package.json +++ b/kleros-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-sdk", - "version": "2.3.1", + "version": "2.4.0", "description": "SDK for Kleros version 2", "repository": "git@github.com:kleros/kleros-v2.git", "homepage": "https://github.com/kleros/kleros-v2/tree/master/kleros-sdk#readme", diff --git a/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts b/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts index 78c4697a7..cec7f0e3f 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://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({ @@ -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) => { diff --git a/web/src/components/EvidenceCard.tsx b/web/src/components/EvidenceCard.tsx index 99f6d7ffc..b61a7f3d6 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"; @@ -185,9 +185,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 = ({ @@ -211,7 +211,7 @@ const EvidenceCard: React.FC = ({ - #{index}. + {isUndefined(index) ? null : #{index}. }

{name}

{name && description ? ( @@ -226,12 +226,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..477cee2fd 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"; @@ -77,14 +81,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 +112,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 +124,17 @@ const Evidence: React.FC = () => { + {!isUndefined(arbitrableEvidences) && arbitrableEvidences.length > 0 ? ( + <> + {arbitrableEvidences.map(({ name, description, fileURI, sender, timestamp, transactionHash }, index) => ( + + ))} + + ) : null} {evidences?.realEvidences ? ( <> {evidences?.realEvidences.map(