Skip to content

Commit f988e09

Browse files
authored
Merge branch 'dev' into feat/cases-by-court-chart
2 parents 2c5d1fd + 53249a7 commit f988e09

File tree

16 files changed

+298
-101
lines changed

16 files changed

+298
-101
lines changed

contracts/src/arbitration/evidence/EvidenceModule.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ contract EvidenceModule is IEvidence, Initializable, UUPSProxiable {
6464

6565
/// @dev Submits evidence for a dispute.
6666
/// @param _externalDisputeID Unique identifier for this dispute outside Kleros. It's the submitter responsability to submit the right evidence group ID.
67-
/// @param _evidence IPFS path to evidence, example: '/ipfs/Qmarwkf7C9RuzDEJNnarT3WZ7kem5bk8DZAzx78acJjMFH/evidence.json'.
67+
/// @param _evidence Stringified evidence object, example: '{"name" : "Justification", "description" : "Description", "fileURI" : "/ipfs/QmWQV5ZFFhEJiW8Lm7ay2zLxC2XS4wx1b2W7FfdrLMyQQc"}'.
6868
function submitEvidence(uint256 _externalDisputeID, string calldata _evidence) external {
6969
emit Evidence(_externalDisputeID, msg.sender, _evidence);
7070
}

contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ contract ModeratedEvidenceModule is IArbitrableV2 {
8484
/// @param _arbitrator The arbitrator of the contract.
8585
/// @param _externalDisputeID Unique identifier for this dispute outside Kleros. It's the submitter responsability to submit the right evidence group ID.
8686
/// @param _party The address of the party submiting the evidence. Note that 0x0 refers to evidence not submitted by any party.
87-
/// @param _evidence IPFS path to evidence, example: '/ipfs/Qmarwkf7C9RuzDEJNnarT3WZ7kem5bk8DZAzx78acJjMFH/evidence.json'
87+
/// @param _evidence Stringified evidence object, example: '{"name" : "Justification", "description" : "Description", "fileURI" : "/ipfs/QmWQV5ZFFhEJiW8Lm7ay2zLxC2XS4wx1b2W7FfdrLMyQQc"}'.
8888
event ModeratedEvidence(
8989
IArbitratorV2 indexed _arbitrator,
9090
uint256 indexed _externalDisputeID,
@@ -201,7 +201,7 @@ contract ModeratedEvidenceModule is IArbitrableV2 {
201201

202202
/// @dev Submits evidence.
203203
/// @param _evidenceGroupID Unique identifier of the evidence group the evidence belongs to. It's the submitter responsability to submit the right evidence group ID.
204-
/// @param _evidence IPFS path to evidence, example: '/ipfs/Qmarwkf7C9RuzDEJNnarT3WZ7kem5bk8DZAzx78acJjMFH/evidence.json'.
204+
/// @param _evidence Stringified evidence object, example: '{"name" : "Justification", "description" : "Description", "fileURI" : "/ipfs/QmWQV5ZFFhEJiW8Lm7ay2zLxC2XS4wx1b2W7FfdrLMyQQc"}'.
205205
function submitEvidence(uint256 _evidenceGroupID, string calldata _evidence) external payable {
206206
// Optimization opportunity: map evidenceID to an incremental index that can be safely assumed to be less than a small uint.
207207
bytes32 evidenceID = keccak256(abi.encodePacked(_evidenceGroupID, _evidence));

contracts/src/arbitration/interfaces/IEvidence.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ interface IEvidence {
77
/// @dev To be raised when evidence is submitted. Should point to the resource (evidences are not to be stored on chain due to gas considerations).
88
/// @param _externalDisputeID Unique identifier for this dispute outside Kleros. It's the submitter responsability to submit the right external dispute ID.
99
/// @param _party The address of the party submiting the evidence. Note that 0x0 refers to evidence not submitted by any party.
10-
/// @param _evidence IPFS path to evidence, example: '/ipfs/Qmarwkf7C9RuzDEJNnarT3WZ7kem5bk8DZAzx78acJjMFH/evidence.json'
10+
/// @param _evidence Stringified evidence object, example: '{"name" : "Justification", "description" : "Description", "fileURI" : "/ipfs/QmWQV5ZFFhEJiW8Lm7ay2zLxC2XS4wx1b2W7FfdrLMyQQc"}'.
1111
event Evidence(uint256 indexed _externalDisputeID, address indexed _party, string _evidence);
1212
}

subgraph/core-neo/subgraph.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
specVersion: 0.0.4
22
schema:
33
file: ./schema.graphql
4+
features:
5+
- fullTextSearch
6+
47
dataSources:
58
- kind: ethereum
69
name: KlerosCore

subgraph/core-university/subgraph.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
specVersion: 0.0.4
22
schema:
33
file: ./schema.graphql
4+
features:
5+
- fullTextSearch
6+
47
dataSources:
58
- kind: ethereum
69
name: KlerosCore

subgraph/core/schema.graphql

+21-1
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,14 @@ interface Evidence {
5252
id: ID!
5353
evidence: String!
5454
evidenceGroup: EvidenceGroup!
55+
evidenceIndex: String!
5556
sender: User!
57+
senderAddress: String!
5658
timestamp: BigInt!
59+
name: String
60+
description: String
61+
fileURI: String
62+
fileTypeExtension: String
5763
}
5864

5965
############
@@ -292,12 +298,18 @@ type ClassicEvidenceGroup implements EvidenceGroup @entity {
292298
nextEvidenceIndex: BigInt!
293299
}
294300

295-
type ClassicEvidence implements Evidence @entity {
301+
type ClassicEvidence implements Evidence @entity(immutable: true) {
296302
id: ID! # classicEvidenceGroup.id-nextEvidenceIndex
297303
evidence: String!
298304
evidenceGroup: EvidenceGroup!
305+
evidenceIndex: String!
299306
sender: User!
307+
senderAddress: String!
300308
timestamp: BigInt!
309+
name: String
310+
description: String
311+
fileURI: String
312+
fileTypeExtension: String
301313
}
302314

303315
type ClassicContribution implements Contribution @entity {
@@ -311,3 +323,11 @@ type ClassicContribution implements Contribution @entity {
311323
choice: BigInt!
312324
rewardWithdrawn: Boolean!
313325
}
326+
327+
type _Schema_
328+
@fulltext(
329+
name: "evidenceSearch"
330+
language: en
331+
algorithm: rank
332+
include: [{ entity: "ClassicEvidence", fields: [{ name: "name" }, { name: "description" },{ name: "senderAddress"},{ name: "evidenceIndex"}] }]
333+
)

subgraph/core/src/EvidenceModule.ts

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,63 @@
1+
import { json, JSONValueKind, log } from "@graphprotocol/graph-ts";
12
import { Evidence as EvidenceEvent } from "../generated/EvidenceModule/EvidenceModule";
23
import { ClassicEvidence } from "../generated/schema";
34
import { ensureClassicEvidenceGroup } from "./entities/ClassicEvidenceGroup";
45
import { ensureUser } from "./entities/User";
56
import { ONE } from "./utils";
7+
import { JSONValueToMaybeString } from "../../utils";
68

79
export function handleEvidenceEvent(event: EvidenceEvent): void {
810
const evidenceGroupID = event.params._externalDisputeID.toString();
911
const evidenceGroup = ensureClassicEvidenceGroup(evidenceGroupID);
1012
const evidenceIndex = evidenceGroup.nextEvidenceIndex;
1113
evidenceGroup.nextEvidenceIndex = evidenceGroup.nextEvidenceIndex.plus(ONE);
1214
evidenceGroup.save();
13-
const evidence = new ClassicEvidence(`${evidenceGroupID}-${evidenceIndex.toString()}`);
15+
const evidenceId = `${evidenceGroupID}-${evidenceIndex.toString()}`;
16+
const evidence = new ClassicEvidence(evidenceId);
17+
evidence.evidenceIndex = evidenceIndex.plus(ONE).toString();
1418
const userId = event.params._party.toHexString();
1519
evidence.timestamp = event.block.timestamp;
1620
evidence.evidence = event.params._evidence;
1721
evidence.evidenceGroup = evidenceGroupID.toString();
1822
evidence.sender = userId;
23+
evidence.senderAddress = userId;
1924
ensureUser(userId);
25+
26+
let jsonObjValueAndSuccess = json.try_fromString(event.params._evidence);
27+
if (!jsonObjValueAndSuccess.isOk || jsonObjValueAndSuccess.isError) {
28+
log.error(`Error getting json object for evidenceId {}`, [evidenceId]);
29+
evidence.save();
30+
return;
31+
}
32+
33+
if (jsonObjValueAndSuccess.value.isNull() || jsonObjValueAndSuccess.value.kind !== JSONValueKind.OBJECT) {
34+
log.error(`Encountered invalid parsed value for evidenceId {}`, [evidenceId]);
35+
evidence.save();
36+
return;
37+
}
38+
39+
let jsonObj = jsonObjValueAndSuccess.value.toObject();
40+
if (!jsonObj) {
41+
log.error(`Error converting json object for evidenceId {}`, [evidenceId]);
42+
evidence.save();
43+
return;
44+
}
45+
46+
let name = jsonObj.get("name");
47+
let description = jsonObj.get("description");
48+
let fileURI = jsonObj.get("fileURI");
49+
let fileTypeExtension = jsonObj.get("fileTypeExtension");
50+
51+
evidence.name = JSONValueToMaybeString(name);
52+
evidence.description = JSONValueToMaybeString(description);
53+
54+
if (fileURI) {
55+
evidence.fileURI = JSONValueToMaybeString(fileURI);
56+
}
57+
58+
if (fileTypeExtension) {
59+
evidence.fileTypeExtension = JSONValueToMaybeString(fileTypeExtension);
60+
}
61+
2062
evidence.save();
2163
}

subgraph/core/subgraph.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
specVersion: 0.0.4
22
schema:
33
file: ./schema.graphql
4+
features:
5+
- fullTextSearch
6+
47
dataSources:
58
- kind: ethereum
69
name: KlerosCore

subgraph/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kleros/kleros-v2-subgraph",
3-
"version": "0.5.1",
3+
"version": "0.7.0",
44
"license": "MIT",
55
"scripts": {
66
"update:core:arbitrum-sepolia-devnet": "./scripts/update.sh arbitrumSepoliaDevnet arbitrum-sepolia core/subgraph.yaml",

subgraph/utils/index.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { JSONValue, JSONValueKind } from "@graphprotocol/graph-ts";
2+
3+
export function JSONValueToMaybeString(value: JSONValue | null, _default: string = "-"): string {
4+
// Subgraph considers an empty string to be null and
5+
// the handler crashes when attempting to save the entity.
6+
// This is a security vulnerability because an adversary
7+
// could manually craft an item with missing columns
8+
// and the item would not show up in the UI, passing
9+
// the challenge period unoticed.
10+
//
11+
// We fix this by setting the field manually to a hifen.
12+
if (value == null || value.isNull()) {
13+
return "-";
14+
}
15+
16+
switch (value.kind) {
17+
case JSONValueKind.BOOL:
18+
return value.toBool() == true ? "true" : "false";
19+
case JSONValueKind.STRING:
20+
return value.toString();
21+
case JSONValueKind.NUMBER:
22+
return value.toBigInt().toHexString();
23+
default:
24+
return _default;
25+
}
26+
}
+10
Loading

web/src/components/EvidenceCard.tsx

+9-26
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import { Card } from "@kleros/ui-components-library";
99

1010
import AttachmentIcon from "svgs/icons/attachment.svg";
1111

12-
import { useIPFSQuery } from "hooks/useIPFSQuery";
1312
import { formatDate } from "utils/date";
1413
import { getIpfsUrl } from "utils/getIpfsUrl";
1514
import { shortenAddress } from "utils/shortenAddress";
1615

16+
import { type Evidence } from "src/graphql/graphql";
17+
1718
import { landscapeStyle } from "styles/landscapeStyle";
1819
import { responsiveSize } from "styles/responsiveSize";
1920

@@ -61,20 +62,6 @@ const BottomShade = styled.div`
6162
}
6263
`;
6364

64-
const StyledA = styled.a`
65-
display: flex;
66-
margin-left: auto;
67-
gap: ${responsiveSize(5, 6)};
68-
${landscapeStyle(
69-
() => css`
70-
> svg {
71-
width: 16px;
72-
fill: ${({ theme }) => theme.primaryBlue};
73-
}
74-
`
75-
)}
76-
`;
77-
7865
const AccountContainer = styled.div`
7966
display: flex;
8067
flex-direction: row;
@@ -136,24 +123,20 @@ const AttachedFileText: React.FC = () => (
136123
</>
137124
);
138125

139-
interface IEvidenceCard {
140-
evidence: string;
126+
interface IEvidenceCard extends Pick<Evidence, "evidence" | "timestamp" | "name" | "description" | "fileURI"> {
141127
sender: string;
142128
index: number;
143-
timestamp: string;
144129
}
145130

146-
const EvidenceCard: React.FC<IEvidenceCard> = ({ evidence, sender, index, timestamp }) => {
147-
const { data } = useIPFSQuery(evidence);
148-
131+
const EvidenceCard: React.FC<IEvidenceCard> = ({ evidence, sender, index, timestamp, name, description, fileURI }) => {
149132
return (
150133
<StyledCard>
151134
<TextContainer>
152135
<Index>#{index}:</Index>
153-
{data ? (
136+
{name && description ? (
154137
<>
155-
<h3>{data.name}</h3>
156-
<StyledReactMarkdown>{data.description}</StyledReactMarkdown>
138+
<h3>{name}</h3>
139+
<StyledReactMarkdown>{description}</StyledReactMarkdown>
157140
</>
158141
) : (
159142
<p>{evidence}</p>
@@ -165,8 +148,8 @@ const EvidenceCard: React.FC<IEvidenceCard> = ({ evidence, sender, index, timest
165148
<p>{shortenAddress(sender)}</p>
166149
</AccountContainer>
167150
<Timestamp>{formatDate(Number(timestamp), true)}</Timestamp>
168-
{data && typeof data.fileURI !== "undefined" && (
169-
<StyledLink to={`attachment/?url=${getIpfsUrl(data.fileURI)}`}>
151+
{fileURI && (
152+
<StyledLink to={`attachment/?url=${getIpfsUrl(fileURI)}`}>
170153
<AttachmentIcon />
171154
<AttachedFileText />
172155
</StyledLink>

web/src/hooks/queries/useEvidences.ts

+41-16
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,60 @@ import { REFETCH_INTERVAL } from "consts/index";
44
import { useGraphqlBatcher } from "context/GraphqlBatcher";
55

66
import { graphql } from "src/graphql";
7-
import { EvidencesQuery } from "src/graphql/graphql";
7+
import { EvidenceDetailsFragment, EvidencesQuery } from "src/graphql/graphql";
88
export type { EvidencesQuery };
99

10+
export const evidenceFragment = graphql(`
11+
fragment EvidenceDetails on ClassicEvidence {
12+
id
13+
evidence
14+
sender {
15+
id
16+
}
17+
timestamp
18+
name
19+
description
20+
fileURI
21+
fileTypeExtension
22+
evidenceIndex
23+
}
24+
`);
25+
1026
const evidencesQuery = graphql(`
1127
query Evidences($evidenceGroupID: String) {
12-
evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: desc) {
13-
id
14-
evidence
15-
sender {
16-
id
17-
}
18-
timestamp
28+
evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: asc) {
29+
...EvidenceDetails
1930
}
2031
}
2132
`);
2233

23-
export const useEvidences = (evidenceGroup?: string) => {
34+
const evidenceSearchQuery = graphql(`
35+
query EvidenceSearch($keywords: String!, $evidenceGroupID: String) {
36+
evidenceSearch(text: $keywords, where: { evidenceGroup: $evidenceGroupID }) {
37+
...EvidenceDetails
38+
}
39+
}
40+
`);
41+
42+
export const useEvidences = (evidenceGroup?: string, keywords?: string) => {
2443
const isEnabled = evidenceGroup !== undefined;
2544
const { graphqlBatcher } = useGraphqlBatcher();
2645

27-
return useQuery({
28-
queryKey: [`evidencesQuery${evidenceGroup}`],
46+
const document = keywords ? evidenceSearchQuery : evidencesQuery;
47+
return useQuery<{ evidences: EvidenceDetailsFragment[] }>({
48+
queryKey: [
49+
keywords ? `evidenceSearchQuery${evidenceGroup}-${keywords}` : `evidencesQuery${evidenceGroup}`,
50+
],
2951
enabled: isEnabled,
3052
refetchInterval: REFETCH_INTERVAL,
31-
queryFn: async () =>
32-
await graphqlBatcher.fetch({
53+
queryFn: async () => {
54+
const result = await graphqlBatcher.fetch({
3355
id: crypto.randomUUID(),
34-
document: evidencesQuery,
35-
variables: { evidenceGroupID: evidenceGroup?.toString() },
36-
}),
56+
document: document,
57+
variables: { evidenceGroupID: evidenceGroup?.toString(), keywords: keywords },
58+
});
59+
60+
return keywords ? { evidences: [...result.evidenceSearch] } : result;
61+
},
3762
});
3863
};

0 commit comments

Comments
 (0)