Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions bots/dispute-archival-bot/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# subgraphs for the archived core contract
export CORE_SUBGRAPH_URL=""
export DTR_SUBGRAPH_URL=""

export IPFS_UPLOAD_URL=""

# required by data mappings
export GRAPH_API_KEY=""

# address of dispute archive contract
export DISPUTE_ARCHIVE_ADDRESS=""

export ALCHEMY_API_KEY=""
export PRIVATE_KEY=""
2 changes: 2 additions & 0 deletions bots/dispute-archival-bot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
!.env.example
24 changes: 24 additions & 0 deletions bots/dispute-archival-bot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# dispute-archival-bot

Archives disputes from an archived KlerosCore contract.

DisputeArchive Contract : [arbitrum sepolia](https://sepolia.arbiscan.io/address/0x13713cf6D261704A07aB8a44d88CE5d795fdF99d) (For testing, archived the mainnet beta disputes on arbitrum sepolia)

### Archived data includes:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix heading hierarchy (MD001) to avoid markdownlint warnings.

Line 7 jumps from # to ###; the section headings should be ## under the top title.

📘 Suggested markdown fix
-### Archived data includes:
+## Archived data includes:
@@
-### Notes:
+## Notes:
@@
-### Pending
+## Pending

Also applies to: 14-14, 21-21

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 7-7: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/dispute-archival-bot/README.md` at line 7, The README has incorrect
heading levels: change the three subordinate headings currently written as "###
Archived data includes:", and the other two similar headings at the same file,
to "##" so they are direct subsections under the top-level title; update each
instance of "###" to "##" for those headings to satisfy MD001 and maintain
proper hierarchy.


1. Populated dispute data from `kleros-sdk`
2. Evidences for the disputes (Un-filtered)
3. Dispute details from Subgraph. Subgraph here is the one that indexed the archived KlerosCore
4. Templates for the dispute. These are saved in case the dispute is broken, in which case populated dispute data will be never. Acts as a recovery.

### Notes:

1. Bot does not constantly monitor the archive core contract, it runs one time and archives any non archived disputes.
2. The subgraph query is limited at 1000 entities, for now beta will likely not cross 1000 disputes and 1000 evidences per dispute and 1000 draws per dispute.

:warning: Subgraph schema was changed between Neo/beta Kleros Core and the new stable versions. This bot queries the older subgraph with old schema for mainnet beta.

### Pending

1. Logic does not check for ongoing disputes yet, it archives regardless. During stable release, the disputes will all be closed, so should not be an issue.
2. Subgraph support pending
168 changes: 168 additions & 0 deletions bots/dispute-archival-bot/abi/DisputeArchive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
export const disputeArchiveAbi = [
{
inputs: [],
stateMutability: "nonpayable",
type: "constructor",
},
{
inputs: [],
name: "CIDCannotBeEmpty",
type: "error",
},
{
inputs: [],
name: "DisputeAlreadyArchived",
type: "error",
},
{
inputs: [],
name: "DisputeNotInArchive",
type: "error",
},
{
inputs: [],
name: "OnlyOwner",
type: "error",
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "uint256",
name: "id",
type: "uint256",
},
{
indexed: false,
internalType: "uint96",
name: "courtId",
type: "uint96",
},
{
indexed: false,
internalType: "string",
name: "cid",
type: "string",
},
],
name: "ArchivedDispute",
type: "event",
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "uint256",
name: "id",
type: "uint256",
},
{
indexed: false,
internalType: "string",
name: "cid",
type: "string",
},
{
indexed: false,
internalType: "string",
name: "reason",
type: "string",
},
],
name: "ArchivedDisputeAmended",
type: "event",
},
{
inputs: [
{
internalType: "uint256",
name: "id",
type: "uint256",
},
{
internalType: "string",
name: "cid",
type: "string",
},
{
internalType: "string",
name: "reason",
type: "string",
},
],
name: "amend",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
name: "archivedDisputeToCid",
outputs: [
{
internalType: "string",
name: "",
type: "string",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "owner",
outputs: [
{
internalType: "address",
name: "",
type: "address",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "uint256",
name: "id",
type: "uint256",
},
{
internalType: "uint96",
name: "courtId",
type: "uint96",
},
{
internalType: "string",
name: "cid",
type: "string",
},
],
name: "register",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "newOwner",
type: "address",
},
],
name: "updateOwner",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
] as const;
46 changes: 46 additions & 0 deletions bots/dispute-archival-bot/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const variableKeysMap = {
coreSubgraphUrl: "CORE_SUBGRAPH_URL",
dtrSubgraphUrl: "DTR_SUBGRAPH_URL",
graphApiKey: "GRAPH_API_KEY",
ipfsUploadUrl: "IPFS_UPLOAD_URL",
disputeArchiveAddress: "DISPUTE_ARCHIVE_ADDRESS",
alchemyApiKey: "ALCHEMY_API_KEY",
privateKey: "PRIVATE_KEY",
} as const;

type VariableKey = keyof typeof variableKeysMap;

type EnvConfig = Record<VariableKey, string>;

export const getEnvConfig = (): EnvConfig => {
const config: EnvConfig = {
coreSubgraphUrl: "",
dtrSubgraphUrl: "",
graphApiKey: "",
ipfsUploadUrl: "",
disputeArchiveAddress: "",
alchemyApiKey: "",
privateKey: "",
};

for (const key of Object.keys(variableKeysMap) as VariableKey[]) {
const envKey = variableKeysMap[key];
const value = process.env[envKey];

if (!value || value.trim() === "") throw new EnvVariableNotConfiguredError(envKey);

config[key] = value;
}

return config;
};

class EnvVariableNotConfiguredError extends Error {
constructor(variableKey: string) {
super(`${variableKey} not configured!`);

Object.setPrototypeOf(this, EnvVariableNotConfiguredError.prototype);

this.name = "EnvVariableNotConfiguredError";
}
}
37 changes: 37 additions & 0 deletions bots/dispute-archival-bot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import "dotenv/config";
import { uploadToIpfs } from "./utils/uploadToIpfs.ts";
import { fetchDisputeArchiveSnapshot } from "./utils/fetchDisputeArchiveSnapshot.ts";
import { fetchDisputeIds } from "./utils/fetchDisputeIds.ts";
import { isDisputeArchived, registerCid } from "./utils/contract.ts";

async function main() {
console.log("Fetching dispute IDs...");
const disputeIdsUnsorted = await fetchDisputeIds();

console.log(`Fetched ${disputeIdsUnsorted.length} ids`);

const disputeIds = disputeIdsUnsorted.sort((a, b) => Number(a) - Number(b));

Check warning on line 13 in bots/dispute-archival-bot/index.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this array "sort" operation to a separate statement or replace it with "toSorted".

See more on https://sonarcloud.io/project/issues?id=kleros_kleros-v2&issues=AZ4IVt9bvj42Jf6yqctO&open=AZ4IVt9bvj42Jf6yqctO&pullRequest=2280

// skip Ids already archived
for (const id of disputeIds) {
const isArchived = await isDisputeArchived(id);
if (isArchived) {
console.log(`Skipping dispute ${id}. Already archived.`);
continue;
}

console.log(`Archiving dispute ${id} ...`);

const snapshot = await fetchDisputeArchiveSnapshot(BigInt(id));

const cid = await uploadToIpfs(id, snapshot);

const courtID = snapshot.dispute.court.id;

const hash = await registerCid(id, courtID, cid);

console.log(`Dispute ${id} archived with cid: ${cid}. Transaction hash: ${hash}`);
}
Comment on lines +16 to +34
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Isolate per-dispute failures so one error doesn’t stop the whole run.

A single thrown error currently aborts processing of all subsequent disputes.

🛠️ Suggested loop hardening
   for (const id of disputeIds) {
-    const isArchived = await isDisputeArchived(id);
-    if (isArchived) {
-      console.log(`Skipping dispute ${id}. Already archived.`);
-      continue;
-    }
-
-    console.log(`Archiving dispute ${id} ...`);
-
-    const snapshot = await fetchDisputeArchiveSnapshot(BigInt(id));
-
-    const cid = await uploadToIpfs(id, snapshot);
-
-    const hash = await registerCid(id, cid);
-
-    console.log(`Dispute ${id} archived with cid: ${cid}. Transaction hash: ${hash}`);
+    try {
+      const isArchived = await isDisputeArchived(id);
+      if (isArchived) {
+        console.log(`Skipping dispute ${id}. Already archived.`);
+        continue;
+      }
+
+      console.log(`Archiving dispute ${id} ...`);
+      const snapshot = await fetchDisputeArchiveSnapshot(BigInt(id));
+      const cid = await uploadToIpfs(id, snapshot);
+      const hash = await registerCid(id, cid);
+      console.log(`Dispute ${id} archived with cid: ${cid}. Transaction hash: ${hash}`);
+    } catch (err) {
+      console.error(`Failed to archive dispute ${id}:`, err);
+      continue;
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const id of disputeIds) {
const isArchived = await isDisputeArchived(id);
if (isArchived) {
console.log(`Skipping dispute ${id}. Already archived.`);
continue;
}
console.log(`Archiving dispute ${id} ...`);
const snapshot = await fetchDisputeArchiveSnapshot(BigInt(id));
const cid = await uploadToIpfs(id, snapshot);
const hash = await registerCid(id, cid);
console.log(`Dispute ${id} archived with cid: ${cid}. Transaction hash: ${hash}`);
}
for (const id of disputeIds) {
try {
const isArchived = await isDisputeArchived(id);
if (isArchived) {
console.log(`Skipping dispute ${id}. Already archived.`);
continue;
}
console.log(`Archiving dispute ${id} ...`);
const snapshot = await fetchDisputeArchiveSnapshot(BigInt(id));
const cid = await uploadToIpfs(id, snapshot);
const hash = await registerCid(id, cid);
console.log(`Dispute ${id} archived with cid: ${cid}. Transaction hash: ${hash}`);
} catch (err) {
console.error(`Failed to archive dispute ${id}:`, err);
continue;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/dispute-archival-bot/index.ts` around lines 16 - 32, The loop over
disputeIds should be hardened so a failure for one dispute doesn't stop the
whole run: wrap the per-dispute workflow inside a try/catch around the calls to
isDisputeArchived(id), fetchDisputeArchiveSnapshot(BigInt(id)), uploadToIpfs(id,
snapshot), and registerCid(id, cid) (i.e., the body of the for (const id of
disputeIds) loop), log a clear error message including the id and the caught
error, and continue to the next id in the catch block; keep the existing success
logs for archived/skipped and completed cases.

}

