@ton-agent-kit/plugin-identity 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @ton-agent-kit/plugin-identity
2
+
3
+ Identity plugin for on-chain agent registration, discovery, and reputation management on TON. Agents can register themselves, discover other agents by capability, and query reputation scores backed by a Tact smart contract.
4
+
5
+ Part of [TON Agent Kit](https://github.com/Andy00L/ton-agent-kit).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @ton-agent-kit/plugin-identity @ton-agent-kit/core zod
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { TonAgentKit, KeypairWallet } from "@ton-agent-kit/core";
17
+ import IdentityPlugin from "@ton-agent-kit/plugin-identity";
18
+
19
+ const agent = new TonAgentKit(wallet).use(IdentityPlugin);
20
+
21
+ // Register the agent on-chain
22
+ await agent.runAction("register_agent", {
23
+ name: "my-agent",
24
+ services: ["swap", "analytics"],
25
+ });
26
+
27
+ // Discover other agents
28
+ const agents = await agent.runAction("discover_agent", {
29
+ service: "swap",
30
+ });
31
+
32
+ // Check reputation
33
+ const rep = await agent.runAction("get_agent_reputation", {
34
+ agentAddress: "EQBx...",
35
+ });
36
+ console.log(rep.score);
37
+ ```
38
+
39
+ ## Actions
40
+
41
+ | Action | Description |
42
+ |---|---|
43
+ | `register_agent` | Register the current agent on-chain with metadata. |
44
+ | `discover_agent` | Discover registered agents, optionally filtered by service. |
45
+ | `get_agent_reputation` | Query an agent's on-chain reputation score. |
46
+ | `deploy_reputation_contract` | Deploy a new Reputation smart contract. |
47
+ | `withdraw_reputation_fees` | Withdraw accumulated fees from the Reputation contract. |
48
+ | `process_pending_ratings` | Process queued ratings into finalized scores. |
49
+ | `get_open_disputes` | List currently open reputation disputes. |
50
+ | `trigger_cleanup` | Trigger cleanup of expired or stale agent registrations. |
51
+ | `get_agent_cleanup_info` | Get cleanup eligibility info for an agent. |
52
+
53
+ ## Documentation
54
+
55
+ See [docs/reputation-system.md](https://github.com/Andy00L/ton-agent-kit/blob/main/docs/reputation-system.md) for the full reputation system design.
56
+
57
+ ## Links
58
+
59
+ - [GitHub](https://github.com/Andy00L/ton-agent-kit)
60
+ - [npm](https://www.npmjs.com/package/@ton-agent-kit/plugin-identity)
61
+
62
+ ## License
63
+
64
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ton-agent-kit/plugin-identity",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Identity plugin for TON Agent Kit — agent registry, discovery, and reputation",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ import { Address, internal, toNano, beginCell } from "@ton/core";
3
+
4
+ import { defineAction, toFriendlyAddress, sendTransaction } from "@ton-agent-kit/core";
5
+ import { saveReputationConfig } from "../reputation-config";
6
+
7
+ export const deployReputationContractAction = defineAction({
8
+ name: "deploy_reputation_contract",
9
+ description:
10
+ "Deploy the on-chain Reputation smart contract (Tact) to TON. Once deployed, register_agent and get_agent_reputation will use it automatically for verifiable, immutable reputation scores.",
11
+ schema: z.object({
12
+ fee: z
13
+ .string()
14
+ .optional()
15
+ .describe("Fee per write operation in TON. Defaults to '0.01'."),
16
+ }),
17
+ handler: async (agent, params) => {
18
+ try {
19
+ // Dynamic import of compiled Tact contract
20
+ // This file is generated by: bun run contracts/deploy-reputation.ts
21
+ let Reputation: any;
22
+ let storeDeploy: any;
23
+ try {
24
+ const mod = await import("../contracts/Reputation_Reputation");
25
+ Reputation = mod.Reputation;
26
+ storeDeploy = mod.storeDeploy;
27
+ } catch {
28
+ return {
29
+ deployed: false,
30
+ message:
31
+ "Compiled contract not found. Run 'bun run contracts/deploy-reputation.ts' first to compile the Tact contract, then copy output to packages/plugin-identity/src/contracts/.",
32
+ };
33
+ }
34
+
35
+ const ownerAddress = agent.wallet.address;
36
+ const reputation = await Reputation.fromInit(ownerAddress);
37
+
38
+ const deployBody = beginCell()
39
+ .store(storeDeploy({ $$type: "Deploy", queryId: BigInt(0) }))
40
+ .endCell();
41
+
42
+ await sendTransaction(agent, [
43
+ internal({
44
+ to: reputation.address,
45
+ value: toNano("0.05"),
46
+ bounce: false,
47
+ init: reputation.init!,
48
+ body: deployBody,
49
+ }),
50
+ ]);
51
+
52
+ const contractAddr = reputation.address.toRawString();
53
+
54
+ // Save config so other actions auto-detect the contract
55
+ saveReputationConfig({
56
+ contractAddress: contractAddr,
57
+ network: agent.network,
58
+ deployedAt: new Date().toISOString(),
59
+ });
60
+
61
+ return {
62
+ deployed: true,
63
+ contractAddress: contractAddr,
64
+ friendlyContract: toFriendlyAddress(reputation.address, agent.network),
65
+ network: agent.network,
66
+ fee: params.fee || "0.01",
67
+ message: `Reputation contract deployed at ${toFriendlyAddress(reputation.address, agent.network)}. Agents can now register on-chain.`,
68
+ };
69
+ } catch (err: any) {
70
+ return {
71
+ deployed: false,
72
+ error: err.message,
73
+ message: `Failed to deploy reputation contract: ${err.message}`,
74
+ };
75
+ }
76
+ },
77
+ });
@@ -2,50 +2,98 @@ import { z } from "zod";
2
2
  import { Address } from "@ton/core";
3
3
  import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
4
4
  import { loadAgentRegistry } from "../utils";
5
+ import { resolveContractAddress } from "../reputation-config";
6
+ import { callContractGetter, lookupAgentIndex, parseAgentDataFromStack } from "../reputation-helpers";
5
7
 
6
- export const discoverAgentAction = defineAction({
7
- name: "discover_agent",
8
- description:
9
- "Find registered agents by capability or name. Search the local agent registry to find agents that can perform specific tasks.",
10
- schema: z.object({
11
- capability: z
12
- .string()
13
- .optional()
14
- .describe("Capability to search for (e.g., 'price_feed', 'trading')"),
15
- name: z.string().optional().describe("Agent name to search for"),
16
- }),
17
- handler: async (agent, params) => {
18
- const registry = loadAgentRegistry();
19
- let results = Object.values(registry);
20
-
21
- if (params.capability) {
22
- const cap = params.capability.toLowerCase();
23
- results = results.filter((a: any) =>
24
- a.capabilities.some((c: string) => c.toLowerCase().includes(cap)),
25
- );
26
- }
27
-
28
- if (params.name) {
29
- const name = params.name.toLowerCase();
30
- results = results.filter((a: any) =>
31
- a.name.toLowerCase().includes(name),
32
- );
33
- }
34
-
35
- return {
36
- query: { capability: params.capability, name: params.name },
37
- count: results.length,
38
- agents: results.map((a: any) => ({
39
- id: a.id,
40
- name: a.name,
41
- address: a.address,
42
- friendlyAddress: toFriendlyAddress(Address.parse(a.address), agent.network),
43
- capabilities: a.capabilities,
44
- description: a.description,
45
- endpoint: a.endpoint,
46
- reputation: a.reputation,
47
- registeredAt: a.registeredAt,
48
- })),
49
- };
50
- },
51
- });
8
+ export function createDiscoverAgentAction(contractAddress?: string) {
9
+ return defineAction({
10
+ name: "discover_agent",
11
+ description:
12
+ "Find registered agents by capability or name. Searches both on-chain (if contract deployed) and local registry. Set includeOffline=true to include unavailable agents.",
13
+ schema: z.object({
14
+ capability: z
15
+ .string()
16
+ .optional()
17
+ .describe("Capability to search for (e.g., 'price_feed', 'trading')"),
18
+ name: z.string().optional().describe("Agent name to search for"),
19
+ includeOffline: z
20
+ .boolean()
21
+ .optional()
22
+ .describe("Include offline/unavailable agents. Defaults to false."),
23
+ }),
24
+ handler: async (agent, params) => {
25
+ const addr = resolveContractAddress(contractAddress, agent.network);
26
+
27
+ // ── Hybrid mode: JSON registry for discovery, on-chain for reputation ──
28
+ const registry = loadAgentRegistry();
29
+ let results = Object.values(registry);
30
+
31
+ if (!params.includeOffline) {
32
+ results = results.filter((a: any) => a.available !== false);
33
+ }
34
+
35
+ if (params.capability) {
36
+ const cap = params.capability.toLowerCase();
37
+ results = results.filter((a: any) =>
38
+ a.capabilities.some((c: string) => c.toLowerCase().includes(cap)),
39
+ );
40
+ }
41
+
42
+ if (params.name) {
43
+ const name = params.name.toLowerCase();
44
+ results = results.filter((a: any) => a.name.toLowerCase().includes(name));
45
+ }
46
+
47
+ // Enrich with on-chain reputation if contract is available
48
+ if (addr) {
49
+ const apiBase =
50
+ agent.network === "testnet"
51
+ ? "https://testnet.tonapi.io/v2"
52
+ : "https://tonapi.io/v2";
53
+
54
+ for (const a of results as any[]) {
55
+ try {
56
+ const idx = await lookupAgentIndex(apiBase, addr, a.name, agent.config.TONAPI_KEY);
57
+ if (idx !== null) {
58
+ const dataRes = await callContractGetter(
59
+ apiBase, addr, "agentData", [idx.toString()], agent.config.TONAPI_KEY,
60
+ );
61
+ const parsed = parseAgentDataFromStack(dataRes?.stack);
62
+ if (parsed) {
63
+ const rep = parsed.totalTasks > 0
64
+ ? Math.round((parsed.successes / parsed.totalTasks) * 100)
65
+ : 0;
66
+ a.reputation = { score: rep, totalTasks: parsed.totalTasks, successfulTasks: parsed.successes };
67
+ a.onChain = true;
68
+ }
69
+ }
70
+ } catch {
71
+ // On-chain lookup failed for this agent — keep JSON data
72
+ }
73
+ }
74
+ }
75
+
76
+ return {
77
+ query: { capability: params.capability, name: params.name, includeOffline: params.includeOffline },
78
+ count: results.length,
79
+ agents: results.map((a: any) => ({
80
+ id: a.id,
81
+ name: a.name,
82
+ address: a.address,
83
+ friendlyAddress: a.address ? toFriendlyAddress(Address.parse(a.address), agent.network) : "",
84
+ capabilities: a.capabilities,
85
+ available: a.available !== false,
86
+ description: a.description,
87
+ endpoint: a.endpoint,
88
+ reputation: a.reputation,
89
+ registeredAt: a.registeredAt,
90
+ onChain: a.onChain || false,
91
+ })),
92
+ onChain: !!addr,
93
+ contractAddress: addr || undefined,
94
+ };
95
+ },
96
+ });
97
+ }
98
+
99
+ export const discoverAgentAction = createDiscoverAgentAction();
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "@ton-agent-kit/core";
3
+ import { resolveContractAddress } from "../reputation-config";
4
+ import { callContractGetter } from "../reputation-helpers";
5
+
6
+ export const getAgentCleanupInfoAction = defineAction({
7
+ name: "get_agent_cleanup_info",
8
+ description:
9
+ "Check if an agent is eligible for cleanup. Shows score, ratings count, last active date, and which cleanup condition would trigger removal. " +
10
+ "Reasons: 1=bad_score (<20% with 100+ ratings), 2=inactive (30+ days), 3=ghost (0 ratings after 7 days).",
11
+ schema: z.object({
12
+ agentIndex: z.coerce.number().describe("Agent index to check"),
13
+ }),
14
+ handler: async (agent, params) => {
15
+ const addr = resolveContractAddress(undefined, agent.network);
16
+ if (!addr) {
17
+ return { message: "No reputation contract configured" };
18
+ }
19
+
20
+ const apiBase = agent.network === "testnet"
21
+ ? "https://testnet.tonapi.io/v2"
22
+ : "https://tonapi.io/v2";
23
+
24
+ const result = await callContractGetter(
25
+ apiBase, addr, "agentCleanupInfo",
26
+ [params.agentIndex.toString()],
27
+ agent.config.TONAPI_KEY,
28
+ );
29
+
30
+ if (!result?.stack || result.stack.length === 0) {
31
+ return { message: "Failed to read cleanup info" };
32
+ }
33
+
34
+ // agentCleanupInfo returns a struct as a tuple
35
+ const stack = result.stack;
36
+ let items = stack;
37
+ if (stack[0]?.type === "tuple" && stack[0].tuple) {
38
+ items = stack[0].tuple;
39
+ }
40
+
41
+ const parseNum = (item: any): number => {
42
+ if (!item || item.type !== "num") return 0;
43
+ const s = item.num as string;
44
+ if (s.startsWith("-0x") || s.startsWith("-0X")) return Number(-BigInt(s.slice(1)));
45
+ return Number(BigInt(s));
46
+ };
47
+ const parseBool = (item: any): boolean => {
48
+ if (!item) return false;
49
+ if (item.type === "num") {
50
+ const s = item.num as string;
51
+ if (s.startsWith("-0x")) return -BigInt(s.slice(1)) !== 0n;
52
+ return BigInt(s) !== 0n;
53
+ }
54
+ return false;
55
+ };
56
+
57
+ const info = {
58
+ index: parseNum(items[0]),
59
+ exists: parseBool(items[1]),
60
+ score: parseNum(items[2]),
61
+ totalRatings: parseNum(items[3]),
62
+ registeredAt: parseNum(items[4]),
63
+ lastActive: parseNum(items[5]),
64
+ daysSinceActive: parseNum(items[6]),
65
+ daysSinceRegistered: parseNum(items[7]),
66
+ eligibleForCleanup: parseBool(items[8]),
67
+ cleanupReason: parseNum(items[9]),
68
+ };
69
+
70
+ const reasons: Record<number, string> = {
71
+ 0: "none — agent is healthy",
72
+ 1: "bad_score — score < 20% with 100+ ratings",
73
+ 2: "inactive — no activity for 30+ days",
74
+ 3: "ghost — 0 ratings after 7+ days since registration",
75
+ };
76
+
77
+ return {
78
+ ...info,
79
+ cleanupReasonText: reasons[info.cleanupReason] || "unknown",
80
+ onChain: true,
81
+ contractAddress: addr,
82
+ };
83
+ },
84
+ });
@@ -1,53 +1,126 @@
1
1
  import { z } from "zod";
2
- import { Address } from "@ton/core";
3
- import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
2
+ import { Address, internal, toNano } from "@ton/core";
3
+ import { defineAction, toFriendlyAddress, sendTransaction } from "@ton-agent-kit/core";
4
4
  import { loadAgentRegistry, saveAgentRegistry } from "../utils";
5
+ import { resolveContractAddress } from "../reputation-config";
6
+ import { buildRateBody, callContractGetter, lookupAgentIndex, parseAgentDataFromStack } from "../reputation-helpers";
5
7
 
6
- export const getAgentReputationAction = defineAction({
7
- name: "get_agent_reputation",
8
- description:
9
- "Get the reputation score of a registered agent. Set addTask=true and success=true to record a successful task. Set addTask=true and success=false to record a failed task.",
10
- schema: z.object({
11
- agentId: z.string().describe("Agent ID to check or update"),
12
- addTask: z
13
- .union([z.boolean(), z.string()])
14
- .optional()
15
- .describe("Set to true to record a completed task"),
16
- success: z
17
- .union([z.boolean(), z.string()])
18
- .optional()
19
- .describe("If addTask is true, whether the task was successful"),
20
- }),
21
- handler: async (agent, params) => {
22
- const registry = loadAgentRegistry();
23
- const agentRecord = registry[params.agentId];
24
- if (!agentRecord) throw new Error(`Agent not found: ${params.agentId}`);
25
-
26
- // Coerce string booleans
27
- const addTask = typeof params.addTask === "string" ? params.addTask === "true" : params.addTask;
28
- const success = typeof params.success === "string" ? params.success === "true" : params.success;
29
-
30
- // Update reputation if recording a task
31
- if (addTask) {
32
- agentRecord.reputation.totalTasks += 1;
33
- if (success !== false) {
34
- agentRecord.reputation.successfulTasks += 1;
8
+ export function createGetAgentReputationAction(contractAddress?: string) {
9
+ return defineAction({
10
+ name: "get_agent_reputation",
11
+ description:
12
+ "Get the reputation score of a registered agent. Set addTask=true and success=true/false to record a task result. Works both on-chain and locally.",
13
+ schema: z.object({
14
+ agentId: z.string().describe("Agent ID or name to check or update"),
15
+ addTask: z
16
+ .union([z.boolean(), z.string()])
17
+ .optional()
18
+ .describe("Set to true to record a completed task"),
19
+ success: z
20
+ .union([z.boolean(), z.string()])
21
+ .optional()
22
+ .describe("If addTask is true, whether the task was successful"),
23
+ }),
24
+ handler: async (agent, params) => {
25
+ const addTask = typeof params.addTask === "string" ? params.addTask === "true" : params.addTask;
26
+ const success = typeof params.success === "string" ? params.success === "true" : params.success;
27
+
28
+ const addr = resolveContractAddress(contractAddress, agent.network);
29
+
30
+ // ── On-chain mode ──
31
+ if (addr) {
32
+ try {
33
+ const apiBase =
34
+ agent.network === "testnet"
35
+ ? "https://testnet.tonapi.io/v2"
36
+ : "https://tonapi.io/v2";
37
+
38
+ // Get the actual agent name for on-chain lookup
39
+ // Prefer JSON registry (has the exact name used at registration)
40
+ const jsonRegistry = loadAgentRegistry();
41
+ const jsonRecord = jsonRegistry[params.agentId];
42
+ const agentName = jsonRecord?.name
43
+ || (params.agentId.startsWith("agent_") ? params.agentId.slice(6) : params.agentId);
44
+
45
+ // If rating, send Rate message
46
+ if (addTask) {
47
+ const body = buildRateBody(agentName, success !== false);
48
+
49
+ await sendTransaction(agent, [
50
+ internal({
51
+ to: Address.parse(addr),
52
+ value: toNano("0.03"),
53
+ bounce: true,
54
+ body,
55
+ }),
56
+ ]);
57
+ }
58
+
59
+ // Look up agent index by name hash
60
+ const idx = await lookupAgentIndex(apiBase, addr, agentName, agent.config.TONAPI_KEY);
61
+
62
+ if (idx !== null) {
63
+ // Get on-chain data using index
64
+ const dataRes = await callContractGetter(
65
+ apiBase, addr, "agentData", [idx.toString()], agent.config.TONAPI_KEY,
66
+ );
67
+ const repRes = await callContractGetter(
68
+ apiBase, addr, "agentReputation", [idx.toString()], agent.config.TONAPI_KEY,
69
+ );
70
+
71
+ const agentData = dataRes?.stack ? parseAgentDataFromStack(dataRes.stack) : null;
72
+ const repScore = repRes?.stack?.[0]?.num
73
+ ? Number(BigInt(repRes.stack[0].num))
74
+ : 0;
75
+
76
+ return {
77
+ agentId: params.agentId,
78
+ name: agentName,
79
+ reputation: {
80
+ score: repScore,
81
+ totalTasks: agentData?.totalTasks || 0,
82
+ successfulTasks: agentData?.successes || 0,
83
+ },
84
+ available: agentData?.available ?? true,
85
+ onChain: true,
86
+ contractAddress: addr,
87
+ };
88
+ }
89
+
90
+ // Agent not found on-chain — fall through to JSON
91
+ } catch {
92
+ // Fall through to JSON
93
+ }
35
94
  }
36
- agentRecord.reputation.score = Math.round(
37
- (agentRecord.reputation.successfulTasks /
38
- agentRecord.reputation.totalTasks) *
39
- 100,
40
- );
41
- saveAgentRegistry(registry);
42
- }
43
-
44
- return {
45
- agentId: params.agentId,
46
- name: agentRecord.name,
47
- address: agentRecord.address,
48
- friendlyAddress: toFriendlyAddress(Address.parse(agentRecord.address), agent.network),
49
- reputation: agentRecord.reputation,
50
- registeredAt: agentRecord.registeredAt,
51
- };
52
- },
53
- });
95
+
96
+ // ── JSON fallback mode ──
97
+ const registry = loadAgentRegistry();
98
+ const agentRecord = registry[params.agentId];
99
+ if (!agentRecord) throw new Error(`Agent not found: ${params.agentId}`);
100
+
101
+ if (addTask) {
102
+ agentRecord.reputation.totalTasks += 1;
103
+ if (success !== false) {
104
+ agentRecord.reputation.successfulTasks += 1;
105
+ }
106
+ agentRecord.reputation.score = Math.round(
107
+ (agentRecord.reputation.successfulTasks / agentRecord.reputation.totalTasks) * 100,
108
+ );
109
+ saveAgentRegistry(registry);
110
+ }
111
+
112
+ return {
113
+ agentId: params.agentId,
114
+ name: agentRecord.name,
115
+ address: agentRecord.address,
116
+ friendlyAddress: toFriendlyAddress(Address.parse(agentRecord.address), agent.network),
117
+ reputation: agentRecord.reputation,
118
+ available: agentRecord.available !== false,
119
+ onChain: false,
120
+ registeredAt: agentRecord.registeredAt,
121
+ };
122
+ },
123
+ });
124
+ }
125
+
126
+ export const getAgentReputationAction = createGetAgentReputationAction();
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "@ton-agent-kit/core";
3
+ import { resolveContractAddress } from "../reputation-config";
4
+ import { callContractGetter, parseDisputeData } from "../reputation-helpers";
5
+
6
+ export const getOpenDisputesAction = defineAction({
7
+ name: "get_open_disputes",
8
+ description:
9
+ "Get all open (unsettled) disputes from the reputation contract. Arbiters use this to find disputes they can join and vote on.",
10
+ schema: z.object({
11
+ limit: z.coerce.number().optional().describe("Max disputes to return. Default 20."),
12
+ }),
13
+ handler: async (agent, params) => {
14
+ const addr = resolveContractAddress(undefined, agent.network);
15
+ if (!addr) {
16
+ return { disputes: [], count: 0, message: "No reputation contract configured" };
17
+ }
18
+
19
+ const apiBase = agent.network === "testnet"
20
+ ? "https://testnet.tonapi.io/v2"
21
+ : "https://tonapi.io/v2";
22
+
23
+ const limit = params.limit || 20;
24
+
25
+ const countRes = await callContractGetter(apiBase, addr, "disputeCount", undefined, agent.config.TONAPI_KEY);
26
+ const totalDisputes = countRes?.stack?.[0]?.num ? Number(BigInt(countRes.stack[0].num)) : 0;
27
+
28
+ if (totalDisputes === 0) {
29
+ return { disputes: [], count: 0, total: 0, message: "No disputes registered" };
30
+ }
31
+
32
+ const disputes: any[] = [];
33
+ let checked = 0;
34
+ for (let i = totalDisputes - 1; i >= 0 && disputes.length < limit; i--) {
35
+ try {
36
+ const dataRes = await callContractGetter(apiBase, addr, "disputeData", [i.toString()], agent.config.TONAPI_KEY);
37
+ if (dataRes?.stack) {
38
+ const dispute = parseDisputeData(dataRes.stack);
39
+ if (dispute && !dispute.settled) {
40
+ disputes.push({
41
+ index: i,
42
+ escrowAddress: dispute.escrowAddress,
43
+ depositor: dispute.depositor,
44
+ beneficiary: dispute.beneficiary,
45
+ amount: dispute.amount,
46
+ votingDeadline: new Date(dispute.votingDeadline * 1000).toISOString(),
47
+ isExpired: Date.now() / 1000 > dispute.votingDeadline,
48
+ });
49
+ }
50
+ }
51
+ } catch {}
52
+ checked++;
53
+ if (checked > 100) break;
54
+ }
55
+
56
+ return {
57
+ disputes,
58
+ count: disputes.length,
59
+ total: totalDisputes,
60
+ onChain: true,
61
+ contractAddress: addr,
62
+ message: `Found ${disputes.length} open dispute(s)`,
63
+ };
64
+ },
65
+ });