Skip to content

feat(bots): dispute-archival-bot-first-iteration#2280

Draft
tractorss wants to merge 3 commits into
devfrom
feat/dispute-archival-bot
Draft

feat(bots): dispute-archival-bot-first-iteration#2280
tractorss wants to merge 3 commits into
devfrom
feat/dispute-archival-bot

Conversation

@tractorss
Copy link
Copy Markdown
Contributor

@tractorss tractorss commented May 8, 2026

Diagram (tentative)

dispute-archival-bot

Summary by CodeRabbit

  • New Features

    • Preserve historical disputes as IPFS snapshots and register them on-chain via a new archive contract.
    • Automated archival bot to collect, upload, and register dispute snapshots.
    • UI and data hooks to surface archived dispute snapshots and use archived evidences/details when available.
    • Subgraph and indexing updated to mark and serve archived disputes.
  • Documentation

    • Added operational README and example environment template; local env file is ignored.

Review Change Stack


PR-Codex overview

This PR introduces a DisputeArchive feature, allowing for the archival of dispute data from Kleros into IPFS, along with updates to related queries and components to support this functionality.

Detailed summary

  • Added .env and .env.example for environment variables.
  • Modified package.json to include new dependencies and scripts.
  • Introduced GraphQLError and GraphQLResponse types in types.ts.
  • Updated schema.graphql to include isArchived and archiveCid.
  • Implemented DisputeArchive contract with events for archiving disputes.
  • Created utility functions for fetching and uploading dispute data to IPFS.
  • Enhanced various hooks and components to integrate archived dispute data.
  • Updated README with usage instructions and notes on functionality.

The following files were skipped due to too many changes: contracts/deployments/arbitrumSepoliaDevnet/DisputeArchive.json

✨ Ask PR-Codex anything about this PR by commenting with /codex {your question}

@netlify
Copy link
Copy Markdown

netlify Bot commented May 8, 2026

Deploy Preview for kleros-v2-testnet-devtools failed. Why did it fail? →

Name Link
🔨 Latest commit 5ce32b3
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-testnet-devtools/deploys/6a08af00c55f550008d6efeb

@netlify
Copy link
Copy Markdown

netlify Bot commented May 8, 2026

Deploy Preview for kleros-v2-neo failed. Why did it fail? →

Name Link
🔨 Latest commit 5ce32b3
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-neo/deploys/6a08aeff4421d000086de972

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Walkthrough

This PR introduces a complete dispute archival system: a DisputeArchive Solidity contract and deploy artifacts, subgraph schema and mapping updates to record archived disputes, and a TypeScript archival bot with utilities to fetch dispute data, assemble snapshots, upload to IPFS, and register CIDs on-chain.

Changes

Dispute Archival Contract & Bot Implementation

