diff --git a/contracts/package.json b/contracts/package.json index d2eb5fc23..6225f6dd1 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -102,6 +102,7 @@ "typescript": "^5.3.3" }, "dependencies": { - "@kleros/vea-contracts": "^0.4.0" + "@kleros/vea-contracts": "^0.4.0", + "viem": "^2.21.26" } } diff --git a/contracts/tsconfig.json b/contracts/tsconfig.json index c6e40bb30..932cefbdb 100644 --- a/contracts/tsconfig.json +++ b/contracts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@kleros/kleros-v2-tsconfig/base.json", + "extends": "@kleros/kleros-v2-tsconfig/base18.json", "include": [ "./src", "./scripts", diff --git a/kleros-sdk/.gitignore b/kleros-sdk/.gitignore new file mode 100644 index 000000000..3ed7dc1bc --- /dev/null +++ b/kleros-sdk/.gitignore @@ -0,0 +1,24 @@ +node_modules + +# vite +development +build +dist +lib + +# misc +.eslintcache +.DS_Store +.env +.env.test +.env.testnet +.env.devnet +.env.local +.env.development.local +.env.test.local +.env.production.local + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/kleros-sdk/README.md b/kleros-sdk/README.md index 1c929a4cc..28d807b82 100644 --- a/kleros-sdk/README.md +++ b/kleros-sdk/README.md @@ -1,4 +1,4 @@ -# @kleros/kleros-v2-sdk +# @kleros/kleros-sdk _Archon's successor_ diff --git a/kleros-sdk/package.json b/kleros-sdk/package.json index 9f2421d07..cd19c32eb 100644 --- a/kleros-sdk/package.json +++ b/kleros-sdk/package.json @@ -1,15 +1,17 @@ { "name": "@kleros/kleros-sdk", - "version": "0.1.0", + "version": "2.1.7", "description": "SDK for Kleros version 2", - "main": "index.ts", "repository": "git@github.com:kleros/kleros-v2.git", "author": "Kleros", "license": "MIT", - "alias": { - "src": "./src", - "dataMappings": "./src/dataMappings" - }, + "main": "./lib/src/index.js", + "types": "./lib/src/index.d.ts", + "module": "./lib/src/index.js", + "files": [ + "lib/**/*", + "!lib/**/test/*" + ], "packageManager": "yarn@4.0.2+sha256.825003a0f561ad09a3b1ac4a3b3ea6207af2796d54f62a9420520915721f5186", "engines": { "node": ">=16.0.0" @@ -18,24 +20,34 @@ "volta": { "node": "20.11.0" }, + "publishConfig": { + "access": "public", + "tag": "latest" + }, "scripts": { - "build": "your-build-script", + "clean": "rimraf lib", + "build": "yarn clean && tsc", "test": "vitest", "test:ui": "vitest --ui", - "test:run": "vitest run" + "test:run": "vitest run", + "release:patch": "scripts/publish.sh patch", + "release:minor": "scripts/publish.sh minor", + "release:major": "scripts/publish.sh major" }, "devDependencies": { "@types/mustache": "^4.2.5", "@vitest/ui": "^1.1.3", "mocha": "^10.2.0", + "rimraf": "^6.0.1", "ts-node": "^10.9.2", "typescript": "^5.3.3", "vitest": "^1.1.3" }, "dependencies": { - "@kleros/kleros-v2-contracts": "workspace:^", "@reality.eth/reality-eth-lib": "^3.2.30", + "@urql/core": "^5.0.8", "mustache": "^4.2.0", + "viem": "^2.21.26", "zod": "^3.22.4" } } diff --git a/kleros-sdk/scripts/publish.sh b/kleros-sdk/scripts/publish.sh new file mode 100755 index 000000000..f0b957963 --- /dev/null +++ b/kleros-sdk/scripts/publish.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +#-------------------------------------- +# Error handling +#-------------------------------------- + +set -Ee +function _catch { + # Don't propagate to outer shell + exit 0 +} +function _finally { + # TODO: rollback version bump + rm -rf $SCRIPT_DIR/../dist +} +trap _catch ERR +trap _finally EXIT + +#-------------------------------------- + +# Check if any tracked files are currently changed, ignoring untracked files +if [ -n "$(git status --porcelain -uno)" ]; then + echo "Error: There are uncommitted changes in tracked files. Please commit or stash them before publishing." + exit 1 +fi + +yarn version $1 + +version=$(cat package.json | jq -r .version) +echo "Publishing version $version" + +git add package.json +git commit -m "chore(sdk): release @kleros/kleros-sdk@$version" +git tag "@kleros/kleros-sdk@$version" -m "@kleros/kleros-sdk@$version" +git push +git push --tags + +yarn clean +yarn build +yarn npm publish diff --git a/kleros-sdk/src/dataMappings/actions/callAction.ts b/kleros-sdk/src/dataMappings/actions/callAction.ts index c949b955e..d8a2e7b0c 100644 --- a/kleros-sdk/src/dataMappings/actions/callAction.ts +++ b/kleros-sdk/src/dataMappings/actions/callAction.ts @@ -1,19 +1,23 @@ import { parseAbiItem } from "viem"; -import { AbiCallMapping } from "src/dataMappings/utils/actionTypes"; -import { createResultObject } from "src/dataMappings/utils/createResultObject"; -import { configureSDK, getPublicClient } from "src/sdk"; +import { AbiCallMapping } from "../utils/actionTypes"; +import { createResultObject } from "../utils/createResultObject"; +import { getPublicClient } from "../../sdk"; +import { SdkNotConfiguredError } from "../../errors"; -export const callAction = async (mapping: AbiCallMapping, alchemyApiKey: string) => { - configureSDK({ apiKey: alchemyApiKey }); +export const callAction = async (mapping: AbiCallMapping) => { const publicClient = getPublicClient(); - const { abi: source, address, args, seek, populate } = mapping; + if (!publicClient) { + throw new SdkNotConfiguredError(); + } + + const { abi: source, address, functionName, args, seek, populate } = mapping; const parsedAbi = typeof source === "string" ? parseAbiItem(source) : source; const data = await publicClient.readContract({ address, abi: [parsedAbi], - functionName: "TODO: FIX ME", + functionName, args, }); diff --git a/kleros-sdk/src/dataMappings/actions/eventAction.ts b/kleros-sdk/src/dataMappings/actions/eventAction.ts index 1befb9035..0f460af1d 100644 --- a/kleros-sdk/src/dataMappings/actions/eventAction.ts +++ b/kleros-sdk/src/dataMappings/actions/eventAction.ts @@ -1,13 +1,16 @@ -import { parseAbiItem } from "viem"; -import { type AbiEvent } from "abitype"; -import { AbiEventMapping } from "src/dataMappings/utils/actionTypes"; -import { createResultObject } from "src/dataMappings/utils/createResultObject"; -import { configureSDK, getPublicClient } from "src/sdk"; +import { parseAbiItem, type AbiEvent } from "viem"; +import { AbiEventMapping } from "../utils/actionTypes"; +import { createResultObject } from "../utils/createResultObject"; +import { getPublicClient } from "../../sdk"; +import { SdkNotConfiguredError } from "../../errors"; -export const eventAction = async (mapping: AbiEventMapping, alchemyApiKey: string) => { - configureSDK({ apiKey: alchemyApiKey }); +export const eventAction = async (mapping: AbiEventMapping) => { const publicClient = getPublicClient(); + if (!publicClient) { + throw new SdkNotConfiguredError(); + } + const { abi: source, address, eventFilter, seek, populate } = mapping; const parsedAbi = parseAbiItem(source) as AbiEvent; @@ -15,8 +18,8 @@ export const eventAction = async (mapping: AbiEventMapping, alchemyApiKey: strin address, event: parsedAbi, args: eventFilter.args, - fromBlock: eventFilter.fromBlock ? BigInt(eventFilter.fromBlock.toString()) : undefined, - toBlock: eventFilter.toBlock ? BigInt(eventFilter.toBlock.toString()) : undefined, + fromBlock: eventFilter.fromBlock, + toBlock: eventFilter.toBlock, }); const contractEvent = await publicClient.getFilterLogs({ filter }); diff --git a/kleros-sdk/src/dataMappings/actions/fetchIpfsJsonAction.ts b/kleros-sdk/src/dataMappings/actions/fetchIpfsJsonAction.ts index f2ea6f29c..7a648dd02 100644 --- a/kleros-sdk/src/dataMappings/actions/fetchIpfsJsonAction.ts +++ b/kleros-sdk/src/dataMappings/actions/fetchIpfsJsonAction.ts @@ -1,6 +1,7 @@ -import { FetchIpfsJsonMapping } from "src/dataMappings/utils/actionTypes"; -import { createResultObject } from "src/dataMappings/utils/createResultObject"; -import { MAX_BYTE_SIZE } from "src/consts"; +import { MAX_BYTE_SIZE } from "../../consts"; +import { RequestError } from "../../errors"; +import { FetchIpfsJsonMapping } from "../utils/actionTypes"; +import { createResultObject } from "../utils/createResultObject"; export const fetchIpfsJsonAction = async (mapping: FetchIpfsJsonMapping) => { const { ipfsUri, seek, populate } = mapping; @@ -13,26 +14,26 @@ export const fetchIpfsJsonAction = async (mapping: FetchIpfsJsonMapping) => { } else if (!ipfsUri.startsWith("http")) { httpUri = `https://ipfs.io/ipfs/${ipfsUri}`; } else { - throw new Error("Invalid IPFS URI format"); + throw new RequestError("Invalid IPFS URI format", httpUri); } const response = await fetch(httpUri, { method: "GET" }); if (!response.ok) { - throw new Error("Failed to fetch data from IPFS"); + throw new RequestError("Failed to fetch data from IPFS", httpUri); } const contentLength = response.headers.get("content-length"); if (contentLength && parseInt(contentLength) > MAX_BYTE_SIZE) { - throw new Error("Response size is too large"); + throw new RequestError("Response size is too large", httpUri); } const contentType = response.headers.get("content-type"); if (!contentType || !contentType.includes("application/json")) { - throw new Error("Fetched data is not JSON"); + throw new RequestError("Fetched data is not JSON", httpUri); } - const data = await response.json(); + const data = (await response.json()) as any; return createResultObject(data, seek, populate); }; diff --git a/kleros-sdk/src/dataMappings/actions/jsonAction.ts b/kleros-sdk/src/dataMappings/actions/jsonAction.ts index 87e787131..86b14447d 100644 --- a/kleros-sdk/src/dataMappings/actions/jsonAction.ts +++ b/kleros-sdk/src/dataMappings/actions/jsonAction.ts @@ -1,5 +1,5 @@ import { JsonMapping } from "../utils/actionTypes"; -import { createResultObject } from "src/dataMappings/utils/createResultObject"; +import { createResultObject } from "../utils/createResultObject"; export const jsonAction = (mapping: JsonMapping) => { const { value, seek, populate } = mapping; diff --git a/kleros-sdk/src/dataMappings/actions/subgraphAction.ts b/kleros-sdk/src/dataMappings/actions/subgraphAction.ts index 018579c04..d3d7b492b 100644 --- a/kleros-sdk/src/dataMappings/actions/subgraphAction.ts +++ b/kleros-sdk/src/dataMappings/actions/subgraphAction.ts @@ -1,5 +1,5 @@ import { SubgraphMapping } from "../utils/actionTypes"; -import { createResultObject } from "src/dataMappings/utils/createResultObject"; +import { createResultObject } from "../utils/createResultObject"; export const subgraphAction = async (mapping: SubgraphMapping) => { const { endpoint, query, variables, seek, populate } = mapping; @@ -13,7 +13,7 @@ export const subgraphAction = async (mapping: SubgraphMapping) => { body: JSON.stringify({ query, variables }), }); - const { data } = await response.json(); + const { data } = (await response.json()) as any; return createResultObject(data, seek, populate); }; diff --git a/kleros-sdk/src/dataMappings/dataMapping.json b/kleros-sdk/src/dataMappings/dataMapping.json deleted file mode 100644 index 27f5ed4fd..000000000 --- a/kleros-sdk/src/dataMappings/dataMapping.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { - "type": "subgraph", - "endpoint": "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2", - "query": "query($id: ID!) { pair(id: $id) { id token0Price token1Price } }", - "seek": [ - "token0Price", - "token1Price" - ], - "populate": [ - "price1", - "price2" - ] - }, - { - "type": "abi/event", - "abi": "event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount)", - "address": "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2", - "eventFilter": { - "fromBlock": "36205881", - "toBlock": "latest", - "args": { - "_courtID": 1 - } - }, - "seek": [ - "amount" - ], - "populate": [ - "amount" - ] - }, - { - "type": "abi/call", - "abi": "function appealCost(uint256 _disputeID) public view returns (uint256)", - "address": "0x5a2bC1477ABE705dB4955Cda7DE064eA79D563d1", - "args": [ - "1" - ], - "seek": [ - "cost" - ], - "populate": [ - "cost" - ] - }, - { - "type": "json", - "value": { - "name": "John Doe", - "age": 30, - "email": "johndoe@example.com" - }, - "seek": [ - "name", - "age", - "email" - ], - "populate": [ - "name", - "age", - "email" - ] - }, - { - "type": "fetch/ipfs/json", - "ipfsUri": "ipfs://QmZ3Cmnip8bmFNruuTuCdxPymEjyK9VcQEyf2beDYcaHaK/metaEvidence.json", - "seek": [ - "title" - ], - "populate": [ - "title" - ] - } -] diff --git a/kleros-sdk/src/dataMappings/dataMapping.ts b/kleros-sdk/src/dataMappings/dataMapping.ts deleted file mode 100644 index 17dafb8f1..000000000 --- a/kleros-sdk/src/dataMappings/dataMapping.ts +++ /dev/null @@ -1,92 +0,0 @@ -export type SubgraphMapping = { - endpoint: string; // Subgraph endpoint - query: string; // Subgraph query - seek: string[]; // Subgraph query parameters value used to populate the template variables - populate: string[]; // Populated template variables -}; - -export type AbiEventMapping = { - abi: string; // ABI of the contract emitting the event - address: string; // Address of the contract emitting the event - eventFilter: { - // Event filter (eg. specific parameter value, block number range, event index) - fromBlock: BigInt | string; // Block number range start - toBlock: BigInt | string; // Block number range end - args: any; // Event parameter value to filter on - }; - seek: string[]; // Event parameters value used to populate the template variables - populate: string[]; // Populated template variables -}; - -export type AbiCallMapping = { - abi: string; // ABI of the contract emitting the event - address: string; // Address of the contract emitting the event - args: any[]; // Function arguments - seek: string[]; // Call return parameters used to populate the template variables - populate: string[]; // Populated template variables -}; - -export type JsonMapping = { - value: object; // Hardcoded object, to be stringified. - seek: string[]; // JSON keys used to populate the template variables - populate: string[]; // Populated template variables -}; - -export type FetchIpfsJsonMapping = { - ipfsUri: string; // IPFS URL - seek: string[]; // JSON keys used to populate the template variables - populate: string[]; // Populated template variables -}; - -const subgraphMappingExample: SubgraphMapping = { - endpoint: "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2", - query: ` - query($id: ID!) { - pair(id: $id) { - id - token0Price - token1Price - } - } - `, - seek: ["token0Price", "token1Price"], - populate: ["price1", "price2"], -}; - -const abiEventMappingExample: AbiEventMapping = { - abi: "event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount)", - address: "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2", - eventFilter: { - fromBlock: BigInt(36205881), - toBlock: "latest", - args: { - _courtID: 1, - }, - }, - seek: ["amount"], - populate: ["amount"], -}; - -const abiCallMappingExample: AbiCallMapping = { - abi: "function appealCost(uint256 _disputeID) public view returns (uint256)", - address: "0x5a2bC1477ABE705dB4955Cda7DE064eA79D563d1", - args: [BigInt(1)], - seek: ["cost"], - populate: ["cost"], -}; - -const jsonMappingExample: JsonMapping = { - value: { - name: "John Doe", - age: 30, - email: "johndoe@example.com", - }, - seek: ["name", "age", "email"], - populate: ["name", "age", "email"], -}; - -const fetchIpfsJsonMappingExample: FetchIpfsJsonMapping = { - ipfsUri: "ipfs://QmZ3Cmnip8bmFNruuTuCdxPymEjyK9VcQEyf2beDYcaHaK/metaEvidence.json", - seek: ["title"], - populate: ["title"], -}; diff --git a/kleros-sdk/src/dataMappings/decoder.ts b/kleros-sdk/src/dataMappings/decoder.ts deleted file mode 100644 index b30e6e927..000000000 --- a/kleros-sdk/src/dataMappings/decoder.ts +++ /dev/null @@ -1,55 +0,0 @@ -import request from "graphql-request"; -import { TypedDocumentNode } from "@graphql-typed-document-node/core"; -import { DisputeDetails } from "./disputeDetails"; - -export type Decoder = (externalDisputeID: string, disputeTemplate: Partial) => Promise; - -// https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md -export type CAIP10 = `eip155:${number}:0x${string}`; - -export const graphqlQueryFnHelper = async ( - url: string, - query: TypedDocumentNode, - parametersObject: Record, - chainId = 421613 -) => { - return request(url, query, parametersObject); -}; - -// TODO: generate graphql query -const disputeTemplateQuery = graphql(` - query DisputeTemplate($id: ID!) { - disputeTemplate(id: $id) { - id - templateTag - templateData - templateDataMappings - } - } -`); - -export const genericDecoder = async ( - externalDisputeID: string, - arbitrableDisputeID: string, - disputeTemplateID: string, - disputeTemplateRegistry: CAIP10 -): Promise => { - let subgraphUrl; - switch (disputeTemplateRegistry) { - case "eip155:421613:0x22A58a17F12A718d18C9B6Acca3E311Da1b00A04": // Devnet - subgraphUrl = process.env.REACT_APP_DISPUTE_TEMPLATE_ARBGOERLI_SUBGRAPH_DEVNET; - break; - case "eip155:421613:0xA55D4b90c1F8D1fD0408232bF6FA498dD6786385": // Testnet - subgraphUrl = process.env.REACT_APP_DISPUTE_TEMPLATE_ARBGOERLI_SUBGRAPH_TESTNET; - break; - default: - throw new Error(`Unsupported dispute template registry: ${disputeTemplateRegistry}`); - } - const { disputeTemplate } = await request(subgraphUrl, disputeTemplateQuery, { id: disputeTemplateID.toString() }); - switch (disputeTemplate.specification) { - case "KIP99": - return await kip99Decoder(externalDisputeID, disputeTemplate); - default: - throw new Error(`Unsupported dispute template specification: ${disputeTemplate.specification}`); - } -}; diff --git a/kleros-sdk/src/dataMappings/disputeDetails.ts b/kleros-sdk/src/dataMappings/disputeDetails.ts deleted file mode 100644 index 28a3133a6..000000000 --- a/kleros-sdk/src/dataMappings/disputeDetails.ts +++ /dev/null @@ -1,39 +0,0 @@ -export type DisputeDetails = { - title: string; - description: string; - question: string; - type: QuestionType; - answers: Answer[]; - policyURI: string; - attachment: Attachment; - frontendUrl: string; - arbitrableChainID: string; - arbitrableAddress: `0x${string}`; - arbitratorChainID: string; - arbitratorAddress: `0x${string}`; - category: string; - lang: string; - specification: string; - version: string; - // missing metadata -}; - -export enum QuestionType { - Bool = "bool", - Datetime = "datetime", - MultipleSelect = "multiple-select", - SingleSelect = "single-select", - Uint = "uint", -} - -export type Answer = { - title: string; - description: string; - id: `0x${string}`; - reserved: boolean; -}; - -export type Attachment = { - label: string; - uri: string; -}; diff --git a/kleros-sdk/src/dataMappings/executeActions.ts b/kleros-sdk/src/dataMappings/executeActions.ts index 013e1d5d6..f78e4e3b6 100644 --- a/kleros-sdk/src/dataMappings/executeActions.ts +++ b/kleros-sdk/src/dataMappings/executeActions.ts @@ -1,3 +1,4 @@ +import { UnsupportedActionError } from "../errors"; import { callAction } from "./actions/callAction"; import { eventAction } from "./actions/eventAction"; import { fetchIpfsJsonAction } from "./actions/fetchIpfsJsonAction"; @@ -32,16 +33,16 @@ export const executeAction = async ( case "json": return jsonAction(validateJsonMapping(mapping)); case "abi/call": - return await callAction(validateAbiCallMapping(mapping), context.alchemyApiKey as string); + return await callAction(validateAbiCallMapping(mapping)); case "abi/event": - return await eventAction(validateAbiEventMapping(mapping), context.alchemyApiKey as string); + return await eventAction(validateAbiEventMapping(mapping)); case "fetch/ipfs/json": return await fetchIpfsJsonAction(validateFetchIpfsJsonMapping(mapping)); case "reality": mapping = validateRealityMapping(mapping); return await retrieveRealityData(mapping.realityQuestionID, context.arbitrableAddress as Address); default: - throw new Error(`Unsupported action type: ${mapping.type}`); + throw new UnsupportedActionError(`Unsupported action type: ${JSON.stringify(mapping)}`); } }; diff --git a/kleros-sdk/src/dataMappings/index.ts b/kleros-sdk/src/dataMappings/index.ts new file mode 100644 index 000000000..74599330a --- /dev/null +++ b/kleros-sdk/src/dataMappings/index.ts @@ -0,0 +1 @@ +export * from "./executeActions"; diff --git a/kleros-sdk/src/dataMappings/retrieveRealityData.ts b/kleros-sdk/src/dataMappings/retrieveRealityData.ts index be3b377a1..ab8b8f6b0 100644 --- a/kleros-sdk/src/dataMappings/retrieveRealityData.ts +++ b/kleros-sdk/src/dataMappings/retrieveRealityData.ts @@ -1,9 +1,18 @@ +import { InvalidContextError, NotFoundError } from "../errors"; import { executeAction } from "./executeActions"; import { AbiEventMapping } from "./utils/actionTypes"; +export type RealityAnswer = { + title: string; + description: string; + id: string; + reserved: boolean; + last?: boolean; +}; + export const retrieveRealityData = async (realityQuestionID: string, arbitrable?: `0x${string}`) => { if (!arbitrable) { - throw new Error("No arbitrable address provided"); + throw new InvalidContextError("No arbitrable address provided"); } const questionMapping: AbiEventMapping = { type: "abi/event", @@ -11,7 +20,7 @@ export const retrieveRealityData = async (realityQuestionID: string, arbitrable? address: arbitrable, eventFilter: { args: [realityQuestionID], - fromBlock: "0x1", + fromBlock: "earliest", toBlock: "latest", }, seek: [ @@ -49,7 +58,7 @@ export const retrieveRealityData = async (realityQuestionID: string, arbitrable? address: arbitrable, eventFilter: { args: [0], - fromBlock: "0x1", + fromBlock: "earliest", toBlock: "latest", }, seek: ["template_id", "question_text"], @@ -59,8 +68,12 @@ export const retrieveRealityData = async (realityQuestionID: string, arbitrable? const templateData = await executeAction(templateMapping); console.log("templateData", templateData); - if (!templateData || !questionData) { - throw new Error("Failed to retrieve template or question data"); + if (!templateData) { + throw new NotFoundError("Template Data", "Failed to retrieve template data"); + } + + if (!questionData) { + throw new NotFoundError("Question Data", "Failed to retrieve question data"); } const rc_question = require("@reality.eth/reality-eth-lib/formatters/question.js"); @@ -71,14 +84,6 @@ export const retrieveRealityData = async (realityQuestionID: string, arbitrable? console.log("populatedTemplate", populatedTemplate); - interface RealityAnswer { - title: string; - description: string; - id: string; - reserved: boolean; - last?: boolean; - } - let answers: RealityAnswer[] = []; if (populatedTemplate.type === "bool") { answers = [ diff --git a/kleros-sdk/src/dataMappings/utils/actionTypeValidators.ts b/kleros-sdk/src/dataMappings/utils/actionTypeValidators.ts index b95296043..74b064922 100644 --- a/kleros-sdk/src/dataMappings/utils/actionTypeValidators.ts +++ b/kleros-sdk/src/dataMappings/utils/actionTypeValidators.ts @@ -1,3 +1,4 @@ +import { InvalidMappingError } from "../../errors"; import { SubgraphMapping, AbiEventMapping, @@ -5,47 +6,40 @@ import { JsonMapping, ActionMapping, FetchIpfsJsonMapping, - RealityMapping, } from "./actionTypes"; export const validateSubgraphMapping = (mapping: ActionMapping) => { - if ((mapping as SubgraphMapping).endpoint === undefined) { - throw new Error("Invalid mapping for graphql action."); - } - return mapping as SubgraphMapping; + return validateMapping(mapping as SubgraphMapping, ["endpoint"]); }; export const validateAbiEventMapping = (mapping: ActionMapping) => { - if ((mapping as AbiEventMapping).abi === undefined || (mapping as AbiEventMapping).eventFilter === undefined) { - throw new Error("Invalid mapping for abi/event action."); - } - return mapping as AbiEventMapping; + return validateMapping(mapping as AbiEventMapping, ["abi", "eventFilter"]); }; export const validateAbiCallMapping = (mapping: ActionMapping) => { - if ((mapping as AbiCallMapping).abi === undefined || (mapping as AbiCallMapping).args === undefined) { - throw new Error("Invalid mapping for abi/call action."); - } - return mapping as AbiCallMapping; + return validateMapping(mapping as AbiCallMapping, ["abi", "functionName"]); }; export const validateJsonMapping = (mapping: ActionMapping) => { - if ((mapping as JsonMapping).value === undefined) { - throw new Error("Invalid mapping for json action."); - } - return mapping as JsonMapping; + return validateMapping(mapping as JsonMapping, ["value"]); }; export const validateFetchIpfsJsonMapping = (mapping: ActionMapping) => { - if ((mapping as FetchIpfsJsonMapping).ipfsUri === undefined) { - throw new Error("Invalid mapping for fetch/ipfs/json action."); - } - return mapping as FetchIpfsJsonMapping; + return validateMapping(mapping as FetchIpfsJsonMapping, ["ipfsUri"]); }; export const validateRealityMapping = (mapping: ActionMapping) => { - if (mapping.type !== "reality" || typeof (mapping as RealityMapping).realityQuestionID !== "string") { - throw new Error("Invalid mapping for reality action."); + if (mapping.type !== "reality" || typeof mapping.realityQuestionID !== "string") { + throw new InvalidMappingError("Expected field 'realityQuestionID' to be a string."); + } + return mapping; +}; + +const validateMapping = (mapping: T, requiredFields: (keyof T)[]) => { + for (const field of requiredFields) { + if (mapping[field] === undefined) { + throw new InvalidMappingError(`${field.toString()} is required for ${mapping.type}`); + } } - return mapping as RealityMapping; + return mapping; }; diff --git a/kleros-sdk/src/dataMappings/utils/actionTypes.ts b/kleros-sdk/src/dataMappings/utils/actionTypes.ts index efc7a2343..630afda6b 100644 --- a/kleros-sdk/src/dataMappings/utils/actionTypes.ts +++ b/kleros-sdk/src/dataMappings/utils/actionTypes.ts @@ -1,52 +1,45 @@ -import { type Address } from "viem"; +import { type Address, type BlockNumber, type BlockTag } from "viem"; -export type JsonMapping = { - type: string; - value: object; +type MappingType = "graphql" | "abi/call" | "abi/event" | "json" | "fetch/ipfs/json" | "reality"; + +type AbstractMapping = { + type: T; seek: string[]; populate: string[]; }; -export interface SubgraphMapping { - type: string; +export type JsonMapping = AbstractMapping<"json"> & { + value: object; +}; + +export type SubgraphMapping = AbstractMapping<"graphql"> & { endpoint: string; query: string; variables: { [key: string]: unknown }; - seek: string[]; - populate: string[]; -} +}; -export type AbiCallMapping = { - type: string; +export type AbiCallMapping = AbstractMapping<"abi/call"> & { abi: string; address: Address; + functionName: string; args: any[]; - seek: string[]; - populate: string[]; }; -export type AbiEventMapping = { - type: string; +export type AbiEventMapping = AbstractMapping<"abi/event"> & { abi: string; address: Address; eventFilter: { - fromBlock: BigInt | string; - toBlock: BigInt | string; + fromBlock: BlockNumber | BlockTag; + toBlock: BlockNumber | BlockTag; args: any; }; - seek: string[]; - populate: string[]; }; -export type FetchIpfsJsonMapping = { - type: string; +export type FetchIpfsJsonMapping = AbstractMapping<"fetch/ipfs/json"> & { ipfsUri: string; - seek: string[]; - populate: string[]; }; -export type RealityMapping = { - type: "reality"; +export type RealityMapping = AbstractMapping<"reality"> & { realityQuestionID: string; }; diff --git a/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts b/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts index 3366cf750..d9948ea70 100644 --- a/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts +++ b/kleros-sdk/src/dataMappings/utils/disputeDetailsSchema.ts @@ -9,7 +9,7 @@ export const isMultiaddr = (str: string): boolean => str ); -export const ethAddressSchema = z.string().refine((value) => isAddress(value), { +export const ethAddressSchema = z.string().refine((value) => isAddress(value, { strict: false }), { message: "Provided address is invalid.", }); diff --git a/kleros-sdk/src/dataMappings/utils/index.ts b/kleros-sdk/src/dataMappings/utils/index.ts new file mode 100644 index 000000000..bacf46150 --- /dev/null +++ b/kleros-sdk/src/dataMappings/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./populateTemplate"; +export * from "./retrieveVariables"; +export * from "./disputeDetailsTypes"; diff --git a/kleros-sdk/src/dataMappings/utils/populateTemplate.ts b/kleros-sdk/src/dataMappings/utils/populateTemplate.ts index 57377706b..1ad8c319b 100644 --- a/kleros-sdk/src/dataMappings/utils/populateTemplate.ts +++ b/kleros-sdk/src/dataMappings/utils/populateTemplate.ts @@ -1,6 +1,7 @@ import mustache from "mustache"; import { DisputeDetails } from "./disputeDetailsTypes"; import DisputeDetailsSchema from "./disputeDetailsSchema"; +import { InvalidFormatError } from "../../errors"; export const populateTemplate = (mustacheTemplate: string, data: any): DisputeDetails => { const render = mustache.render(mustacheTemplate, data); @@ -9,7 +10,7 @@ export const populateTemplate = (mustacheTemplate: string, data: any): DisputeDe const validation = DisputeDetailsSchema.safeParse(dispute); if (!validation.success) { console.error("Validation errors:", validation.error.errors, "\n\nDispute details:", `${JSON.stringify(dispute)}`); - throw new Error("Invalid dispute details format"); + throw new InvalidFormatError("Invalid dispute details format"); } console.log(dispute); diff --git a/kleros-sdk/src/dataMappings/utils/replacePlaceholdersWithValues.ts b/kleros-sdk/src/dataMappings/utils/replacePlaceholdersWithValues.ts index d628afb7e..2f5520ef7 100644 --- a/kleros-sdk/src/dataMappings/utils/replacePlaceholdersWithValues.ts +++ b/kleros-sdk/src/dataMappings/utils/replacePlaceholdersWithValues.ts @@ -1,5 +1,7 @@ import mustache from "mustache"; +import retrieveVariables from "./retrieveVariables"; import { ActionMapping } from "./actionTypes"; +import { InvalidContextError } from "../../errors"; export function replacePlaceholdersWithValues( mapping: ActionMapping, @@ -7,6 +9,7 @@ export function replacePlaceholdersWithValues( ): ActionMapping | ActionMapping[] { function replace(obj: ActionMapping): ActionMapping | ActionMapping[] { if (typeof obj === "string") { + validateContext(obj, context); return mustache.render(obj, context) as unknown as ActionMapping; } else if (Array.isArray(obj)) { return obj.map(replace) as unknown as ActionMapping[]; @@ -21,3 +24,18 @@ export function replacePlaceholdersWithValues( return replace(mapping); } + +/** + * + * @param template + * @param context + * @description retrieves all variables from a template and validates if they are provided in the context + */ +const validateContext = (template: string, context: Record) => { + const variables = retrieveVariables(template); + + variables.forEach((variable) => { + if (!context[variable]) throw new InvalidContextError(`Expected key "${variable}" to be provided in context.`); + }); + return true; +}; diff --git a/kleros-sdk/src/dataMappings/utils/retrieveVariables.ts b/kleros-sdk/src/dataMappings/utils/retrieveVariables.ts new file mode 100644 index 000000000..386dbe4a7 --- /dev/null +++ b/kleros-sdk/src/dataMappings/utils/retrieveVariables.ts @@ -0,0 +1,20 @@ +import mustache from "mustache"; + +/** + * + * @param template + * @returns Variables[] returns the variables in a template string + * @note This is a naive implementation and wil not work for complex tags, only works for {{}} and {{{}}} tags for now. + * Reference : https://github.com/janl/mustache.js/issues/538 + */ +const retrieveVariables = (template: string) => + mustache + .parse(template) + .filter(function (v) { + return v[0] === "name" || v[0] === "&"; + }) // add more conditions here to include more tags + .map(function (v) { + return v[1]; + }); + +export default retrieveVariables; diff --git a/kleros-sdk/src/errors/index.ts b/kleros-sdk/src/errors/index.ts new file mode 100644 index 000000000..af000ba37 --- /dev/null +++ b/kleros-sdk/src/errors/index.ts @@ -0,0 +1,57 @@ +export class CustomError extends Error { + constructor(name: string, message: string) { + super(message); + this.name = name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +} + +export class InvalidContextError extends CustomError { + constructor(message: string) { + super("InvalidContextError", message); + } +} + +export class InvalidMappingError extends CustomError { + constructor(message: string) { + super("InvalidMappingError", message); + } +} + +export class NotFoundError extends CustomError { + public resourceName: string; + + constructor(resourceName: string, message: string) { + super("NotFoundError", message); + this.resourceName = resourceName; + } +} +export class RequestError extends CustomError { + public endpoint: string | undefined; + + constructor(message: string, endpoint?: string) { + super("RequestError", message); + this.endpoint = endpoint; + } +} + +export class UnsupportedActionError extends CustomError { + constructor(message: string) { + super("UnsupportedActionError", message); + } +} + +export class InvalidFormatError extends CustomError { + constructor(message: string) { + super("InvalidFormatError", message); + } +} + +export class SdkNotConfiguredError extends CustomError { + constructor() { + super("SdkNotConfiguredError", "SDK not configured. Please call `configureSDK` before using."); + } +} diff --git a/kleros-sdk/src/index.ts b/kleros-sdk/src/index.ts new file mode 100644 index 000000000..92719d922 --- /dev/null +++ b/kleros-sdk/src/index.ts @@ -0,0 +1,5 @@ +export * from "./sdk"; +export * from "./types"; +export * from "./utils"; +export * from "./dataMappings"; +export * from "./dataMappings/utils"; diff --git a/kleros-sdk/src/requests/fetchDisputeDetails.ts b/kleros-sdk/src/requests/fetchDisputeDetails.ts new file mode 100644 index 000000000..adb58c3f8 --- /dev/null +++ b/kleros-sdk/src/requests/fetchDisputeDetails.ts @@ -0,0 +1,53 @@ +import { RequestError } from "../errors"; +import { CombinedError, gql } from "@urql/core"; +import getClient from "./gqlClient"; + +type DisputeDetailsQueryResponse = { + dispute: { + arbitrated: { + id: string; + }; + arbitrableChainId: number; + externalDisputeId: number; + templateId: number; + }; +}; + +const query = gql` + query DisputeDetails($id: ID!) { + dispute(id: $id) { + arbitrated { + id + } + arbitrableChainId + externalDisputeId + templateId + } + } +`; + +const fetchDisputeDetails = async (endpoint: string, id: bigint) => { + const variables = { id: id.toString() }; + + try { + const client = getClient(endpoint); + return client + .query(query, variables) + .toPromise() + .then((res) => { + if (res?.error) { + throw res.error; + } + return res?.data; + }); + } catch (error: unknown) { + if (error instanceof CombinedError) { + throw error; + } else if (error instanceof Error) { + throw new RequestError(`Error querying Dispute Details: ${error.message}`, endpoint); + } + throw new RequestError("An unknown error occurred while querying Dispute Details", endpoint); + } +}; + +export default fetchDisputeDetails; diff --git a/kleros-sdk/src/requests/fetchDisputeTemplateFromId.ts b/kleros-sdk/src/requests/fetchDisputeTemplateFromId.ts new file mode 100644 index 000000000..acc57a3f3 --- /dev/null +++ b/kleros-sdk/src/requests/fetchDisputeTemplateFromId.ts @@ -0,0 +1,44 @@ +import { CombinedError, gql } from "@urql/core"; +import { RequestError } from "../errors"; +import getClient from "./gqlClient"; + +type DisputeTemplateQueryResponse = { + disputeTemplate: { + templateData: string; + templateDataMappings: string; + }; +}; +const query = gql` + query DisputeTemplate($id: ID!) { + disputeTemplate(id: $id) { + templateData + templateDataMappings + } + } +`; + +const fetchDisputeTemplateFromId = async (endpoint: string, id: number) => { + const variables = { id: id.toString() }; + + try { + const client = getClient(endpoint); + return client + .query(query, variables) + .toPromise() + .then((res) => { + if (res?.error) { + throw res.error; + } + return res?.data; + }); + } catch (error: unknown) { + if (error instanceof CombinedError) { + throw error; + } else if (error instanceof Error) { + throw new RequestError(`Error querying Dispute Template: ${error.message}`, endpoint); + } + throw new RequestError("An unknown error occurred while querying Dispute Template", endpoint); + } +}; + +export default fetchDisputeTemplateFromId; diff --git a/kleros-sdk/src/requests/gqlClient.ts b/kleros-sdk/src/requests/gqlClient.ts new file mode 100644 index 000000000..dcae74a31 --- /dev/null +++ b/kleros-sdk/src/requests/gqlClient.ts @@ -0,0 +1,18 @@ +import { cacheExchange, Client, fetchExchange } from "@urql/core"; + +const clients = new Map(); + +const getClient = (endpoint: string) => { + let client = clients.get(endpoint); + + if (!client) { + client = new Client({ + url: endpoint, + exchanges: [cacheExchange, fetchExchange], + }); + clients.set(endpoint, client); + } + return client; +}; + +export default getClient; diff --git a/kleros-sdk/src/sdk.ts b/kleros-sdk/src/sdk.ts index bec9ff64c..00e9c2940 100644 --- a/kleros-sdk/src/sdk.ts +++ b/kleros-sdk/src/sdk.ts @@ -1,22 +1,18 @@ -import { createPublicClient, webSocket, type PublicClient } from "viem"; -import { arbitrumSepolia } from "viem/chains"; +import { createPublicClient, type PublicClient } from "viem"; +import { SdkConfig } from "./types"; +import { SdkNotConfiguredError } from "./errors"; let publicClient: PublicClient | undefined; -export const configureSDK = (config: { apiKey?: string }) => { - if (config.apiKey) { - const ALCHEMY_API_KEY = config.apiKey; - const transport = webSocket(`wss://arb-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`); - publicClient = createPublicClient({ - chain: arbitrumSepolia, - transport, - }); +export const configureSDK = (config: SdkConfig) => { + if (config.client) { + publicClient = createPublicClient(config.client); } }; -export const getPublicClient = (): PublicClient => { +export const getPublicClient = (): PublicClient | undefined => { if (!publicClient) { - throw new Error("SDK not configured. Please call `configureSDK` before using."); + throw new SdkNotConfiguredError(); } return publicClient; }; diff --git a/kleros-sdk/src/types/index.ts b/kleros-sdk/src/types/index.ts new file mode 100644 index 000000000..1d4c58190 --- /dev/null +++ b/kleros-sdk/src/types/index.ts @@ -0,0 +1,17 @@ +import { PublicClientConfig } from "viem"; + +export type SdkConfig = { + client: PublicClientConfig; +}; + +type GetDisputeParametersOptions = { + sdkConfig?: SdkConfig; + additionalContext?: Record; +}; + +export type GetDisputeParameters = { + disputeId: bigint; + coreSubgraph: string; + dtrSubgraph: string; + options?: GetDisputeParametersOptions; +}; diff --git a/kleros-sdk/src/utils/getDispute.ts b/kleros-sdk/src/utils/getDispute.ts new file mode 100644 index 000000000..c73fa1f24 --- /dev/null +++ b/kleros-sdk/src/utils/getDispute.ts @@ -0,0 +1,60 @@ +import { executeActions } from "../dataMappings"; +import { DisputeDetails, populateTemplate } from "../dataMappings/utils"; +import { NotFoundError } from "../errors"; +import fetchDisputeDetails from "../requests/fetchDisputeDetails"; +import fetchDisputeTemplateFromId from "../requests/fetchDisputeTemplateFromId"; +import { configureSDK } from "../sdk"; +import { GetDisputeParameters } from "../types"; + +/** + * Retrieves dispute parameters based on the provided dispute ID and subgraph endpoints. + * + * @param {GetDisputeParameters} disputeParameters - The parameters required to get the dispute. + * @param {bigint} disputeParameters.disputeId - A unique numeric identifier of the dispute in the Kleros Core contract. + * @param {string} disputeParameters.coreSubgraph - Endpoint for the Kleros core subgraph to use. + * @param {string} disputeParameters.dtrSubgraph - Endpoint for the Kleros dispute template registry subgraph. + * @param {GetDisputeParametersOptions | undefined} disputeParameters.options - Optional parameters to configure the SDK and provide additional context, if not configured already. + */ +export const getDispute = async (disputeParameters: GetDisputeParameters): Promise => { + if (disputeParameters.options?.sdkConfig) { + configureSDK(disputeParameters.options.sdkConfig); + } + const { disputeId, dtrSubgraph, coreSubgraph, options } = disputeParameters; + + const disputeDetails = await fetchDisputeDetails(coreSubgraph, disputeId); + + if (!disputeDetails?.dispute) { + throw new NotFoundError("Dispute Details", `Dispute details not found for disputeId: ${disputeId}`); + } + + const template = await fetchDisputeTemplateFromId(dtrSubgraph, disputeDetails.dispute.templateId); + + if (!template) { + throw new NotFoundError( + "Dispute Template", + `Template not found for template ID: ${disputeDetails.dispute.templateId}` + ); + } + + const { templateData, templateDataMappings } = template.disputeTemplate; + + const initialContext = { + arbitrableAddress: disputeDetails.dispute.arbitrated.id, + arbitrableChainID: disputeDetails.dispute.arbitrableChainId, + externalDisputeID: disputeDetails.dispute.externalDisputeId, + ...options?.additionalContext, + }; + + let data = {}; + if (templateDataMappings) { + try { + data = await executeActions(JSON.parse(templateDataMappings), initialContext); + } catch (err: any) { + throw err; + } + } + + const populatedTemplate = populateTemplate(templateData, data); + + return populatedTemplate; +}; diff --git a/kleros-sdk/src/utils/index.ts b/kleros-sdk/src/utils/index.ts new file mode 100644 index 000000000..4858866fa --- /dev/null +++ b/kleros-sdk/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./getDispute"; diff --git a/kleros-sdk/test/__snapshots__/disputeDetailsSchema.test.ts.snap b/kleros-sdk/test/__snapshots__/disputeDetailsSchema.test.ts.snap new file mode 100644 index 000000000..35eaeb093 --- /dev/null +++ b/kleros-sdk/test/__snapshots__/disputeDetailsSchema.test.ts.snap @@ -0,0 +1,19 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Dispute Details Schema > snapshot 1`] = ` +{ + "foo": "bar", +} +`; + +exports[`Kleros SDK schema > snapshot 1`] = ` +{ + "foo": "bar", +} +`; + +exports[`Kleros SDK schemas > snapshot 1`] = ` +{ + "foo": "bar", +} +`; diff --git a/kleros-sdk/test/__snapshots__/schema.test.ts.snap b/kleros-sdk/test/__snapshots__/schema.test.ts.snap new file mode 100644 index 000000000..6ee93505c --- /dev/null +++ b/kleros-sdk/test/__snapshots__/schema.test.ts.snap @@ -0,0 +1,7 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Kleros SDK schema > snapshot 1`] = ` +{ + "foo": "bar", +} +`; diff --git a/kleros-sdk/test/dataMappings.test.ts b/kleros-sdk/test/dataMappings.test.ts index 2b9559197..ecff6a1eb 100644 --- a/kleros-sdk/test/dataMappings.test.ts +++ b/kleros-sdk/test/dataMappings.test.ts @@ -1,12 +1,18 @@ import { describe, expect, it, vi } from "vitest"; -import { populateTemplate } from "src/dataMappings/utils/populateTemplate"; -import { jsonAction } from "src/dataMappings/actions/jsonAction"; -import { subgraphAction } from "src/dataMappings/actions/subgraphAction"; -import { callAction } from "src/dataMappings/actions/callAction"; -import { eventAction } from "src/dataMappings/actions/eventAction"; -import { fetchIpfsJsonAction } from "src/dataMappings/actions/fetchIpfsJsonAction"; -import { createResultObject } from "src/dataMappings/utils/createResultObject"; -import { executeActions } from "src/dataMappings/executeActions"; +import { createResultObject } from "../src/dataMappings/utils/createResultObject"; +import { executeActions, populateTemplate } from "../src"; +import { + AbiCallMapping, + AbiEventMapping, + FetchIpfsJsonMapping, + JsonMapping, + SubgraphMapping, +} from "../src/dataMappings/utils/actionTypes"; +import { jsonAction } from "../src/dataMappings/actions/jsonAction"; +import { subgraphAction } from "../src/dataMappings/actions/subgraphAction"; +import { callAction } from "../src/dataMappings/actions/callAction"; +import { eventAction } from "../src/dataMappings/actions/eventAction"; +import { fetchIpfsJsonAction } from "../src/dataMappings/actions/fetchIpfsJsonAction"; global.fetch = vi.fn().mockResolvedValue({ json: async () => ({ @@ -19,7 +25,7 @@ global.fetch = vi.fn().mockResolvedValue({ }), }); -vi.mock("src/sdk", () => ({ +vi.mock("../src/sdk", () => ({ configureSDK: vi.fn(), getPublicClient: vi.fn().mockReturnValue({ readContract: vi.fn().mockResolvedValue([BigInt(1), false, false]), @@ -100,7 +106,7 @@ vi.mock("src/dataMappings/actions/eventAction", () => ({ }), })); -vi.mock("src/dataMappings/actions/fetchIpfsJsonAction", () => ({ +vi.mock("../src/dataMappings/actions/fetchIpfsJsonAction", () => ({ fetchIpfsJsonAction: vi.fn(async (mapping) => { return createResultObject( { @@ -136,6 +142,7 @@ describe("full flow test", () => { type: "abi/call", abi: "function currentRuling(uint256 _disputeID) public view returns (uint256 ruling, bool tied, bool overridden)", address: "0xA54e7A16d7460e38a8F324eF46782FB520d58CE8", + functionName: "currentRuling", args: ["0"], seek: ["0", "1", "2"], populate: ["ruling", "tied", "overridden"], @@ -145,7 +152,7 @@ describe("full flow test", () => { abi: "event Transfer(address indexed from, address indexed to, uint256 value)", address: "0xa8e4235129258404A2ed3D36DAd20708CcB2d0b7", eventFilter: { - fromBlock: "earliest", + fromBlock: "123", toBlock: "latest", args: [], }, @@ -174,6 +181,7 @@ describe("full flow test", () => { { title: "Yes", description: "User is responsible", id: "0x01" }, { title: "No", description: "User is not responsible", id: "0x02" }, ], + policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json", details: { ruling: "{{ruling}}", tied: "{{tied}}", @@ -182,6 +190,9 @@ describe("full flow test", () => { toAddress: "{{toAddress}}", transferValue: "{{transferValue}}", }, + arbitratorChainID: "421614", + arbitratorAddress: "0x0987654321098765432109876543210987654321", + version: "1.0", }); const initialContext = { alchemyApiKey: "mocked_api_key" }; @@ -199,6 +210,7 @@ describe("full flow test", () => { { title: "Yes", description: "User is responsible", id: "0x01" }, { title: "No", description: "User is not responsible", id: "0x02" }, ], + policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json", details: { ruling: "1", tied: "false", @@ -207,13 +219,16 @@ describe("full flow test", () => { toAddress: "0x0987654321098765432109876543210987654321", transferValue: "100", }, + arbitratorChainID: "421614", + arbitratorAddress: "0x0987654321098765432109876543210987654321", + version: "1.0", }); }); }); describe("jsonAction", () => { it("should extract and map data correctly", () => { - const mapping = { + const mapping: JsonMapping = { type: "json", value: exampleObject.evidence.fileURI, seek: ["photo", "video"], @@ -227,7 +242,7 @@ describe("jsonAction", () => { }); it("should handle empty JSON object gracefully", () => { - const mapping = { + const mapping: JsonMapping = { type: "json", value: {}, seek: ["nonexistentField"], @@ -240,7 +255,7 @@ describe("jsonAction", () => { describe("subgraphAction with variables", () => { it("should fetch GraphQL data with variables and return in expected format", async () => { - const mapping = { + const mapping: SubgraphMapping = { type: "graphql", endpoint: "mocked_endpoint", query: `query GetEscrows($buyer: Bytes!) { @@ -269,16 +284,17 @@ describe("subgraphAction with variables", () => { describe("callAction", () => { it("should call the contract and return in expected format", async () => { - const mapping = { + const mapping: AbiCallMapping = { type: "abi/call", abi: "function currentRuling(uint256 _disputeID) public view returns (uint256 ruling, bool tied, bool overridden)", + functionName: "currentRuling", address: "0xA54e7A16d7460e38a8F324eF46782FB520d58CE8", args: ["0"], seek: ["0", "1", "2"], populate: ["ruling", "tied", "overridden"], }; - const result = (await callAction(mapping, "")) as CallActionResult; + const result = (await callAction(mapping)) as CallActionResult; expect(result).to.have.property("ruling"); expect(result.ruling).to.be.a("bigint"); @@ -291,7 +307,7 @@ describe("callAction", () => { describe("eventAction", () => { it("should fetch event data and return populated data", async () => { - const mapping = { + const mapping: AbiEventMapping = { type: "abi/event", abi: "event Transfer(address indexed from, address indexed to, uint256 value)", address: "0xa8e4235129258404A2ed3D36DAd20708CcB2d0b7", @@ -304,7 +320,7 @@ describe("eventAction", () => { populate: ["fromAddress", "toAddress", "transferValue"], }; - const result = (await eventAction(mapping, "")) as EventActionResult; + const result = (await eventAction(mapping)) as EventActionResult; expect(result).to.have.property("fromAddress", "0x1234567890123456789012345678901234567890"); expect(result).to.have.property("toAddress", "0x0987654321098765432109876543210987654321"); @@ -314,7 +330,7 @@ describe("eventAction", () => { describe("fetchIpfsJsonAction", () => { it("should fetch JSON data from IPFS and return the expected result", async () => { - const mapping = { + const mapping: FetchIpfsJsonMapping = { type: "fetch/ipfs/json", ipfsUri: "/ipfs/QmQ2XoA25HmnPUEWDduxj6LYwMwp6jtXPFRMHcNF2EvJfU/file.json", seek: ["name", "firstName", "lastName", "anotherFile"], @@ -345,11 +361,8 @@ describe("populateTemplate", () => { reserved: false, }, ], - policyURI: "https://example.com/policy", - frontendUrl: "https://example.com", - arbitrableChainID: "100", - arbitrableAddress: "0x1234567890123456789012345678901234567890", - arbitratorChainID: "421613", + policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json", + arbitratorChainID: "421614", arbitratorAddress: "0x0987654321098765432109876543210987654321", category: "General", lang: "en_US", @@ -376,11 +389,8 @@ describe("populateTemplate", () => { reserved: false, }, ], - policyURI: "https://example.com/policy", - frontendUrl: "https://example.com", - arbitrableChainID: "100", - arbitrableAddress: "0x1234567890123456789012345678901234567890", - arbitratorChainID: "421613", + policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json", + arbitratorChainID: "421614", arbitratorAddress: "0x0987654321098765432109876543210987654321", category: "General", lang: "en_US", @@ -394,6 +404,22 @@ describe("populateTemplate", () => { title: "Test Title", description: "Test Description", question: "{{missingQuestion}}", + type: "single-select", + answers: [ + { + title: "Yes", + description: "Affirmative", + id: "0x01", + reserved: false, + }, + ], + policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json", + arbitratorChainID: "421614", + arbitratorAddress: "0x0987654321098765432109876543210987654321", + category: "General", + lang: "en_US", + specification: "Spec", + version: "1.0", }); const data = { @@ -406,6 +432,22 @@ describe("populateTemplate", () => { title: "Test Title", description: "Test Description", question: "", + type: "single-select", + answers: [ + { + title: "Yes", + description: "Affirmative", + id: "0x01", + reserved: false, + }, + ], + policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json", + arbitratorChainID: "421614", + arbitratorAddress: "0x0987654321098765432109876543210987654321", + category: "General", + lang: "en_US", + specification: "Spec", + version: "1.0", }); }); diff --git a/kleros-sdk/test/disputeDetailsSchema.test.ts b/kleros-sdk/test/disputeDetailsSchema.test.ts index ac2c7878a..d9f132e73 100644 --- a/kleros-sdk/test/disputeDetailsSchema.test.ts +++ b/kleros-sdk/test/disputeDetailsSchema.test.ts @@ -3,7 +3,7 @@ import { ethAddressSchema, ensNameSchema, ethAddressOrEnsNameSchema, -} from "src/dataMappings/utils/disputeDetailsSchema"; +} from "../src/dataMappings/utils/disputeDetailsSchema"; describe("Dispute Details Schema", () => { it("snapshot", () => { diff --git a/kleros-sdk/tsconfig.json b/kleros-sdk/tsconfig.json index 6a9b24bb5..8ce8e0686 100644 --- a/kleros-sdk/tsconfig.json +++ b/kleros-sdk/tsconfig.json @@ -1,43 +1,27 @@ { - "extends": "@kleros/kleros-v2-tsconfig/react-library.json", + "extends": "@kleros/kleros-v2-tsconfig/base20.json", "compilerOptions": { - "baseUrl": ".", - "paths": { - "~*": [ - "./*" - ], - "src*": [ - "./src*" - ], - "dataMappings*": [ - "./src/dataMappings*" - ] - }, - "target": "ES6", - "module": "CommonJS", - "outDir": "build/dist", + "outDir": "lib", "allowJs": true, "forceConsistentCasingInFileNames": true, - "strictNullChecks": true, - "noUnusedLocals": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, + "esModuleInterop": true, "removeComments": true, - "isolatedModules": true + "isolatedModules": true, + "noEmit": false, + "declaration": true }, "include": [ "src", "test" ], "exclude": [ - "node_modules", "build", - "scripts", - "acceptance-tests", - "webpack", - "jest", - "src/setupTests.ts", "dist", - "commitlint.config.js" + "lib", + "node_modules", + "scripts", + "webpack" ] } diff --git a/kleros-sdk/vitest.config.ts b/kleros-sdk/vitest.config.ts new file mode 100644 index 000000000..e87f6f23e --- /dev/null +++ b/kleros-sdk/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig, configDefaults } from "vitest/config"; + +export default defineConfig({ + test: { + exclude: [...configDefaults.exclude, "**/lib/**"], + }, +}); diff --git a/subgraph/core-neo/subgraph.yaml b/subgraph/core-neo/subgraph.yaml index e98508cc4..bfcbf3ad9 100644 --- a/subgraph/core-neo/subgraph.yaml +++ b/subgraph/core-neo/subgraph.yaml @@ -3,7 +3,6 @@ schema: file: ./schema.graphql features: - fullTextSearch - dataSources: - kind: ethereum name: KlerosCore diff --git a/subgraph/core-university/subgraph.yaml b/subgraph/core-university/subgraph.yaml index 03c1f6c72..bf28f408a 100644 --- a/subgraph/core-university/subgraph.yaml +++ b/subgraph/core-university/subgraph.yaml @@ -3,7 +3,6 @@ schema: file: ./schema.graphql features: - fullTextSearch - dataSources: - kind: ethereum name: KlerosCore diff --git a/subgraph/core/schema.graphql b/subgraph/core/schema.graphql index 3ed29d85f..0da8fa48a 100644 --- a/subgraph/core/schema.graphql +++ b/subgraph/core/schema.graphql @@ -174,6 +174,10 @@ type Dispute @entity { jurors: [User!]! @derivedFrom(field: "disputes") shifts: [TokenAndETHShift!]! @derivedFrom(field: "dispute") disputeKitDispute: [DisputeKitDispute!]! @derivedFrom(field: "coreDispute") + isCrossChain: Boolean + arbitrableChainId:BigInt + externalDisputeId:BigInt + templateId:BigInt } type PeriodIndexCounter @entity { diff --git a/subgraph/core/src/entities/Dispute.ts b/subgraph/core/src/entities/Dispute.ts index 5fe16c892..49bea55ad 100644 --- a/subgraph/core/src/entities/Dispute.ts +++ b/subgraph/core/src/entities/Dispute.ts @@ -1,3 +1,4 @@ +import { BigInt, ByteArray, crypto, dataSource, ethereum } from "@graphprotocol/graph-ts"; import { KlerosCore, DisputeCreation } from "../../generated/KlerosCore/KlerosCore"; import { Court, Dispute } from "../../generated/schema"; import { ZERO } from "../utils"; @@ -27,4 +28,68 @@ export function createDisputeFromEvent(event: DisputeCreation): void { const roundID = `${disputeID.toString()}-${ZERO.toString()}`; dispute.currentRound = roundID; dispute.save(); + + updateDisputeRequestData(event); +} + +// source: contracts/src/arbitration/interfaces/IArbitrableV2.sol +const DisputeRequest = "DisputeRequest(address,uint256,uint256,uint256,string)"; +const DisputeRequestSignature = crypto.keccak256(ByteArray.fromUTF8(DisputeRequest)); + +// note : we are using bytes32 in place of string as strings cannot be decoded and it breaks the function. +// It is okay for us, as we are only interested in the uint256 in frontend. +const DisputeRequestTypestring = "(uint256,uint256,bytes32)"; // _externalDisputeId,_templateId,_templateUri + +// source: contracts/src/gateway/interfaces/IHomeGateway.sol +const CrossChainDisputeIncoming = + "CrossChainDisputeIncoming(address,uint256,address,uint256,uint256,uint256,uint256,string)"; +const CrossChainDisputeIncomingSignature = crypto.keccak256(ByteArray.fromUTF8(CrossChainDisputeIncoming)); + +// note : arbitrable is an indexed arg, so it will topic[1] +const CrossChainDisputeIncomingTypestring = "(address,uint256,uint256,uint256,string)"; // arbitrator, _arbitrableChainId, _externalDisputeId, _templateId, _templateUri + +export const updateDisputeRequestData = (event: DisputeCreation): void => { + const dispute = Dispute.load(event.params._disputeID.toString()); + if (!dispute) return; + + const receipt = event.receipt; + if (!receipt) return; + + const logs = receipt.logs; + + // note that the topic at 0th index is always the event signature + const disputeRequestEventIndex = logs.findIndex((log) => log.topics[0] == DisputeRequestSignature); + const crossChainDisputeEventIndex = logs.findIndex((log) => log.topics[0] == CrossChainDisputeIncomingSignature); + + if (crossChainDisputeEventIndex !== -1) { + const crossChainDisputeEvent = logs[crossChainDisputeEventIndex]; + + const decoded = ethereum.decode(CrossChainDisputeIncomingTypestring, crossChainDisputeEvent.data); + if (!decoded) return; + dispute.isCrossChain = true; + dispute.arbitrableChainId = decoded.toTuple()[1].toBigInt(); + dispute.externalDisputeId = decoded.toTuple()[2].toBigInt(); + dispute.templateId = decoded.toTuple()[3].toBigInt(); + dispute.save(); + return; + } else if (disputeRequestEventIndex !== -1) { + const disputeRequestEvent = logs[disputeRequestEventIndex]; + + const decoded = ethereum.decode(DisputeRequestTypestring, disputeRequestEvent.data); + if (!decoded) return; + dispute.isCrossChain = false; + dispute.arbitrableChainId = getHomeChainId(dataSource.network()); + dispute.externalDisputeId = decoded.toTuple()[0].toBigInt(); + dispute.templateId = decoded.toTuple()[1].toBigInt(); + dispute.save(); + return; + } +}; + +// workaround, since hashmap don't work in subgraphs. +// https://thegraph.com/docs/en/developing/supported-networks/ +function getHomeChainId(name: string): BigInt { + if (name == "arbitrum-one") return BigInt.fromI32(42161); + else if (name == "arbitrum-sepolia") return BigInt.fromI32(421614); + else return BigInt.fromI32(1); } diff --git a/subgraph/core/subgraph.yaml b/subgraph/core/subgraph.yaml index f431077d9..ba08525c9 100644 --- a/subgraph/core/subgraph.yaml +++ b/subgraph/core/subgraph.yaml @@ -1,9 +1,8 @@ -specVersion: 0.0.4 +specVersion: 0.0.5 schema: file: ./schema.graphql features: - fullTextSearch - dataSources: - kind: ethereum name: KlerosCore @@ -14,7 +13,7 @@ dataSources: startBlock: 3638878 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - User @@ -39,6 +38,7 @@ dataSources: handler: handleAppealDecision - event: DisputeCreation(indexed uint256,indexed address) handler: handleDisputeCreation + receipt: true - event: Draw(indexed address,indexed uint256,uint256,uint256) handler: handleDraw - event: NewPeriod(indexed uint256,uint8) @@ -69,7 +69,7 @@ dataSources: startBlock: 3084568 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - Court @@ -89,7 +89,7 @@ dataSources: startBlock: 3638835 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - ClassicDispute @@ -124,7 +124,7 @@ dataSources: startBlock: 3638735 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - ClassicEvidenceGroup @@ -145,7 +145,7 @@ dataSources: startBlock: 3638850 mapping: kind: ethereum/events - apiVersion: 0.0.6 + apiVersion: 0.0.7 language: wasm/assemblyscript entities: - JurorTokensPerCourt diff --git a/subgraph/package.json b/subgraph/package.json index 0c89a01ea..f0c546309 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-subgraph", - "version": "0.7.6", + "version": "0.8.6", "license": "MIT", "scripts": { "update:core:arbitrum-sepolia-devnet": "./scripts/update.sh arbitrumSepoliaDevnet arbitrum-sepolia core/subgraph.yaml", diff --git a/tsconfig/base.json b/tsconfig/base.json deleted file mode 100644 index cbd15edc5..000000000 --- a/tsconfig/base.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "target": "es2021", - "module": "commonjs", - "moduleResolution": "node", - "outDir": "dist", - "strict": true, - "esModuleInterop": true, - "declaration": true, - "sourceMap": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": false, - "resolveJsonModule": true, - "experimentalDecorators": true - }, - "exclude": [ - "node_modules" - ] -} diff --git a/tsconfig/base18.json b/tsconfig/base18.json new file mode 100644 index 000000000..8436dd693 --- /dev/null +++ b/tsconfig/base18.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node18/tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "exclude": [ + "node_modules" + ] +} diff --git a/tsconfig/base20.json b/tsconfig/base20.json new file mode 100644 index 000000000..45fcb964c --- /dev/null +++ b/tsconfig/base20.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "exclude": [ + "node_modules" + ] +} diff --git a/tsconfig/package.json b/tsconfig/package.json index e07f71c30..47582628d 100644 --- a/tsconfig/package.json +++ b/tsconfig/package.json @@ -3,9 +3,12 @@ "version": "1.0.0", "private": true, "files": [ - "base.json" + "base18.json", + "base20.json" ], "devDependencies": { - "@kleros/kleros-v2-eslint-config": "*" + "@kleros/kleros-v2-eslint-config": "*", + "@tsconfig/node18": "^18.2.4", + "@tsconfig/node20": "^20.1.4" } } diff --git a/tsconfig/react-library.json b/tsconfig/react-library.json index 3f69cb2c3..81ef5ffa7 100644 --- a/tsconfig/react-library.json +++ b/tsconfig/react-library.json @@ -1,14 +1,13 @@ { - "$schema": "https://json.schemastore.org/tsconfig", "display": "React Library", - "extends": "./base.json", + "extends": "./base20.json", "compilerOptions": { "jsx": "react", "lib": [ "es6", "dom", "esnext.asynciterable", - "es2017" + "es2023" ], "module": "ESNext", "target": "es6" diff --git a/web/.env.devnet-university.public b/web/.env.devnet-university.public index 1e167ec4b..2e65b57cd 100644 --- a/web/.env.devnet-university.public +++ b/web/.env.devnet-university.public @@ -9,3 +9,6 @@ export REACT_APP_ATLAS_URI=http://localhost:3000 export WALLETCONNECT_PROJECT_ID= export ALCHEMY_API_KEY= export NODE_OPTIONS='--max-old-space-size=7680' + +# devtools +export REACT_APP_GRAPH_API_KEY= \ No newline at end of file diff --git a/web/.env.devnet.public b/web/.env.devnet.public index 6422335a8..fcd51045e 100644 --- a/web/.env.devnet.public +++ b/web/.env.devnet.public @@ -6,6 +6,6 @@ export REACT_APP_STATUS_URL=https://kleros-v2-devnet.betteruptime.com/badge export REACT_APP_GENESIS_BLOCK_ARBSEPOLIA=3084598 export REACT_APP_ATLAS_URI=http://localhost:3000 export REACT_APP_DEVTOOLS_URL=https://dev--kleros-v2-testnet-devtools.netlify.app -export WALLETCONNECT_PROJECT_ID= -export ALCHEMY_API_KEY= -export NODE_OPTIONS='--max-old-space-size=7680' \ No newline at end of file +export NODE_OPTIONS='--max-old-space-size=7680' +# devtools +export REACT_APP_GRAPH_API_KEY= diff --git a/web/.env.local.public b/web/.env.local.public index b5a5cdd43..a41935879 100644 --- a/web/.env.local.public +++ b/web/.env.local.public @@ -6,3 +6,6 @@ export REACT_APP_ATLAS_URI=http://localhost:3000 export WALLETCONNECT_PROJECT_ID= export ALCHEMY_API_KEY= export NODE_OPTIONS='--max-old-space-size=7680' + +# devtools +export REACT_APP_GRAPH_API_KEY= \ No newline at end of file diff --git a/web/.env.testnet.public b/web/.env.testnet.public index b6bd4e057..e91ba5ea6 100644 --- a/web/.env.testnet.public +++ b/web/.env.testnet.public @@ -8,4 +8,6 @@ export REACT_APP_GENESIS_BLOCK_ARBSEPOLIA=3842783 export REACT_APP_DEVTOOLS_URL=https://devtools.v2-testnet.kleros.builders export WALLETCONNECT_PROJECT_ID= export ALCHEMY_API_KEY= -export NODE_OPTIONS='--max-old-space-size=7680' \ No newline at end of file +export NODE_OPTIONS='--max-old-space-size=7680' +# devtools +export REACT_APP_GRAPH_API_KEY= \ No newline at end of file diff --git a/web/src/context/Web3Provider.tsx b/web/src/context/Web3Provider.tsx index acb3f0784..342ac5ed9 100644 --- a/web/src/context/Web3Provider.tsx +++ b/web/src/context/Web3Provider.tsx @@ -6,6 +6,8 @@ import { createConfig, fallback, http, WagmiProvider, webSocket } from "wagmi"; import { mainnet, arbitrumSepolia, arbitrum, gnosisChiado, gnosis, sepolia } from "wagmi/chains"; import { walletConnect } from "wagmi/connectors"; +import { configureSDK } from "@kleros/kleros-sdk/src/sdk"; + import { ALL_CHAINS, DEFAULT_CHAIN } from "consts/chains"; import { isProductionDeployment } from "consts/index"; @@ -62,6 +64,13 @@ const wagmiConfig = createConfig({ connectors: [walletConnect({ projectId, showQrModal: false })], }); +configureSDK({ + client: { + chain: isProduction ? arbitrum : arbitrumSepolia, + transport: transports[isProduction ? arbitrum.id : arbitrumSepolia.id], + }, +}); + createWeb3Modal({ wagmiConfig, projectId, diff --git a/web/src/hooks/queries/useDisputeDetailsQuery.ts b/web/src/hooks/queries/useDisputeDetailsQuery.ts index 4fd0e5029..c5b359d08 100644 --- a/web/src/hooks/queries/useDisputeDetailsQuery.ts +++ b/web/src/hooks/queries/useDisputeDetailsQuery.ts @@ -30,6 +30,10 @@ const disputeDetailsQuery = graphql(` nbVotes } currentRoundIndex + isCrossChain + arbitrableChainId + externalDisputeId + templateId } } `); diff --git a/web/src/hooks/queries/useEvidenceGroup.ts b/web/src/hooks/queries/useEvidenceGroup.ts deleted file mode 100644 index c4ce9f401..000000000 --- a/web/src/hooks/queries/useEvidenceGroup.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getContract } from "viem"; -import { usePublicClient } from "wagmi"; - -import { GENESIS_BLOCK_ARBSEPOLIA } from "consts/index"; -import { iArbitrableV2Abi } from "hooks/contracts/generated"; -import { isUndefined } from "utils/index"; - -export const useEvidenceGroup = (disputeID?: string, arbitrableAddress?: `0x${string}`) => { - const isEnabled = !isUndefined(arbitrableAddress); - const publicClient = usePublicClient(); - return useQuery({ - queryKey: [`EvidenceGroup${disputeID}${arbitrableAddress}`], - enabled: isEnabled, - staleTime: Infinity, - queryFn: async () => { - if (arbitrableAddress && !isUndefined(disputeID)) { - const arbitrable = getContract({ - abi: iArbitrableV2Abi, - address: arbitrableAddress, - client: { public: publicClient }, - }); - const disputeFilter = await arbitrable.createEventFilter.DisputeRequest( - { - _arbitratorDisputeID: BigInt(disputeID), - }, - { - fromBlock: GENESIS_BLOCK_ARBSEPOLIA, - toBlock: "latest", - } - ); - - const disputeEvents = await publicClient.getFilterLogs({ - filter: disputeFilter, - }); - - return disputeEvents[0].args._externalDisputeID; - } else throw Error; - }, - }); -}; diff --git a/web/src/hooks/queries/usePopulatedDisputeData.ts b/web/src/hooks/queries/usePopulatedDisputeData.ts index 75c62f8ea..8175d6354 100644 --- a/web/src/hooks/queries/usePopulatedDisputeData.ts +++ b/web/src/hooks/queries/usePopulatedDisputeData.ts @@ -1,22 +1,17 @@ import { useQuery } from "@tanstack/react-query"; -import { getContract, HttpRequestError, PublicClient, RpcError } from "viem"; -import { usePublicClient } from "wagmi"; +import { HttpRequestError, RpcError } from "viem"; import { executeActions } from "@kleros/kleros-sdk/src/dataMappings/executeActions"; import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; import { populateTemplate } from "@kleros/kleros-sdk/src/dataMappings/utils/populateTemplate"; -import { GENESIS_BLOCK_ARBSEPOLIA } from "consts/index"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; -import { iArbitrableV2Abi } from "hooks/contracts/generated"; -import { useEvidenceGroup } from "queries/useEvidenceGroup"; import { debounceErrorToast } from "utils/debounceErrorToast"; import { isUndefined } from "utils/index"; -import { DEFAULT_CHAIN } from "consts/chains"; import { graphql } from "src/graphql"; -import { useIsCrossChainDispute } from "../useIsCrossChainDispute"; +import { useDisputeDetailsQuery } from "./useDisputeDetailsQuery"; const disputeTemplateQuery = graphql(` query DisputeTemplate($id: ID!) { @@ -30,33 +25,27 @@ const disputeTemplateQuery = graphql(` `); export const usePopulatedDisputeData = (disputeID?: string, arbitrableAddress?: `0x${string}`) => { - const publicClient = usePublicClient(); - const { data: crossChainData, isError } = useIsCrossChainDispute(disputeID, arbitrableAddress); + const { data: disputeData } = useDisputeDetailsQuery(disputeID); const { graphqlBatcher } = useGraphqlBatcher(); - const { data: externalDisputeID } = useEvidenceGroup(disputeID, arbitrableAddress); const isEnabled = !isUndefined(disputeID) && - !isUndefined(crossChainData) && - !isUndefined(arbitrableAddress) && - !isUndefined(externalDisputeID); + !isUndefined(disputeData) && + !isUndefined(disputeData?.dispute) && + !isUndefined(disputeData.dispute?.arbitrableChainId) && + !isUndefined(disputeData.dispute?.externalDisputeId) && + !isUndefined(disputeData.dispute?.templateId); return useQuery({ - queryKey: [`DisputeTemplate${disputeID}${arbitrableAddress}${externalDisputeID}`], + queryKey: [`DisputeTemplate${disputeID}${arbitrableAddress}${disputeData?.dispute?.externalDisputeId}`], enabled: isEnabled, staleTime: Infinity, queryFn: async () => { - if (isEnabled && !isError) { + if (isEnabled) { try { - const { isCrossChainDispute, crossChainTemplateId } = crossChainData; - - const templateId = isCrossChainDispute - ? crossChainTemplateId - : await getTemplateId(arbitrableAddress, disputeID, publicClient); - const { disputeTemplate } = await graphqlBatcher.fetch({ id: crypto.randomUUID(), document: disputeTemplateQuery, - variables: { id: templateId.toString() }, + variables: { id: disputeData.dispute?.templateId.toString() }, isDisputeTemplate: true, }); @@ -66,10 +55,10 @@ export const usePopulatedDisputeData = (disputeID?: string, arbitrableAddress?: const initialContext = { disputeID: disputeID, arbitrableAddress: arbitrableAddress, - arbitrableChainID: isCrossChainDispute ? crossChainData.crossChainId.toString() : DEFAULT_CHAIN.toString(), + arbitrableChainID: disputeData.dispute?.arbitrableChainId, graphApiKey: import.meta.env.REACT_APP_GRAPH_API_KEY, alchemyApiKey: import.meta.env.ALCHEMY_API_KEY, - externalDisputeID: externalDisputeID, + externalDisputeID: disputeData.dispute?.externalDisputeId, }; const data = dataMappings ? await executeActions(JSON.parse(dataMappings), initialContext) : {}; @@ -88,28 +77,3 @@ export const usePopulatedDisputeData = (disputeID?: string, arbitrableAddress?: }, }); }; - -const getTemplateId = async ( - arbitrableAddress: `0x${string}`, - disputeID: string, - publicClient: PublicClient -): Promise => { - const arbitrable = getContract({ - abi: iArbitrableV2Abi, - address: arbitrableAddress, - client: { public: publicClient }, - }); - const disputeFilter = await arbitrable.createEventFilter.DisputeRequest( - { - _arbitratorDisputeID: BigInt(disputeID), - }, - { - fromBlock: GENESIS_BLOCK_ARBSEPOLIA, - toBlock: "latest", - } - ); - const disputeEvents = await publicClient.getFilterLogs({ - filter: disputeFilter, - }); - return disputeEvents[0].args._templateId ?? 0n; -}; diff --git a/web/src/hooks/useIsCrossChainDispute.ts b/web/src/hooks/useIsCrossChainDispute.ts deleted file mode 100644 index 61540a080..000000000 --- a/web/src/hooks/useIsCrossChainDispute.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { HttpRequestError, RpcError, getContract } from "viem"; -import { usePublicClient } from "wagmi"; - -import { GENESIS_BLOCK_ARBSEPOLIA } from "consts/index"; -import { iHomeGatewayAbi } from "hooks/contracts/generated"; -import { debounceErrorToast } from "utils/debounceErrorToast"; -import { isUndefined } from "utils/index"; - -interface IIsCrossChainDispute { - isCrossChainDispute: boolean; - crossChainId: bigint; - crossChainTemplateId: bigint; - crossChainArbitrableAddress: `0x${string}`; -} - -export const useIsCrossChainDispute = (disputeID?: string, arbitrableAddress?: `0x${string}`) => { - const isEnabled = !isUndefined(arbitrableAddress) && !isUndefined(disputeID); - const publicClient = usePublicClient(); - return useQuery({ - queryKey: [`IsCrossChainDispute${disputeID}`], - enabled: isEnabled, - staleTime: Infinity, - queryFn: async () => { - if (isEnabled) { - try { - const arbitrable = getContract({ - abi: iHomeGatewayAbi, - address: arbitrableAddress, - client: { public: publicClient }, - }); - const crossChainDisputeFilter = await arbitrable.createEventFilter.CrossChainDisputeIncoming( - { - _arbitratorDisputeID: BigInt(disputeID), - }, - { - fromBlock: GENESIS_BLOCK_ARBSEPOLIA, - toBlock: "latest", - } - ); - const crossChainDisputeEvents = await publicClient.getFilterLogs({ - filter: crossChainDisputeFilter, - }); - - if (crossChainDisputeEvents.length > 0) { - return { - isCrossChainDispute: true, - crossChainId: crossChainDisputeEvents[0].args._arbitrableChainId ?? 0n, - crossChainTemplateId: crossChainDisputeEvents[0].args._templateId ?? 0n, - crossChainArbitrableAddress: crossChainDisputeEvents[0].args._arbitrable ?? "0x", - }; - } else { - return { - isCrossChainDispute: false, - crossChainId: 0n, - crossChainTemplateId: 0n, - crossChainArbitrableAddress: "0x", - }; - } - } catch (error) { - if (error instanceof HttpRequestError || error instanceof RpcError) { - debounceErrorToast("RPC failed!, Please avoid voting."); - } - throw Error; - } - } else throw Error; - }, - }); -}; diff --git a/web/src/pages/Cases/CaseDetails/Evidence/index.tsx b/web/src/pages/Cases/CaseDetails/Evidence/index.tsx index 8c088edfc..361936cd4 100644 --- a/web/src/pages/Cases/CaseDetails/Evidence/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Evidence/index.tsx @@ -8,7 +8,7 @@ import { Button } from "@kleros/ui-components-library"; import DownArrow from "svgs/icons/arrow-down.svg"; -import { useEvidenceGroup } from "queries/useEvidenceGroup"; +import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery"; import { useEvidences } from "queries/useEvidences"; import { responsiveSize } from "styles/responsiveSize"; @@ -54,14 +54,14 @@ const ScrollButton = styled(Button)` } `; -const Evidence: React.FC<{ arbitrable?: `0x${string}` }> = ({ arbitrable }) => { +const Evidence: React.FC = () => { const { id } = useParams(); - const { data: evidenceGroup } = useEvidenceGroup(id, arbitrable); + const { data: disputeData } = useDisputeDetailsQuery(id); const ref = useRef(null); const [search, setSearch] = useState(); const [debouncedSearch, setDebouncedSearch] = useState(); - const { data } = useEvidences(evidenceGroup?.toString(), debouncedSearch); + const { data } = useEvidences(disputeData?.dispute?.externalDisputeId?.toString(), debouncedSearch); useDebounce(() => setDebouncedSearch(search), 500, [search]); @@ -76,7 +76,7 @@ const Evidence: React.FC<{ arbitrable?: `0x${string}` }> = ({ arbitrable }) => { return ( - + {data ? ( data.evidences.map(({ evidence, sender, timestamp, name, description, fileURI, evidenceIndex }) => ( diff --git a/web/src/pages/Cases/CaseDetails/index.tsx b/web/src/pages/Cases/CaseDetails/index.tsx index 3f624c000..aa4e54047 100644 --- a/web/src/pages/Cases/CaseDetails/index.tsx +++ b/web/src/pages/Cases/CaseDetails/index.tsx @@ -64,7 +64,7 @@ const CaseDetails: React.FC = () => { } /> - } /> + } /> } /> } /> } /> diff --git a/web/tsconfig.json b/web/tsconfig.json index 0f569d03f..fae4af75b 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -46,7 +46,6 @@ "./src/assets/svgs*" ] }, - "target": "es2020", "rootDir": "src", "outDir": "build/dist", "allowJs": true, @@ -58,6 +57,14 @@ "allowSyntheticDefaultImports": true, "removeComments": true, "isolatedModules": true, + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": false, + "resolveJsonModule": true, "lib": [ "ESNext.Array" ], diff --git a/yarn.lock b/yarn.lock index a3f392624..1d7fc73bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,18 @@ __metadata: version: 8 cacheKey: 10 +"@0no-co/graphql.web@npm:^1.0.5": + version: 1.0.9 + resolution: "@0no-co/graphql.web@npm:1.0.9" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + graphql: + optional: true + checksum: 3caf39263323ba2a51255035e92582573822d213117879e6d1b732e222a8d14fdc703828d46b4a46f86d2027ee08568196f1d64376ee74250fb58dc465698930 + languageName: node + linkType: hard + "@aashutoshrathi/word-wrap@npm:^1.2.3": version: 1.2.6 resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" @@ -7899,14 +7911,16 @@ __metadata: version: 0.0.0-use.local resolution: "@kleros/kleros-sdk@workspace:kleros-sdk" dependencies: - "@kleros/kleros-v2-contracts": "workspace:^" "@reality.eth/reality-eth-lib": "npm:^3.2.30" "@types/mustache": "npm:^4.2.5" + "@urql/core": "npm:^5.0.8" "@vitest/ui": "npm:^1.1.3" mocha: "npm:^10.2.0" mustache: "npm:^4.2.0" + rimraf: "npm:^6.0.1" ts-node: "npm:^10.9.2" typescript: "npm:^5.3.3" + viem: "npm:^2.21.26" vitest: "npm:^1.1.3" zod: "npm:^3.22.4" languageName: unknown @@ -7920,7 +7934,7 @@ __metadata: languageName: unknown linkType: soft -"@kleros/kleros-v2-contracts@workspace:^, @kleros/kleros-v2-contracts@workspace:contracts": +"@kleros/kleros-v2-contracts@workspace:contracts": version: 0.0.0-use.local resolution: "@kleros/kleros-v2-contracts@workspace:contracts" dependencies: @@ -7965,6 +7979,7 @@ __metadata: ts-node: "npm:^10.9.2" typechain: "npm:^8.3.2" typescript: "npm:^5.3.3" + viem: "npm:^2.21.26" languageName: unknown linkType: soft @@ -8020,6 +8035,8 @@ __metadata: resolution: "@kleros/kleros-v2-tsconfig@workspace:tsconfig" dependencies: "@kleros/kleros-v2-eslint-config": "npm:*" + "@tsconfig/node18": "npm:^18.2.4" + "@tsconfig/node20": "npm:^20.1.4" languageName: unknown linkType: soft @@ -9464,7 +9481,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.6.0, @noble/curves@npm:~1.6.0": +"@noble/curves@npm:1.6.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:~1.6.0": version: 1.6.0 resolution: "@noble/curves@npm:1.6.0" dependencies: @@ -9473,7 +9490,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:^1.1.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.4.0": +"@noble/curves@npm:^1.1.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.4.0": version: 1.4.2 resolution: "@noble/curves@npm:1.4.2" dependencies: @@ -12145,6 +12162,20 @@ __metadata: languageName: node linkType: hard +"@tsconfig/node18@npm:^18.2.4": + version: 18.2.4 + resolution: "@tsconfig/node18@npm:18.2.4" + checksum: 80623cb9c129c78d51fe6c4a256ba986f12f02ff02dc2a1e5b33dd13a7983f767b6792cfcd51b3dd1c8256ea105f1fea31f64a2070564e37787ab3d9a1a1e7e3 + languageName: node + linkType: hard + +"@tsconfig/node20@npm:^20.1.4": + version: 20.1.4 + resolution: "@tsconfig/node20@npm:20.1.4" + checksum: 345dba8074647f6c11b8d78afa76d9c16e3436cb56a8e78fe2060014d33a09f3f4fd6ed81dc90e955d3509f926cd7fd61c6ddfd3d5a1d80758d7844f7cc3a99e + languageName: node + linkType: hard + "@typechain/ethers-v5@npm:^11.1.2": version: 11.1.2 resolution: "@typechain/ethers-v5@npm:11.1.2" @@ -13336,6 +13367,16 @@ __metadata: languageName: node linkType: hard +"@urql/core@npm:^5.0.8": + version: 5.0.8 + resolution: "@urql/core@npm:5.0.8" + dependencies: + "@0no-co/graphql.web": "npm:^1.0.5" + wonka: "npm:^6.3.2" + checksum: c973e6e89785ae45ef447726557143ce7bc9d9f5b887297f0b315b2ff546d20bdfb814a4c899644bd5c5814761fc8d75a8ac66f67f3d57a3c2eadd3ec88adb60 + languageName: node + linkType: hard + "@vitest/expect@npm:1.2.1": version: 1.2.1 resolution: "@vitest/expect@npm:1.2.1" @@ -40209,6 +40250,28 @@ __metadata: languageName: node linkType: hard +"viem@npm:^2.21.26": + version: 2.21.26 + resolution: "viem@npm:2.21.26" + dependencies: + "@adraffy/ens-normalize": "npm:1.11.0" + "@noble/curves": "npm:1.6.0" + "@noble/hashes": "npm:1.5.0" + "@scure/bip32": "npm:1.5.0" + "@scure/bip39": "npm:1.4.0" + abitype: "npm:1.0.6" + isows: "npm:1.0.6" + webauthn-p256: "npm:0.0.10" + ws: "npm:8.18.0" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 5233c9b34207c71b24a2d47abeefc9bf36b48f73b1601b2e4dfd5b71425f32902b11f85c14b00fd8b7f122469f584b21a49dc2f70a3068315182b487a42b31eb + languageName: node + linkType: hard + "vite-node@npm:1.2.1": version: 1.2.1 resolution: "vite-node@npm:1.2.1" @@ -41199,6 +41262,13 @@ __metadata: languageName: node linkType: hard +"wonka@npm:^6.3.2": + version: 6.3.4 + resolution: "wonka@npm:6.3.4" + checksum: 0f102630182828268b57b54102003449b97abbc2483392239baf856a2fca7b72ae9be67c208415124a3d26a320674ed64387e9bf07a8d0badedb5f607d2ccfdc + languageName: node + linkType: hard + "word-wrap@npm:^1.2.3, word-wrap@npm:~1.2.3": version: 1.2.3 resolution: "word-wrap@npm:1.2.3"