main();

Check warning on line 37 in bots/dispute-archival-bot/index.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer top-level await over an async function `main` call.

See more on https://sonarcloud.io/project/issues?id=kleros_kleros-v2&issues=AZ4IVt9bvj42Jf6yqctP&open=AZ4IVt9bvj42Jf6yqctP&pullRequest=2280
17 changes: 17 additions & 0 deletions bots/dispute-archival-bot/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "dispute-archival-bot",
"packageManager": "yarn@4.10.3",
"type": "module",
"scripts": {
"start": "node index.ts"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

BOT_PKG=$(fd -a '^package\.json$' bots/dispute-archival-bot | head -n1)
BOT_TSCONFIG=$(fd -a '^tsconfig\.json$' bots/dispute-archival-bot | head -n1)

echo "start script:"
jq -r '.scripts.start' "$BOT_PKG"

echo "ts runtime helpers:"
jq -r '.dependencies.tsx // .devDependencies.tsx // "tsx:not-declared"' "$BOT_PKG"
jq -r '.dependencies["ts-node"] // .devDependencies["ts-node"] // "ts-node:not-declared"' "$BOT_PKG"

echo "emitDeclarationOnly:"
jq -r '.compilerOptions.emitDeclarationOnly' "$BOT_TSCONFIG"

Repository: kleros/kleros-v2

Length of output: 172


🏁 Script executed:

# Check the tsconfig.json configuration
cat bots/dispute-archival-bot/tsconfig.json

Repository: kleros/kleros-v2

Length of output: 338


🏁 Script executed:

# Check if index.ts exists and examine it
ls -la bots/dispute-archival-bot/index.ts 2>&1 || echo "index.ts not found"

# Also check for any compiled versions (index.js, dist/, build/, etc.)
find bots/dispute-archival-bot -maxdepth 2 -type f \( -name "index.js" -o -name "index.mjs" \) 2>&1
find bots/dispute-archival-bot -maxdepth 2 -type d \( -name "dist" -o -name "build" -o -name "out" \) 2>&1

Repository: kleros/kleros-v2

Length of output: 144


🏁 Script executed:

# Check the full package.json to see the complete context
cat bots/dispute-archival-bot/package.json

Repository: kleros/kleros-v2

Length of output: 420


🏁 Script executed:

# Check the parent tsconfig being extended
cat bots/dispute-archival-bot/node_modules/@kleros/kleros-v2-tsconfig/base20.json 2>/dev/null || echo "Cannot access parent config in sandbox"

# Also verify if there's a build script or any way TypeScript is supposed to be compiled
cd bots/dispute-archival-bot && find . -name "*.json" -o -name "*.ts" -o -name "*.js" | grep -E "(build|compile|tsconfig)" | head -20

Repository: kleros/kleros-v2

Length of output: 115


The start script requires a TypeScript runtime executor to work.

With emitDeclarationOnly: true in tsconfig.json, TypeScript only generates declaration files (.d.ts), not executable JavaScript. Running node index.ts directly will fail because Node.js cannot execute TypeScript natively. Add a runtime executor like tsx:

Suggested fix
   "scripts": {
-    "start": "node index.ts"
+    "start": "tsx index.ts"
   },
@@
   "dependencies": {
-    "@kleros/kleros-sdk": "workspace:^"
+    "@kleros/kleros-sdk": "workspace:^",
+    "tsx": "^4.20.6"
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/dispute-archival-bot/package.json` at line 6, The start script currently
uses "node index.ts" which fails because tsconfig.json has emitDeclarationOnly:
true and no compiled JS exists; update the package.json "start" script (the
start npm script) to use a TypeScript runtime executor such as tsx (e.g., "tsx
index.ts") or change to run the compiled output (e.g., build step then "node
dist/index.js"); ensure any new dependency (tsx) is added to devDependencies and
update scripts accordingly (e.g., add a "build" that compiles and adjust start
to run compiled JS if you prefer a build step).

},
"dependencies": {
"@kleros/kleros-sdk": "workspace:^"
},
"devDependencies": {
"@kleros/kleros-v2-tsconfig": "workspace:^",
"@types/node": "^25.6.0",
"dotenv": "^17.4.2",
"viem": "^2.24.1"
Comment on lines +11 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

BOT_PKG=$(fd -a '^package\.json$' bots/dispute-archival-bot | head -n1)

echo "runtime imports of dotenv/viem in bot sources:"
rg -n --type=ts -C2 'from "dotenv"|from "viem"|import "dotenv/config"' bots/dispute-archival-bot

echo "declared dependency sections:"
jq -r '{dependencies, devDependencies}' "$BOT_PKG"

Repository: kleros/kleros-v2

Length of output: 1848


dotenv and viem should be runtime dependencies, not dev-only.

These packages are imported directly at runtime: dotenv/config loads at the entry point, and viem is used across multiple runtime utility modules (rpc.ts, contract.ts, fetchDisputeEvidences.ts, fetchDisputeDetailsFromSubgraph.ts). Keeping them in devDependencies will break production installs that omit dev packages.

✅ Suggested fix
   "dependencies": {
-    "@kleros/kleros-sdk": "workspace:^"
+    "@kleros/kleros-sdk": "workspace:^",
+    "dotenv": "^17.4.2",
+    "viem": "^2.24.1"
   },
   "devDependencies": {
     "@kleros/kleros-v2-tsconfig": "workspace:^",
-    "@types/node": "^25.6.0",
-    "dotenv": "^17.4.2",
-    "viem": "^2.24.1"
+    "@types/node": "^25.6.0"
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"devDependencies": {
"@kleros/kleros-v2-tsconfig": "workspace:^",
"@types/node": "^25.6.0",
"dotenv": "^17.4.2",
"viem": "^2.24.1"
"dependencies": {
"@kleros/kleros-sdk": "workspace:^",
"dotenv": "^17.4.2",
"viem": "^2.24.1"
},
"devDependencies": {
"@kleros/kleros-v2-tsconfig": "workspace:^",
"@types/node": "^25.6.0"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/dispute-archival-bot/package.json` around lines 11 - 15, The
package.json incorrectly lists "dotenv" and "viem" under devDependencies but
they are imported at runtime (e.g., "dotenv/config" at the entry point and
"viem" used in rpc.ts, contract.ts, fetchDisputeEvidences.ts,
fetchDisputeDetailsFromSubgraph.ts); move "dotenv" and "viem" from
devDependencies into dependencies in package.json (remove them from the
devDependencies section and add them to dependencies) and then run your package
manager to update lockfile so production installs include them.

}
}
13 changes: 13 additions & 0 deletions bots/dispute-archival-bot/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "@kleros/kleros-v2-tsconfig/base20.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"allowImportingTsExtensions": true,
"types": [
"node"
],
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
68 changes: 68 additions & 0 deletions bots/dispute-archival-bot/utils/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createPublicClient, createWalletClient, type Address } from "viem";
import { getEnvConfig } from "../config.ts";
import { privateKeyToAccount } from "viem/accounts";
import { alchemyTransport } from "./rpc.ts";
import { arbitrumSepolia } from "viem/chains";
import { disputeArchiveAbi } from "../abi/DisputeArchive.ts";