Layer / File(s) Summary
Bot packaging & docs
bots/dispute-archival-bot/.env.example, bots/dispute-archival-bot/.gitignore, bots/dispute-archival-bot/README.md, bots/dispute-archival-bot/package.json, bots/dispute-archival-bot/tsconfig.json, package.json
Adds env template, gitignore rules, README, package metadata, tsconfig for the bot, and expands root workspace pattern to include bots/**.
Environment config
bots/dispute-archival-bot/config.ts
Typed env key map, EnvConfig type, getEnvConfig() that validates presence of required env vars, and EnvVariableNotConfiguredError.
GraphQL types & fetch utilities
bots/dispute-archival-bot/utils/types.ts, bots/dispute-archival-bot/utils/fetchDisputeDetailsFromSubgraph.ts, bots/dispute-archival-bot/utils/fetchDisputeEvidences.ts, bots/dispute-archival-bot/utils/fetchDisputeIds.ts, bots/dispute-archival-bot/utils/fetchDisputeTemplate.ts
Typed GraphQL shapes and functions to query core and DTR subgraphs for dispute details, evidences, dispute IDs, and dispute templates.
RPC & SDK helpers
bots/dispute-archival-bot/utils/rpc.ts, bots/dispute-archival-bot/utils/fetchPopulatedDisputeData.ts
Alchemy RPC URL and fallback transport builder, and Kleros SDK wiring to fetch populated dispute data using Alchemy transport.
Snapshot assembly & IPFS upload
bots/dispute-archival-bot/utils/fetchDisputeArchiveSnapshot.ts, bots/dispute-archival-bot/utils/uploadToIpfs.ts
DisputeArchiveSnapshot type and assembly function that concurrently fetches populated data, evidences, and template; serializes snapshot and uploads to IPFS returning CID.
Contract clients & ABI
bots/dispute-archival-bot/utils/contract.ts, bots/dispute-archival-bot/abi/DisputeArchive.ts
Viem-based public and wallet clients, isDisputeArchived() read, registerCid() write helper, and TypeScript ABI export for DisputeArchive.
Bot orchestration
bots/dispute-archival-bot/index.ts
Entrypoint script: fetch dispute IDs, skip already-archived disputes, assemble snapshots, upload to IPFS, register CIDs on-chain, and log results.
Solidity contract & deploy
contracts/src/archive/DisputeArchive.sol, contracts/deploy/00-home-chan-dispute-archive.ts, contracts/deployments/arbitrumSepoliaDevnet/DisputeArchive.json
New DisputeArchive contract stores CID mapping, provides owner-only register/amend, emits ArchivedDispute (with courtId) and ArchivedDisputeAmended, includes custom errors; deploy script and deployment artifact added.
Subgraph schema & mappings
subgraph/core/schema.graphql, subgraph/core/src/DisputeArchive.ts, subgraph/core/src/archive/disputeArchiveStubs.ts, subgraph/core/src/entities/Dispute.ts, subgraph/core/subgraph.template.yaml, subgraph/core/subgraph.yaml
Adds isArchived and archiveCid to Dispute schema, mapping handlers for archive/amend events, stub helpers to create archive-only disputes, and manifest datasource wiring for DisputeArchive.
Web integration
web/src/hooks/queries/*, web/src/components/Verdict/FinalDecision.tsx, web/src/pages/Cases/CaseDetails/MaintenanceButtons/index.tsx
Client-side hooks and components updated to optionally load archive snapshots from IPFS and to gate fetching/UI when disputes are archived or archive data are loading.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🐰 I hop through subgraphs, hashes, and code,
I stitch each dispute into one tidy node,
Snapshots to IPFS, CIDs shining bright,
The bot signs on-chain in the pale moonlight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introduction of a dispute-archival-bot as the first iteration, matching the detailed changeset across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dispute-archival-bot

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 8, 2026

Deploy Preview for kleros-v2-testnet ready!

Name Link
🔨 Latest commit 5ce32b3
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-testnet/deploys/6a08aeffe1664200086cda6c
😎 Deploy Preview https://deploy-preview-2280--kleros-v2-testnet.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (1)
bots/dispute-archival-bot/.env.example (1)

14-14: 💤 Low value

Add trailing newline for POSIX compliance.

The file is missing a trailing newline at the end. While not critical, adding one aligns with POSIX text file standards and prevents potential issues with certain text processing tools.

📝 Proposed fix
 export ALCHEMY_API_KEY=""
-export PRIVATE_KEY=""
\ No newline at end of file
+export PRIVATE_KEY=""
🤖 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/.env.example` at line 14, The file ends with the
environment variable declaration export PRIVATE_KEY="" and lacks a trailing
newline; update the .env.example so that the final line (export PRIVATE_KEY="")
is followed by a single newline character at EOF to make the file
POSIX-compliant.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@bots/dispute-archival-bot/index.ts`:
- Around line 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.

In `@bots/dispute-archival-bot/package.json`:
- Around line 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.
- 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).

In `@bots/dispute-archival-bot/README.md`:
- 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.

In `@bots/dispute-archival-bot/utils/fetchDisputeDetailsFromSubgraph.ts`:
- Around line 156-157: The current single GraphQL `query` in
fetchDisputeDetailsFromSubgraph.ts that requests multiple list fields (rounds,
drawnJurors, localRounds, answers) can silently drop items if any list exceeds
subgraph page limits; change the implementation to first fetch the top-level
dispute by id (keep the existing `query` but only select scalar/top-level
fields), then implement dedicated paginated queries for each heavy collection
(e.g., queries and helper functions like paginateRounds, paginateDrawnJurors,
paginateLocalRounds, paginateAnswers or a generic paginateSubgraphCollection)
that page by `first/skip` or use cursor-based pagination and filter by
disputeId/roundId as appropriate, and then merge those paginated results into
the dispute snapshot before returning from fetchDisputeDetailsFromSubgraph.

In `@bots/dispute-archival-bot/utils/fetchDisputeEvidences.ts`:
- Around line 26-27: The GraphQL query in fetchDisputeEvidences currently
requests evidences(first: 1000) which silently truncates results for groups
>1000; update the logic in fetchDisputeEvidences to implement cursor (or offset)
pagination for the evidences field (using the evidenceGroupID, orderBy:
timestamp, orderDirection: asc) by repeatedly querying with an after cursor (or
increasing offset), appending each page to the accumulated evidences array and
stopping when a page returns fewer than the pageSize; apply the same change to
the other evidences query occurrences referenced in the diff (the block around
lines 44-77) so all evidence fetches fully paginate instead of using a
single-page cap.

In `@bots/dispute-archival-bot/utils/fetchDisputeIds.ts`:
- Around line 10-16: The current GraphQL query constant (query) in
fetchDisputeIds only requests first: 1000 and will miss any disputes beyond
that; update fetchDisputeIds to paginate and fetch all disputes (use either
skip+first loop or cursor-based pagination if supported), replacing the
hardcoded query with a parameterized query that accepts first and skip (or after
cursor), iterate requests until no more results, collect all dispute ids into a
single array, and ensure existing error handling/logging in fetchDisputeIds
continues to operate per-request.

In `@bots/dispute-archival-bot/utils/fetchPopulatedDisputeData.ts`:
- Around line 29-35: The catch block in fetchPopulatedDisputeData silently
returns undefined and only console.logs the error; change it to (1) replace
console.log with a structured logger call (e.g., processLogger.error) including
disputeId and full error object, (2) push the disputeId and error metadata into
a shared failure collector (e.g., failedDisputeIds array or failedDisputeRecords
map) so callers can retry or report, and (3) differentiate transient vs
permanent failures by inspecting the error (e.g., error.name, instanceof
network/fetch error, or HTTP status on the response) and record a failureType
flag (transient|permanent) in the collector and/or emit a metric/alert call
(e.g., metrics.increment('dispute.fetch.fail', {type})). Ensure the symbol names
referenced are fetchPopulatedDisputeData, disputeId,
failedDisputeIds/failedDisputeRecords, processLogger, and metrics so you can
locate and wire this behavior into existing flows.

In `@bots/dispute-archival-bot/utils/uploadToIpfs.ts`:
- Around line 27-31: The code reads json.cids.length without validating the
response shape; update the uploadToIpfs logic to first ensure the parsed json is
an object and that json.cids exists and is an array (e.g.,
Array.isArray(json.cids)) before checking length, and throw the same upload
error (including disputeId) when cids is missing or not an array; locate the
check around the variable named json in the uploadToIpfs function and replace
the direct json.cids.length access with a defensive check that handles
missing/null responses and non-array cids.

In `@contracts/src/archive/DisputeArchive.sol`:
- Around line 54-56: The updateOwner function currently allows assigning
address(0), which can brick admin actions; modify the updateOwner(address
newOwner) function to reject zero addresses by adding a require(newOwner !=
address(0), "new owner is the zero address") (or similar) before setting owner,
and optionally emit an OwnershipTransferred/OwnerUpdated event after changing
owner to help auditing; reference updateOwner, owner, and onlyOwner when making
the change and ensure register and amend remain guarded by onlyOwner as before.

---

Nitpick comments:
In `@bots/dispute-archival-bot/.env.example`:
- Line 14: The file ends with the environment variable declaration export
PRIVATE_KEY="" and lacks a trailing newline; update the .env.example so that the
final line (export PRIVATE_KEY="") is followed by a single newline character at
EOF to make the file POSIX-compliant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d8484e87-8539-4311-9610-b3b29a92c9d0

📥 Commits

Reviewing files that changed from the base of the PR and between 185686b and 46b212b.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (22)
  • bots/dispute-archival-bot/.env.example
  • bots/dispute-archival-bot/.gitignore
  • bots/dispute-archival-bot/README.md
  • bots/dispute-archival-bot/abi/DisputeArchive.ts
  • bots/dispute-archival-bot/config.ts
  • bots/dispute-archival-bot/index.ts
  • bots/dispute-archival-bot/package.json
  • bots/dispute-archival-bot/tsconfig.json
  • bots/dispute-archival-bot/utils/contract.ts
  • bots/dispute-archival-bot/utils/fetchDisputeArchiveSnapshot.ts
  • bots/dispute-archival-bot/utils/fetchDisputeDetailsFromSubgraph.ts
  • bots/dispute-archival-bot/utils/fetchDisputeEvidences.ts
  • bots/dispute-archival-bot/utils/fetchDisputeIds.ts
  • bots/dispute-archival-bot/utils/fetchDisputeTemplate.ts
  • bots/dispute-archival-bot/utils/fetchPopulatedDisputeData.ts
  • bots/dispute-archival-bot/utils/rpc.ts
  • bots/dispute-archival-bot/utils/types.ts
  • bots/dispute-archival-bot/utils/uploadToIpfs.ts
  • contracts/deploy/00-home-chan-dispute-archive.ts
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeArchive.json
  • contracts/src/archive/DisputeArchive.sol
  • package.json

Comment on lines +16 to +32
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}`);
}
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.

"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).

Comment on lines +11 to +15
"devDependencies": {
"@kleros/kleros-v2-tsconfig": "workspace:^",
"@types/node": "^25.6.0",
"dotenv": "^17.4.2",
"viem": "^2.24.1"
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.


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.

Comment on lines +156 to +157
// assuming that we don't have more than 1000 items to fetch, so no need for pagination
const query = `
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 | 🏗️ Heavy lift

Nested list data can be silently incomplete in this query.

This query fetches multiple list fields in one shot without pagination (rounds, drawnJurors, localRounds, answers). If any exceed the subgraph page defaults, the archive snapshot will miss data.

A safer pattern is: fetch top-level dispute once, then paginate each heavy child collection with dedicated queries keyed by dispute/round IDs.

Also applies to: 189-290

🤖 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/utils/fetchDisputeDetailsFromSubgraph.ts` around
lines 156 - 157, The current single GraphQL `query` in
fetchDisputeDetailsFromSubgraph.ts that requests multiple list fields (rounds,
drawnJurors, localRounds, answers) can silently drop items if any list exceeds
subgraph page limits; change the implementation to first fetch the top-level
dispute by id (keep the existing `query` but only select scalar/top-level
fields), then implement dedicated paginated queries for each heavy collection
(e.g., queries and helper functions like paginateRounds, paginateDrawnJurors,
paginateLocalRounds, paginateAnswers or a generic paginateSubgraphCollection)
that page by `first/skip` or use cursor-based pagination and filter by
disputeId/roundId as appropriate, and then merge those paginated results into
the dispute snapshot before returning from fetchDisputeDetailsFromSubgraph.

Comment on lines +26 to +27
evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: asc, first: 1000) {
id
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 | 🏗️ Heavy lift

Paginate evidence fetch to avoid silent truncation.

The current single-page query caps results at 1000, so disputes with more evidences will be archived incompletely.

🔧 Suggested direction (cursor/offset pagination)
- query Evidences($evidenceGroupID: String) {
-   evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: asc, first: 1000) {
+ query Evidences($evidenceGroupID: String!, $first: Int!, $skip: Int!) {
+   evidences(
+     where: { evidenceGroup: $evidenceGroupID }
+     orderBy: timestamp
+     orderDirection: asc
+     first: $first
+     skip: $skip
+   ) {
      ...
   }
 }
 export async function fetchDisputeEvidences(externalDisputeId: string) {
+  const pageSize = 1000;
+  const all: Evidence[] = [];
+  let skip = 0;
+
+  while (true) {
+    // fetch page with variables: { evidenceGroupID: externalDisputeId, first: pageSize, skip }
+    // parse/validate as today
+    // const page = json.data.evidences;
+    // all.push(...page);
+    // if (page.length < pageSize) break;
+    // skip += pageSize;
+  }
+
+  return all;
-  return json.data.evidences;
 }

Also applies to: 44-77

🤖 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/utils/fetchDisputeEvidences.ts` around lines 26 -
27, The GraphQL query in fetchDisputeEvidences currently requests
evidences(first: 1000) which silently truncates results for groups >1000; update
the logic in fetchDisputeEvidences to implement cursor (or offset) pagination
for the evidences field (using the evidenceGroupID, orderBy: timestamp,
orderDirection: asc) by repeatedly querying with an after cursor (or increasing
offset), appending each page to the accumulated evidences array and stopping
when a page returns fewer than the pageSize; apply the same change to the other
evidences query occurrences referenced in the diff (the block around lines
44-77) so all evidence fetches fully paginate instead of using a single-page
cap.

Comment on lines +10 to +16
const query = `
query Disputes {
disputes(first: 1000) {
id
}
}
`;
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 | 🏗️ Heavy lift

Implement pagination to handle more than 1000 disputes.

The GraphQL query has a hardcoded limit of first: 1000, which means only the first 1000 disputes will be fetched. If the system contains more disputes, newer ones will be silently excluded from archival without any error or warning.

For production use, implement pagination using GraphQL's skip and first parameters to fetch all disputes, or use cursor-based pagination if the subgraph supports it.

💡 Example pagination approach
export async function fetchDisputeIds() {
  const config = getEnvConfig();
  const allIds: string[] = [];
  let skip = 0;
  const first = 1000;
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(config.coreSubgraphUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        query: `
          query Disputes($first: Int!, $skip: Int!) {
            disputes(first: $first, skip: $skip, orderBy: id) {
              id
            }
          }
        `,
        variables: { first, skip },
      }),
    });

    // ... error handling ...

    const disputes = json.data.disputes;
    allIds.push(...disputes.map(d => d.id));
    
    hasMore = disputes.length === first;
    skip += first;
  }

  return allIds;
}
🤖 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/utils/fetchDisputeIds.ts` around lines 10 - 16, The
current GraphQL query constant (query) in fetchDisputeIds only requests first:
1000 and will miss any disputes beyond that; update fetchDisputeIds to paginate
and fetch all disputes (use either skip+first loop or cursor-based pagination if
supported), replacing the hardcoded query with a parameterized query that
accepts first and skip (or after cursor), iterate requests until no more
results, collect all dispute ids into a single array, and ensure existing error
handling/logging in fetchDisputeIds continues to operate per-request.

Comment on lines +29 to +35
} catch (error: unknown) {
if (error instanceof Error) {
// dispute is broken
console.log(`Unable to fetch populated data for dispute ${disputeId}: ${error.message}`);
}
return undefined;
}
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 | 🏗️ Heavy lift

Improve error visibility for failed dispute fetches.

The current error handling silently returns undefined when dispute data cannot be fetched, with only a console log for visibility. This approach could lead to incomplete archives without proper tracking of which disputes failed and why.

For production use, consider:

  • Collecting failed dispute IDs for retry or manual review
  • Emitting metrics/alerts when disputes fail to fetch
  • Differentiating between transient failures (network issues) and permanent failures (broken dispute data)
🤖 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/utils/fetchPopulatedDisputeData.ts` around lines 29
- 35, The catch block in fetchPopulatedDisputeData silently returns undefined
and only console.logs the error; change it to (1) replace console.log with a
structured logger call (e.g., processLogger.error) including disputeId and full
error object, (2) push the disputeId and error metadata into a shared failure
collector (e.g., failedDisputeIds array or failedDisputeRecords map) so callers
can retry or report, and (3) differentiate transient vs permanent failures by
inspecting the error (e.g., error.name, instanceof network/fetch error, or HTTP
status on the response) and record a failureType flag (transient|permanent) in
the collector and/or emit a metric/alert call (e.g.,
metrics.increment('dispute.fetch.fail', {type})). Ensure the symbol names
referenced are fetchPopulatedDisputeData, disputeId,
failedDisputeIds/failedDisputeRecords, processLogger, and metrics so you can
locate and wire this behavior into existing flows.

Comment on lines +27 to +31
const json = (await response.json()) as UploadResponse;

if (json.cids.length === 0) {
throw new Error(`uploadToIpfs: Failed to upload data for dispute ${disputeId}. No CID returned.`);
}
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

Validate response shape before reading cids.length.

If cids is missing or not an array, this path throws a runtime TypeError instead of a controlled upload error.

✅ Small defensive fix
   const json = (await response.json()) as UploadResponse;
-
-  if (json.cids.length === 0) {
+  if (!Array.isArray(json.cids) || json.cids.length === 0) {
     throw new Error(`uploadToIpfs: Failed to upload data for dispute ${disputeId}. No CID returned.`);
   }
📝 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
const json = (await response.json()) as UploadResponse;
if (json.cids.length === 0) {
throw new Error(`uploadToIpfs: Failed to upload data for dispute ${disputeId}. No CID returned.`);
}
const json = (await response.json()) as UploadResponse;
if (!Array.isArray(json.cids) || json.cids.length === 0) {
throw new Error(`uploadToIpfs: Failed to upload data for dispute ${disputeId}. No CID returned.`);
}
🤖 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/utils/uploadToIpfs.ts` around lines 27 - 31, The
code reads json.cids.length without validating the response shape; update the
uploadToIpfs logic to first ensure the parsed json is an object and that
json.cids exists and is an array (e.g., Array.isArray(json.cids)) before
checking length, and throw the same upload error (including disputeId) when cids
is missing or not an array; locate the check around the variable named json in
the uploadToIpfs function and replace the direct json.cids.length access with a
defensive check that handles missing/null responses and non-array cids.

Comment on lines +54 to +56
function updateOwner(address newOwner) external onlyOwner {
owner = newOwner;
}
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

Prevent zero-address owner assignment to avoid bricking admin actions.

Line 55 currently allows address(0), which can permanently disable register, amend, and future ownership updates.

🛡️ Suggested fix
+    error InvalidOwner();
+
     function updateOwner(address newOwner) external onlyOwner {
+        require(newOwner != address(0), InvalidOwner());
         owner = newOwner;
     }
📝 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
function updateOwner(address newOwner) external onlyOwner {
owner = newOwner;
}
error InvalidOwner();
function updateOwner(address newOwner) external onlyOwner {
if (newOwner == address(0)) revert InvalidOwner();
owner = newOwner;
}
Suggested change
function updateOwner(address newOwner) external onlyOwner {
owner = newOwner;
}
function updateOwner(address newOwner) external onlyOwner {
require(newOwner != address(0), "InvalidOwner");
owner = newOwner;
}
🤖 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 `@contracts/src/archive/DisputeArchive.sol` around lines 54 - 56, The
updateOwner function currently allows assigning address(0), which can brick
admin actions; modify the updateOwner(address newOwner) function to reject zero
addresses by adding a require(newOwner != address(0), "new owner is the zero
address") (or similar) before setting owner, and optionally emit an
OwnershipTransferred/OwnerUpdated event after changing owner to help auditing;
reference updateOwner, owner, and onlyOwner when making the change and ensure
register and amend remain guarded by onlyOwner as before.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@subgraph/core/src/archive/disputeArchiveStubs.ts`:
- Around line 95-100: The Court entity is loaded and its fields numberDisputes
and numberClosedDisputes are incremented but not persisted; after mutating court
in the block that calls Court.load(courtId.toString()) and before returning,
call court.save() to persist changes (ensure you still call
updateCourtCumulativeMetric(courtId.toString(), ONE, blockTimestamp,
"numberDisputes") as currently done).

In `@subgraph/core/src/DisputeArchive.ts`:
- Around line 41-45: The handler handleDisputeAmended currently calls
createArchiveDispute(event.params.id, ONE, ...) which uses a synthetic courtId
and can create a new Dispute when none exists; change handleDisputeAmended to
first check for an existing Dispute entity (e.g., Dispute.load(event.params.id))
and only call createArchiveDispute when that Dispute exists; if the Dispute does
not exist, either create only the archive-specific entity (without passing a
placeholder ONE) or call createArchiveDispute with a sentinel (null/undefined)
and update createArchiveDispute to early-return or avoid creating a new Dispute
when courtId is missing so no synthetic court linkage is introduced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5553abad-a7ed-449e-a7b3-4ad05ef6df31

📥 Commits

Reviewing files that changed from the base of the PR and between 46b212b and bac9bdc.

📒 Files selected for processing (11)
  • bots/dispute-archival-bot/abi/DisputeArchive.ts
  • bots/dispute-archival-bot/index.ts
  • bots/dispute-archival-bot/utils/contract.ts
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeArchive.json
  • contracts/src/archive/DisputeArchive.sol
  • subgraph/core/schema.graphql
  • subgraph/core/src/DisputeArchive.ts
  • subgraph/core/src/archive/disputeArchiveStubs.ts
  • subgraph/core/src/entities/Dispute.ts
  • subgraph/core/subgraph.template.yaml
  • subgraph/core/subgraph.yaml
✅ Files skipped from review due to trivial changes (3)
  • subgraph/core/src/entities/Dispute.ts
  • subgraph/core/schema.graphql
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeArchive.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • bots/dispute-archival-bot/abi/DisputeArchive.ts
  • bots/dispute-archival-bot/utils/contract.ts
  • bots/dispute-archival-bot/index.ts

Comment on lines +95 to +100
const court = Court.load(courtId.toString());
if (!court) return;
court.numberDisputes = court.numberDisputes.plus(ONE);
// archived dispute is closed already
court.numberClosedDisputes = court.numberClosedDisputes.plus(ONE);
updateCourtCumulativeMetric(courtId.toString(), ONE, blockTimestamp, "numberDisputes");
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

Persist updated Court counters before returning.

Line 97 and Line 99 mutate court, but without court.save() those changes are not written to the store.

💡 Suggested fix
   const court = Court.load(courtId.toString());
   if (!court) return;
   court.numberDisputes = court.numberDisputes.plus(ONE);
   // archived dispute is closed already
   court.numberClosedDisputes = court.numberClosedDisputes.plus(ONE);
+  court.save();
   updateCourtCumulativeMetric(courtId.toString(), ONE, blockTimestamp, "numberDisputes");
📝 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
const court = Court.load(courtId.toString());
if (!court) return;
court.numberDisputes = court.numberDisputes.plus(ONE);
// archived dispute is closed already
court.numberClosedDisputes = court.numberClosedDisputes.plus(ONE);
updateCourtCumulativeMetric(courtId.toString(), ONE, blockTimestamp, "numberDisputes");
const court = Court.load(courtId.toString());
if (!court) return;
court.numberDisputes = court.numberDisputes.plus(ONE);
// archived dispute is closed already
court.numberClosedDisputes = court.numberClosedDisputes.plus(ONE);
court.save();
updateCourtCumulativeMetric(courtId.toString(), ONE, blockTimestamp, "numberDisputes");
🤖 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 `@subgraph/core/src/archive/disputeArchiveStubs.ts` around lines 95 - 100, The
Court entity is loaded and its fields numberDisputes and numberClosedDisputes
are incremented but not persisted; after mutating court in the block that calls
Court.load(courtId.toString()) and before returning, call court.save() to
persist changes (ensure you still call
updateCourtCumulativeMetric(courtId.toString(), ONE, blockTimestamp,
"numberDisputes") as currently done).

Comment on lines +41 to +45
export function handleDisputeAmended(event: ArchivedDisputeAmended): void {
createArchiveDispute(
event.params.id,
ONE, // this is placeholder, will not be used
event.params.cid,
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

Avoid creating archive-only disputes from ArchivedDisputeAmended with placeholder courtId.

Line 44 passes a synthetic court ID (ONE), but Line 26 can still create a new dispute when missing. That can corrupt indexed data with an incorrect court linkage.

💡 Suggested fix
 function createArchiveDispute(
   disputeId: BigInt,
   courtId: BigInt,
   cid: string,
   reason: string | null,
   timestamp: BigInt,
   blockNumber: BigInt,
-  transactionHash: Bytes
+  transactionHash: Bytes,
+  allowCreateIfMissing: boolean
 ): void {
   const dispute = Dispute.load(disputeId.toString());

   // if dispute is present, then it is an amend event
   if (dispute) {
     dispute.isArchived = true;
     dispute.archiveCid = cid;
     dispute.save();
     return;
   }

+  if (!allowCreateIfMissing) {
+    log.warning("ArchivedDisputeAmended received for unknown dispute {}", [disputeId.toString()]);
+    return;
+  }
   createArchiveOnlyDispute(disputeId, courtId, cid, reason, timestamp, blockNumber, transactionHash);
 }
@@
   createArchiveDispute(
@@
-    event.transaction.hash
+    event.transaction.hash,
+    true
   );
 }
@@
   createArchiveDispute(
@@
-    event.transaction.hash
+    event.transaction.hash,
+    false
   );
 }
📝 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
export function handleDisputeAmended(event: ArchivedDisputeAmended): void {
createArchiveDispute(
event.params.id,
ONE, // this is placeholder, will not be used
event.params.cid,
export function handleDisputeAmended(event: ArchivedDisputeAmended): void {
createArchiveDispute(
event.params.id,
ONE, // this is placeholder, will not be used
event.params.cid,
null,
event.block.timestamp,
event.block.number,
event.transaction.hash,
false
);
}
🤖 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 `@subgraph/core/src/DisputeArchive.ts` around lines 41 - 45, The handler
handleDisputeAmended currently calls createArchiveDispute(event.params.id, ONE,
...) which uses a synthetic courtId and can create a new Dispute when none
exists; change handleDisputeAmended to first check for an existing Dispute
entity (e.g., Dispute.load(event.params.id)) and only call createArchiveDispute
when that Dispute exists; if the Dispute does not exist, either create only the
archive-specific entity (without passing a placeholder ONE) or call
createArchiveDispute with a sentinel (null/undefined) and update
createArchiveDispute to early-return or avoid creating a new Dispute when
courtId is missing so no synthetic court linkage is introduced.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)
5.8% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@web/src/hooks/queries/useDisputeArchiveSnapshot.ts`:
- Around line 234-239: The code uses isUndefined(archiveCid) which misses null
values so getIpfsUrl(archiveCid) can be called with null; update the guard in
useDisputeArchiveSnapshot (the block around archiveCid and the fetch) to return
null when archiveCid is either undefined or null (e.g., check archiveCid == null
or use a utility like isNil) before calling getIpfsUrl(archiveCid) and
performing the fetch to avoid invalid IPFS requests.

In `@web/src/hooks/queries/usePopulatedDisputeData.ts`:
- Around line 39-40: The current enablement logic in usePopulatedDisputeData
uses isUndefined which permits null and can cause the DisputeTemplate($id: ID!)
query to run with a null/invalid id; update the condition that references
disputeData.dispute?.arbitrableChainId and disputeData.dispute?.templateId (and
the similar checks around lines 56-57) to ensure the values are non-null and
non-undefined (e.g., check !== undefined && !== null or use a non-nullish check
like value != null or a truthy check for the id) before enabling the query, and
keep the archivedData guard (!isLoadingArchivedData && archivedData?.populated)
as-is but ensure archivedData.populated is explicitly checked for null/undefined
too so the query never receives a null id.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf37c272-76cd-4cb7-b286-6210269abfaf

📥 Commits

Reviewing files that changed from the base of the PR and between bac9bdc and 5ce32b3.

📒 Files selected for processing (8)
  • subgraph/core/src/archive/disputeArchiveStubs.ts
  • web/src/components/Verdict/FinalDecision.tsx
  • web/src/hooks/queries/useDisputeArchiveSnapshot.ts
  • web/src/hooks/queries/useDisputeDetailsQuery.ts
  • web/src/hooks/queries/useEvidences.ts
  • web/src/hooks/queries/usePopulatedDisputeData.ts
  • web/src/hooks/queries/useVotingHistory.ts
  • web/src/pages/Cases/CaseDetails/MaintenanceButtons/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • subgraph/core/src/archive/disputeArchiveStubs.ts

Comment on lines +234 to +239
const archiveCid = dispute?.archiveCid;
if (isUndefined(archiveCid)) {
return null;
}

const response = await fetch(getIpfsUrl(archiveCid));
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

Handle null archiveCid before IPFS fetch.

archiveCid can be null for non-archived disputes. The current isUndefined check misses that and can trigger invalid IPFS fetches (Line 239), causing query errors on normal disputes.

Suggested fix
-      const archiveCid = dispute?.archiveCid;
-      if (isUndefined(archiveCid)) {
+      const archiveCid = dispute?.archiveCid;
+      if (archiveCid == null || archiveCid === "") {
         return null;
       }
📝 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
const archiveCid = dispute?.archiveCid;
if (isUndefined(archiveCid)) {
return null;
}
const response = await fetch(getIpfsUrl(archiveCid));
const archiveCid = dispute?.archiveCid;
if (archiveCid == null || archiveCid === "") {
return null;
}
const response = await fetch(getIpfsUrl(archiveCid));
🤖 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 `@web/src/hooks/queries/useDisputeArchiveSnapshot.ts` around lines 234 - 239,
The code uses isUndefined(archiveCid) which misses null values so
getIpfsUrl(archiveCid) can be called with null; update the guard in
useDisputeArchiveSnapshot (the block around archiveCid and the fetch) to return
null when archiveCid is either undefined or null (e.g., check archiveCid == null
or use a utility like isNil) before calling getIpfsUrl(archiveCid) and
performing the fetch to avoid invalid IPFS requests.

Comment on lines +39 to +40
((!isUndefined(disputeData.dispute?.arbitrableChainId) && !isUndefined(disputeData.dispute?.templateId)) ||
(!isLoadingArchivedData && !isUndefined(archivedData?.populated)));
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

Tighten enablement checks for nullable template/chain fields.

isUndefined allows null, so the hook can enable and execute DisputeTemplate($id: ID!) with an undefined ID (Line 56). This is a runtime failure path for disputes with nullable metadata.

Suggested fix
-  const isEnabled =
+  const hasTemplateInputs =
+    disputeData?.dispute?.arbitrableChainId != null && disputeData?.dispute?.templateId != null;
+
+  const isEnabled =
     !isUndefined(disputeID) &&
     !isUndefined(disputeData) &&
     !isUndefined(disputeData?.dispute) &&
-    ((!isUndefined(disputeData.dispute?.arbitrableChainId) && !isUndefined(disputeData.dispute?.templateId)) ||
+    (hasTemplateInputs ||
       (!isLoadingArchivedData && !isUndefined(archivedData?.populated)));

Also applies to: 56-57

🤖 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 `@web/src/hooks/queries/usePopulatedDisputeData.ts` around lines 39 - 40, The
current enablement logic in usePopulatedDisputeData uses isUndefined which
permits null and can cause the DisputeTemplate($id: ID!) query to run with a
null/invalid id; update the condition that references
disputeData.dispute?.arbitrableChainId and disputeData.dispute?.templateId (and
the similar checks around lines 56-57) to ensure the values are non-null and
non-undefined (e.g., check !== undefined && !== null or use a non-nullish check
like value != null or a truthy check for the id) before enabling the query, and
keep the archivedData guard (!isLoadingArchivedData && archivedData?.populated)
as-is but ensure archivedData.populated is explicitly checked for null/undefined
too so the query never receives a null id.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant