@ton-agent-kit/plugin-identity 1.0.0 → 1.1.0

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/src/index.ts CHANGED
@@ -1,21 +1,73 @@
1
1
  import { definePlugin } from "@ton-agent-kit/core";
2
- import { registerAgentAction } from "./actions/register-agent";
3
- import { discoverAgentAction } from "./actions/discover-agent";
4
- import { getAgentReputationAction } from "./actions/get-agent-reputation";
2
+ import { createRegisterAgentAction, registerAgentAction } from "./actions/register-agent";
3
+ import { createDiscoverAgentAction, discoverAgentAction } from "./actions/discover-agent";
4
+ import { createGetAgentReputationAction, getAgentReputationAction } from "./actions/get-agent-reputation";
5
+ import { deployReputationContractAction } from "./actions/deploy-reputation-contract";
6
+ import { createWithdrawReputationFeesAction, withdrawReputationFeesAction } from "./actions/withdraw-reputation-fees";
7
+ import { processPendingRatingsAction } from "./actions/process-pending-ratings";
8
+ import { getOpenDisputesAction } from "./actions/get-open-disputes";
9
+ import { triggerCleanupAction } from "./actions/trigger-cleanup";
10
+ import { getAgentCleanupInfoAction } from "./actions/get-agent-cleanup-info";
5
11
 
6
12
  /**
7
- * Identity Plugin Agent registry, discovery, and reputation
13
+ * Identity Plugin -- On-chain agent registration, discovery, and reputation
14
+ * management.
15
+ *
16
+ * Allows AI agents to register themselves on-chain, discover other agents by
17
+ * capability, query and manage reputation scores backed by a Tact smart
18
+ * contract, and perform administrative operations such as fee withdrawal and
19
+ * cleanup of stale registrations.
8
20
  *
9
21
  * Actions:
10
- * - register_agent: Register an AI agent with capabilities
11
- * - discover_agent: Find agents by capability or name
12
- * - get_agent_reputation: Get/update agent reputation scores
22
+ * - `register_agent` -- Register the current agent on-chain with metadata
23
+ * - `discover_agent` -- Discover registered agents, optionally filtered by service
24
+ * - `get_agent_reputation` -- Query an agent's on-chain reputation score
25
+ * - `deploy_reputation_contract` -- Deploy a new Reputation smart contract
26
+ * - `withdraw_reputation_fees` -- Withdraw accumulated fees from the Reputation contract
27
+ * - `process_pending_ratings` -- Process queued ratings into finalized scores
28
+ * - `get_open_disputes` -- List currently open reputation disputes
29
+ * - `trigger_cleanup` -- Trigger cleanup of expired / stale agent registrations
30
+ * - `get_agent_cleanup_info` -- Get cleanup eligibility info for an agent
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import IdentityPlugin from "@ton-agent-kit/plugin-identity";
35
+ * const agent = new TonAgentKit(wallet, rpcUrl).use(IdentityPlugin);
36
+ * await agent.runAction("register_agent", { name: "my-agent", services: ["swap"] });
37
+ * ```
38
+ *
39
+ * @since 1.0.0
13
40
  */
14
- const IdentityPlugin = definePlugin({
15
- name: "identity",
16
- actions: [registerAgentAction, discoverAgentAction, getAgentReputationAction],
17
- });
41
+ export function createIdentityPlugin(opts?: { contractAddress?: string }) {
42
+ const addr = opts?.contractAddress;
43
+ return definePlugin({
44
+ name: "identity",
45
+ actions: [
46
+ createRegisterAgentAction(addr),
47
+ createDiscoverAgentAction(addr),
48
+ createGetAgentReputationAction(addr),
49
+ deployReputationContractAction,
50
+ createWithdrawReputationFeesAction(addr),
51
+ processPendingRatingsAction,
52
+ getOpenDisputesAction,
53
+ triggerCleanupAction,
54
+ getAgentCleanupInfoAction,
55
+ ],
56
+ });
57
+ }
18
58
 