// WARNING: temporary
export const CHAIN = arbitrumSepolia;

const getClients = () => {
const config = getEnvConfig();

const account = privateKeyToAccount(`0x${config.privateKey}`);

return {
publicClient: createPublicClient({
chain: CHAIN,
transport: alchemyTransport(CHAIN.id),
}),
walletClient: createWalletClient({
account,
chain: CHAIN,
transport: alchemyTransport(CHAIN.id),
}),
account,
};
};

export async function registerCid(disputeId: string, courtId: string, cid: string) {
const { publicClient, walletClient, account } = getClients();
const config = getEnvConfig();

const { request } = await publicClient.simulateContract({
account,
address: config.disputeArchiveAddress as Address,
abi: disputeArchiveAbi,
functionName: "register",
args: [BigInt(disputeId), BigInt(courtId), cid],
});

const hash = await walletClient.writeContract(request);

const receipt = await publicClient.waitForTransactionReceipt({
hash,
});

if (receipt.status !== "success") {
throw new Error("Transaction failed");
}

return hash;
}

// checks if the dispute was already archived
export async function isDisputeArchived(disputeId: string) {
const { publicClient } = getClients();
const config = getEnvConfig();

const res = await publicClient.readContract({
address: config.disputeArchiveAddress as Address,
abi: disputeArchiveAbi,
functionName: "archivedDisputeToCid",
args: [BigInt(disputeId)],
});

return res.length !== 0;
}
Loading
Loading