59
+ const IdentityPlugin = createIdentityPlugin();
60
+ /** @since 1.0.0 */
19
61
  export default IdentityPlugin;
20
62
 
21
- export { registerAgentAction, discoverAgentAction, getAgentReputationAction };
63
+ export {
64
+ registerAgentAction,
65
+ discoverAgentAction,
66
+ getAgentReputationAction,
67
+ deployReputationContractAction,
68
+ withdrawReputationFeesAction,
69
+ processPendingRatingsAction,
70
+ getOpenDisputesAction,
71
+ triggerCleanupAction,
72
+ getAgentCleanupInfoAction,
73
+ };
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
+ import { resolve } from "path";
3
+
4
+ const CONFIG_FILE = resolve(".reputation-contract.json");
5
+
6
+ /**
7
+ * Configuration for a locally deployed reputation contract.
8
+ * Stored in `.reputation-contract.json` for persistence across sessions.
9
+ * @since 1.0.0
10
+ */
11
+ export interface ReputationConfig {
12
+ contractAddress: string;
13
+ network: string;
14
+ deployedAt: string;
15
+ }
16
+
17
+ /**
18
+ * Default deployed reputation contracts (hardcoded in SDK).
19
+ * These are used when no factory parameter or local config is found.
20
+ */
21
+ export const DEFAULT_REPUTATION_CONTRACTS: Record<string, string> = {
22
+ testnet: "0:5352445990487167d19102e3d1ed2715d69263972ef014bdc1a7561230e2087c",
23
+ };
24
+
25
+ /**
26
+ * Load the reputation contract configuration from `.reputation-contract.json`.
27
+ * @returns The config object, or null if the file doesn't exist.
28
+ * @since 1.0.0
29
+ */
30
+ export function loadReputationConfig(): ReputationConfig | null {
31
+ try {
32
+ if (existsSync(CONFIG_FILE)) {
33
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
34
+ }
35
+ } catch {}
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Save the reputation contract configuration to `.reputation-contract.json`.
41
+ * @param config - The configuration to persist.
42
+ * @since 1.0.0
43
+ */
44
+ export function saveReputationConfig(config: ReputationConfig): void {
45
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
46
+ }
47
+
48
+ /**
49
+ * Resolve the reputation contract address with fallback chain:
50
+ * 1. Factory parameter (createIdentityPlugin({ contractAddress: "..." }))
51
+ * 2. Local config file (.reputation-contract.json)
52
+ * 3. Default deployed contract (hardcoded in SDK)
53
+ * 4. null — JSON fallback mode (no contract)
54
+ */
55
+ export function resolveContractAddress(factoryAddr: string | undefined, network: string): string | null {
56
+ if (factoryAddr) return factoryAddr;
57
+ const config = loadReputationConfig();
58
+ if (config?.contractAddress) return config.contractAddress;
59
+ if (DEFAULT_REPUTATION_CONTRACTS[network]) return DEFAULT_REPUTATION_CONTRACTS[network];
60
+ return null;
61
+ }
@@ -0,0 +1,316 @@
1
+ import { beginCell, Cell, Address } from "@ton/core";
2
+ import { createHash } from "crypto";
3
+
4
+ // Opcodes from the compiled Tact wrapper (contracts/output/Reputation_Reputation.ts)
5
+ const OP_REGISTER = 950051591;
6
+ const OP_RATE = 2804297358;
7
+ const OP_UPDATE_AVAILABILITY = 1424124491;
8
+ const OP_WITHDRAW = 593874976;
9
+
10
+ /**
11
+ * Compute the SHA-256 hash of a name, matching Tact's `sha256()` function.
12
+ * Used to look up agents by name in the on-chain reputation contract.
13
+ *
14
+ * @param name - The agent name to hash.
15
+ * @returns 256-bit unsigned integer as bigint.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const hash = computeNameHash("price-oracle");
20
+ * // Use with agentIndexByNameHash getter
21
+ * ```
22
+ *
23
+ * @since 1.0.0
24
+ */
25
+ export function computeNameHash(name: string): bigint {
26
+ const hash = createHash("sha256").update(name).digest("hex");
27
+ return BigInt("0x" + hash);
28
+ }
29
+
30
+ /**
31
+ * Build a Register message body for the Reputation contract.
32
+ *
33
+ * @param name - Agent name to register.
34
+ * @param capabilities - Comma-separated list of capabilities (e.g. "price_feed,analytics").
35
+ * @param available - Whether the agent is currently available for work.
36
+ * @returns Encoded Cell ready to send as a message body.
37
+ * @since 1.0.0
38
+ */
39
+ export function buildRegisterBody(name: string, capabilities: string, available: boolean): Cell {
40
+ const b = beginCell();
41
+ b.storeUint(OP_REGISTER, 32);
42
+ b.storeStringRefTail(name);
43
+ b.storeStringRefTail(capabilities);
44
+ b.storeBit(available);
45
+ return b.endCell();
46
+ }
47
+
48
+ /**
49
+ * Build a Rate message body for the Reputation contract.
50
+ */
51
+ export function buildRateBody(agentName: string, success: boolean): Cell {
52
+ const b = beginCell();
53
+ b.storeUint(OP_RATE, 32);
54
+ b.storeStringRefTail(agentName);
55
+ b.storeBit(success);
56
+ return b.endCell();
57
+ }
58
+
59
+ /**
60
+ * Build an UpdateAvailability message body.
61
+ */
62
+ export function buildUpdateAvailabilityBody(name: string, available: boolean): Cell {
63
+ const b = beginCell();
64
+ b.storeUint(OP_UPDATE_AVAILABILITY, 32);
65
+ b.storeStringRefTail(name);
66
+ b.storeBit(available);
67
+ return b.endCell();
68
+ }
69
+
70
+ /**
71
+ * Build a Withdraw message body.
72
+ */
73
+ export function buildWithdrawBody(): Cell {
74
+ const b = beginCell();
75
+ b.storeUint(OP_WITHDRAW, 32);
76
+ return b.endCell();
77
+ }
78
+
79
+ /**
80
+ * Call a getter on any smart contract via the TONAPI `/v2/blockchain/accounts/{address}/methods/{method}` endpoint.
81
+ *
82
+ * @param apiBase - TONAPI base URL (e.g. "https://testnet.tonapi.io/v2").
83
+ * @param contractAddress - Raw contract address (e.g. "0:abc...").
84
+ * @param method - Getter method name (e.g. "agentCount", "escrowData").
85
+ * @param args - Optional array of string arguments to pass to the getter.
86
+ * @param tonapiKey - Optional TONAPI Bearer token for higher rate limits.
87
+ * @returns Parsed JSON response with `stack` array, or null on error.
88
+ * @since 1.0.0
89
+ */
90
+ export async function callContractGetter(
91
+ apiBase: string,
92
+ contractAddress: string,
93
+ method: string,
94
+ args?: string[],
95
+ tonapiKey?: string,
96
+ ): Promise<any> {
97
+ const headers: Record<string, string> = {};
98
+ if (tonapiKey) {
99
+ headers["Authorization"] = `Bearer ${tonapiKey}`;
100
+ }
101
+
102
+ let url = `${apiBase}/blockchain/accounts/${encodeURIComponent(contractAddress)}/methods/${encodeURIComponent(method)}`;
103
+ if (args && args.length > 0) {
104
+ url += "?" + args.map((a) => `args=${encodeURIComponent(a)}`).join("&");
105
+ }
106
+
107
+ const res = await fetch(url, { headers });
108
+ if (!res.ok) return null;
109
+ return res.json();
110
+ }
111
+
112
+ /**
113
+ * Look up an agent's on-chain index by name. Computes sha256(name) and calls
114
+ * the `agentIndexByNameHash` getter on the reputation contract.
115
+ *
116
+ * @param apiBase - TONAPI base URL.
117
+ * @param contractAddress - Reputation contract address.
118
+ * @param agentName - Agent name to look up.
119
+ * @param tonapiKey - Optional TONAPI key.
120
+ * @returns The agent index (0-based), or null if not found.
121
+ * @since 1.0.0
122
+ */
123
+ export async function lookupAgentIndex(
124
+ apiBase: string,
125
+ contractAddress: string,
126
+ agentName: string,
127
+ tonapiKey?: string,
128
+ ): Promise<number | null> {
129
+ const nameHash = computeNameHash(agentName);
130
+ const res = await callContractGetter(
131
+ apiBase, contractAddress, "agentIndexByNameHash",
132
+ ["0x" + nameHash.toString(16)], tonapiKey,
133
+ );
134
+ return parseOptionalNum(res?.stack);
135
+ }
136
+
137
+ /**
138
+ * Parse an AgentData struct from a TONAPI getter response stack.
139
+ * Handles both tuple-wrapped and flat stack formats, and both slice and cell address types.
140
+ *
141
+ * @param stack - The raw TONAPI stack array from a getter response.
142
+ * @returns Parsed agent data with owner, available, totalTasks, successes, registeredAt — or null.
143
+ * @since 1.0.0
144
+ */
145
+ export function parseAgentDataFromStack(stack: any[]): any | null {
146
+ if (!stack || stack.length === 0) return null;
147
+
148
+ const first = stack[0];
149
+ if (!first || first.type === "null") return null;
150
+
151
+ // agentData returns AgentData? — an optional tuple
152
+ let items: any[];
153
+ if (first.type === "tuple" && first.tuple) {
154
+ items = first.tuple;
155
+ } else {
156
+ items = stack;
157
+ }
158
+
159
+ if (items.length < 5) return null;
160
+ try {
161
+ return {
162
+ owner: parseStackAddress(items[0]),
163
+ available: parseStackBool(items[1]),
164
+ totalTasks: parseStackNum(items[2]),
165
+ successes: parseStackNum(items[3]),
166
+ registeredAt: parseStackNum(items[4]),
167
+ };
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Parse an optional Int from TONAPI getter stack.
175
+ * Tact optional: tuple is present or null type on stack.
176
+ */
177
+ export function parseOptionalNum(stack: any[]): number | null {
178
+ if (!stack || stack.length === 0) return null;
179
+ const item = stack[0];
180
+ if (!item || item.type === "null") return null;
181
+ if (item.type === "num" && item.num) {
182
+ return Number(BigInt(item.num));
183
+ }
184
+ return null;
185
+ }
186
+
187
+ /** Parse a TONAPI num string to bigint (handles "-0x1" format) */
188
+ function parseBigInt(s: string): bigint {
189
+ if (s.startsWith("-0x") || s.startsWith("-0X")) {
190
+ return -BigInt(s.slice(1));
191
+ }
192
+ return BigInt(s);
193
+ }
194
+
195
+ function parseStackNum(item: any): number {
196
+ if (!item) return 0;
197
+ if (item.type === "num") {
198
+ return Number(parseBigInt(item.num));
199
+ }
200
+ return 0;
201
+ }
202
+
203
+ function parseStackBool(item: any): boolean {
204
+ if (!item) return false;
205
+ if (item.type === "num") {
206
+ return parseBigInt(item.num) !== 0n;
207
+ }
208
+ if (item.type === "bool") {
209
+ return !!item.value;
210
+ }
211
+ return false;
212
+ }
213
+
214
+ function parseStackAddress(item: any): string {
215
+ if (!item) return "";
216
+ if (item.type === "slice" && item.slice) {
217
+ try {
218
+ return Address.parse(item.slice).toRawString();
219
+ } catch {
220
+ return item.slice;
221
+ }
222
+ }
223
+ // TONAPI returns addresses in tuples as cells (BOC format)
224
+ if (item.type === "cell" && item.cell) {
225
+ try {
226
+ const cell = Cell.fromBoc(Buffer.from(item.cell, "hex"))[0];
227
+ const slice = cell.beginParse();
228
+ const addr = slice.loadAddress();
229
+ return addr ? addr.toRawString() : "";
230
+ } catch {
231
+ // Try base64 encoding as fallback
232
+ try {
233
+ const cell = Cell.fromBoc(Buffer.from(item.cell, "base64"))[0];
234
+ const slice = cell.beginParse();
235
+ const addr = slice.loadAddress();
236
+ return addr ? addr.toRawString() : "";
237
+ } catch {}
238
+ return "";
239
+ }
240
+ }
241
+ return "";
242
+ }
243
+
244
+ /**
245
+ * Parse a DisputeInfo struct from a TONAPI getter response stack.
246
+ *
247
+ * @param stack - The raw TONAPI stack array.
248
+ * @returns Parsed dispute data with escrowAddress, depositor, beneficiary, amount, votingDeadline, settled — or null.
249
+ * @since 1.0.0
250
+ */
251
+ export function parseDisputeData(stack: any[]): any | null {
252
+ if (!stack || stack.length === 0) return null;
253
+
254
+ const first = stack[0];
255
+ if (!first || first.type === "null") return null;
256
+
257
+ let items: any[];
258
+ if (first.type === "tuple" && first.tuple) {
259
+ items = first.tuple;
260
+ } else {
261
+ items = stack;
262
+ }
263
+
264
+ if (items.length < 6) return null;
265
+ try {
266
+ return {
267
+ escrowAddress: parseStackAddress(items[0]),
268
+ depositor: parseStackAddress(items[1]),
269
+ beneficiary: parseStackAddress(items[2]),
270
+ amount: parseStackNum(items[3]),
271
+ votingDeadline: parseStackNum(items[4]),
272
+ settled: parseStackBool(items[5]),
273
+ };
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Compute the SHA-256 hash of a capability name for indexed on-chain lookup.
281
+ * Matches Tact's `sha256()` function output.
282
+ *
283
+ * @param capability - Capability name (e.g. "price_feed", "analytics").
284
+ * @returns 256-bit unsigned integer as bigint.
285
+ * @since 1.0.0
286
+ */
287
+ export function computeCapabilityHash(capability: string): bigint {
288
+ const hash = createHash("sha256").update(capability).digest("hex");
289
+ return BigInt("0x" + hash);
290
+ }
291
+
292
+ /**
293
+ * Parse a linked-list Cell into an array of indexes. Used for both the capability
294
+ * index (`agentsByCapability`) and intent service index (`intentsByServiceHash`).
295
+ * Each node stores a uint32 index + 1-bit hasNext flag + optional ref to the next node.
296
+ *
297
+ * @param cellHex - Hex-encoded BOC string from the TONAPI getter response.
298
+ * @returns Array of parsed uint32 indexes.
299
+ * @since 1.0.0
300
+ */
301
+ export function parseIndexCell(cellHex: string): number[] {
302
+ const indexes: number[] = [];
303
+ try {
304
+ const cell = Cell.fromBoc(Buffer.from(cellHex, "hex"))[0];
305
+ let slice = cell.beginParse();
306
+ while (true) {
307
+ indexes.push(slice.loadUint(32));
308
+ if (slice.remainingBits >= 1 && slice.loadBit()) {
309
+ slice = slice.loadRef().beginParse();
310
+ } else {
311
+ break;
312
+ }
313
+ }
314
+ } catch {}
315
+ return indexes;
316
+ }