arcbounty-agent-sdk 0.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/dist/index.mjs ADDED
@@ -0,0 +1,1029 @@
1
+ // src/ArcBountyAgent.ts
2
+ import {
3
+ createPublicClient,
4
+ createWalletClient,
5
+ decodeEventLog,
6
+ http,
7
+ defineChain,
8
+ isAddress
9
+ } from "viem";
10
+ import { privateKeyToAccount } from "viem/accounts";
11
+
12
+ // src/abi.ts
13
+ var BOUNTY_META_TUPLE = {
14
+ name: "",
15
+ type: "tuple",
16
+ components: [
17
+ { name: "jobId", type: "uint256" },
18
+ { name: "poster", type: "address" },
19
+ { name: "reward", type: "uint256" },
20
+ { name: "deadline", type: "uint256" },
21
+ { name: "ipfsDescHash", type: "string" },
22
+ { name: "category", type: "string" },
23
+ { name: "tags", type: "string[]" },
24
+ { name: "agentId", type: "uint256" },
25
+ { name: "agentOnly", type: "bool" },
26
+ { name: "humanOnly", type: "bool" },
27
+ { name: "whitelistedProvider", type: "address" },
28
+ { name: "assignedProvider", type: "address" },
29
+ { name: "submittedResultHash", type: "string" },
30
+ { name: "submittedAt", type: "uint256" },
31
+ { name: "isTaken", type: "bool" },
32
+ { name: "rejectedAt", type: "uint256" },
33
+ { name: "rejectionReasonHash", type: "string" },
34
+ { name: "inDispute", type: "bool" },
35
+ { name: "resolved", type: "bool" },
36
+ { name: "disputeInitiator", type: "address" },
37
+ { name: "disputeRaisedAt", type: "uint256" },
38
+ { name: "disputeReasonHash", type: "string" },
39
+ { name: "disputeResponseHash", type: "string" },
40
+ { name: "disputeRulingHash", type: "string" }
41
+ ]
42
+ };
43
+ var BOUNTY_ADAPTER_ABI = [
44
+ // ── Write ──
45
+ {
46
+ name: "createBounty",
47
+ type: "function",
48
+ stateMutability: "nonpayable",
49
+ inputs: [{
50
+ name: "p",
51
+ type: "tuple",
52
+ components: [
53
+ { name: "provider", type: "address" },
54
+ { name: "reward", type: "uint256" },
55
+ { name: "deadline", type: "uint256" },
56
+ { name: "ipfsDescHash", type: "string" },
57
+ { name: "category", type: "string" },
58
+ { name: "tags", type: "string[]" },
59
+ { name: "agentOnly", type: "bool" },
60
+ { name: "humanOnly", type: "bool" }
61
+ ]
62
+ }],
63
+ outputs: [{ name: "jobId", type: "uint256" }]
64
+ },
65
+ {
66
+ name: "takeBounty",
67
+ type: "function",
68
+ stateMutability: "nonpayable",
69
+ inputs: [
70
+ { name: "jobId", type: "uint256" },
71
+ { name: "agentId", type: "uint256" }
72
+ ],
73
+ outputs: []
74
+ },
75
+ {
76
+ name: "submitWork",
77
+ type: "function",
78
+ stateMutability: "nonpayable",
79
+ inputs: [
80
+ { name: "jobId", type: "uint256" },
81
+ { name: "ipfsResultHash", type: "string" }
82
+ ],
83
+ outputs: []
84
+ },
85
+ {
86
+ name: "approveBounty",
87
+ type: "function",
88
+ stateMutability: "nonpayable",
89
+ inputs: [
90
+ { name: "jobId", type: "uint256" },
91
+ { name: "reputationScore", type: "uint8" }
92
+ ],
93
+ outputs: []
94
+ },
95
+ {
96
+ name: "autoApprove",
97
+ type: "function",
98
+ stateMutability: "nonpayable",
99
+ inputs: [{ name: "jobId", type: "uint256" }],
100
+ outputs: []
101
+ },
102
+ {
103
+ name: "rejectBounty",
104
+ type: "function",
105
+ stateMutability: "nonpayable",
106
+ inputs: [
107
+ { name: "jobId", type: "uint256" },
108
+ { name: "ipfsReasonHash", type: "string" }
109
+ ],
110
+ outputs: []
111
+ },
112
+ {
113
+ name: "challengeRejection",
114
+ type: "function",
115
+ stateMutability: "nonpayable",
116
+ inputs: [
117
+ { name: "jobId", type: "uint256" },
118
+ { name: "ipfsReasonHash", type: "string" }
119
+ ],
120
+ outputs: []
121
+ },
122
+ {
123
+ name: "finalizeRejection",
124
+ type: "function",
125
+ stateMutability: "nonpayable",
126
+ inputs: [{ name: "jobId", type: "uint256" }],
127
+ outputs: []
128
+ },
129
+ {
130
+ name: "cancelBounty",
131
+ type: "function",
132
+ stateMutability: "nonpayable",
133
+ inputs: [{ name: "jobId", type: "uint256" }],
134
+ outputs: []
135
+ },
136
+ {
137
+ name: "expireBounty",
138
+ type: "function",
139
+ stateMutability: "nonpayable",
140
+ inputs: [{ name: "jobId", type: "uint256" }],
141
+ outputs: []
142
+ },
143
+ {
144
+ name: "disputeBounty",
145
+ type: "function",
146
+ stateMutability: "nonpayable",
147
+ inputs: [
148
+ { name: "jobId", type: "uint256" },
149
+ { name: "ipfsReasonHash", type: "string" }
150
+ ],
151
+ outputs: []
152
+ },
153
+ {
154
+ name: "respondToDispute",
155
+ type: "function",
156
+ stateMutability: "nonpayable",
157
+ inputs: [
158
+ { name: "jobId", type: "uint256" },
159
+ { name: "ipfsResponseHash", type: "string" }
160
+ ],
161
+ outputs: []
162
+ },
163
+ {
164
+ name: "resolveDispute",
165
+ type: "function",
166
+ stateMutability: "nonpayable",
167
+ inputs: [
168
+ { name: "jobId", type: "uint256" },
169
+ { name: "payProvider", type: "bool" },
170
+ { name: "ipfsRulingHash", type: "string" },
171
+ { name: "reputationPenalty", type: "uint8" }
172
+ ],
173
+ outputs: []
174
+ },
175
+ {
176
+ name: "claimDefaultRuling",
177
+ type: "function",
178
+ stateMutability: "nonpayable",
179
+ inputs: [{ name: "jobId", type: "uint256" }],
180
+ outputs: []
181
+ },
182
+ // ── Read ──
183
+ {
184
+ name: "getOpenBounties",
185
+ type: "function",
186
+ stateMutability: "view",
187
+ inputs: [
188
+ { name: "category", type: "string" },
189
+ { name: "offset", type: "uint256" },
190
+ { name: "limit", type: "uint256" }
191
+ ],
192
+ outputs: [{ name: "result", type: "uint256[]" }]
193
+ },
194
+ {
195
+ name: "getBountyMeta",
196
+ type: "function",
197
+ stateMutability: "view",
198
+ inputs: [{ name: "jobId", type: "uint256" }],
199
+ outputs: [BOUNTY_META_TUPLE]
200
+ },
201
+ {
202
+ name: "getMyPostedBounties",
203
+ type: "function",
204
+ stateMutability: "view",
205
+ inputs: [{ name: "poster", type: "address" }],
206
+ outputs: [{ name: "", type: "uint256[]" }]
207
+ },
208
+ {
209
+ name: "getMyAssignedBounties",
210
+ type: "function",
211
+ stateMutability: "view",
212
+ inputs: [{ name: "provider", type: "address" }],
213
+ outputs: [{ name: "", type: "uint256[]" }]
214
+ },
215
+ {
216
+ name: "getAgentBounties",
217
+ type: "function",
218
+ stateMutability: "view",
219
+ inputs: [{ name: "agentId", type: "uint256" }],
220
+ outputs: [{ name: "", type: "uint256[]" }]
221
+ },
222
+ {
223
+ name: "APPROVAL_TIMEOUT",
224
+ type: "function",
225
+ stateMutability: "view",
226
+ inputs: [],
227
+ outputs: [{ name: "", type: "uint256" }]
228
+ },
229
+ {
230
+ name: "getAgentReputation",
231
+ type: "function",
232
+ stateMutability: "view",
233
+ inputs: [{ name: "agentId", type: "uint256" }],
234
+ outputs: [{
235
+ name: "",
236
+ type: "tuple",
237
+ components: [
238
+ { name: "averageScore", type: "uint256" },
239
+ { name: "totalFeedbacks", type: "uint256" },
240
+ { name: "totalJobs", type: "uint256" }
241
+ ]
242
+ }]
243
+ },
244
+ {
245
+ name: "totalBounties",
246
+ type: "function",
247
+ stateMutability: "view",
248
+ inputs: [],
249
+ outputs: [{ name: "", type: "uint256" }]
250
+ },
251
+ {
252
+ name: "arbitrator",
253
+ type: "function",
254
+ stateMutability: "view",
255
+ inputs: [],
256
+ outputs: [{ name: "", type: "address" }]
257
+ },
258
+ {
259
+ name: "DISPUTE_RESPONSE_WINDOW",
260
+ type: "function",
261
+ stateMutability: "view",
262
+ inputs: [],
263
+ outputs: [{ name: "", type: "uint256" }]
264
+ },
265
+ {
266
+ name: "REJECTION_CHALLENGE_WINDOW",
267
+ type: "function",
268
+ stateMutability: "view",
269
+ inputs: [],
270
+ outputs: [{ name: "", type: "uint256" }]
271
+ },
272
+ // ── Events ──
273
+ {
274
+ name: "BountyCreated",
275
+ type: "event",
276
+ inputs: [
277
+ { name: "jobId", type: "uint256", indexed: true },
278
+ { name: "poster", type: "address", indexed: true },
279
+ { name: "reward", type: "uint256", indexed: false },
280
+ { name: "category", type: "string", indexed: false },
281
+ { name: "deadline", type: "uint256", indexed: false }
282
+ ]
283
+ },
284
+ {
285
+ name: "BountyTaken",
286
+ type: "event",
287
+ inputs: [
288
+ { name: "jobId", type: "uint256", indexed: true },
289
+ { name: "provider", type: "address", indexed: true },
290
+ { name: "agentId", type: "uint256", indexed: false }
291
+ ]
292
+ },
293
+ {
294
+ name: "WorkSubmitted",
295
+ type: "event",
296
+ inputs: [
297
+ { name: "jobId", type: "uint256", indexed: true },
298
+ { name: "provider", type: "address", indexed: true },
299
+ { name: "ipfsResultHash", type: "string", indexed: false }
300
+ ]
301
+ },
302
+ {
303
+ name: "BountyCompleted",
304
+ type: "event",
305
+ inputs: [
306
+ { name: "jobId", type: "uint256", indexed: true },
307
+ { name: "agentId", type: "uint256", indexed: false },
308
+ { name: "reputationScore", type: "uint256", indexed: false }
309
+ ]
310
+ },
311
+ {
312
+ name: "DisputeRaised",
313
+ type: "event",
314
+ inputs: [
315
+ { name: "jobId", type: "uint256", indexed: true },
316
+ { name: "initiator", type: "address", indexed: true },
317
+ { name: "reasonHash", type: "string", indexed: false }
318
+ ]
319
+ },
320
+ {
321
+ name: "DisputeResponded",
322
+ type: "event",
323
+ inputs: [
324
+ { name: "jobId", type: "uint256", indexed: true },
325
+ { name: "responder", type: "address", indexed: true },
326
+ { name: "responseHash", type: "string", indexed: false }
327
+ ]
328
+ },
329
+ {
330
+ name: "DisputeResolved",
331
+ type: "event",
332
+ inputs: [
333
+ { name: "jobId", type: "uint256", indexed: true },
334
+ { name: "payProvider", type: "bool", indexed: false },
335
+ { name: "rulingHash", type: "string", indexed: false },
336
+ { name: "defaultRuling", type: "bool", indexed: false }
337
+ ]
338
+ }
339
+ ];
340
+ var IDENTITY_REGISTRY_ABI = [
341
+ {
342
+ name: "register",
343
+ type: "function",
344
+ stateMutability: "nonpayable",
345
+ inputs: [{ name: "metadataURI", type: "string" }],
346
+ outputs: [{ name: "agentId", type: "uint256" }]
347
+ },
348
+ {
349
+ name: "ownerOf",
350
+ type: "function",
351
+ stateMutability: "view",
352
+ inputs: [{ name: "agentId", type: "uint256" }],
353
+ outputs: [{ name: "", type: "address" }]
354
+ },
355
+ {
356
+ name: "Transfer",
357
+ type: "event",
358
+ inputs: [
359
+ { name: "from", type: "address", indexed: true },
360
+ { name: "to", type: "address", indexed: true },
361
+ { name: "tokenId", type: "uint256", indexed: true }
362
+ ]
363
+ }
364
+ ];
365
+ var ERC20_ABI = [
366
+ {
367
+ name: "approve",
368
+ type: "function",
369
+ stateMutability: "nonpayable",
370
+ inputs: [
371
+ { name: "spender", type: "address" },
372
+ { name: "amount", type: "uint256" }
373
+ ],
374
+ outputs: [{ name: "", type: "bool" }]
375
+ },
376
+ {
377
+ name: "allowance",
378
+ type: "function",
379
+ stateMutability: "view",
380
+ inputs: [
381
+ { name: "owner", type: "address" },
382
+ { name: "spender", type: "address" }
383
+ ],
384
+ outputs: [{ name: "", type: "uint256" }]
385
+ },
386
+ {
387
+ name: "balanceOf",
388
+ type: "function",
389
+ stateMutability: "view",
390
+ inputs: [{ name: "account", type: "address" }],
391
+ outputs: [{ name: "", type: "uint256" }]
392
+ }
393
+ ];
394
+
395
+ // src/constants.ts
396
+ var ARC_TESTNET_RPC = "https://rpc.testnet.arc.network";
397
+ var ARC_TESTNET_CHAIN_ID = 5042002;
398
+ var CONTRACTS = {
399
+ AGENTIC_COMMERCE: "0x0747EEf0706327138c69792bF28Cd525089e4583",
400
+ IDENTITY_REGISTRY: "0x8004A818BFB912233c491871b3d84c89A494BD9e",
401
+ REPUTATION_REGISTRY: "0x8004B663056A597Dffe9eCcC1965A193B7388713",
402
+ USDC: "0x3600000000000000000000000000000000000000"
403
+ };
404
+ var USDC_DECIMALS = 6;
405
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
406
+ var IPFS_GATEWAYS = [
407
+ "https://gateway.pinata.cloud/ipfs/",
408
+ "https://ipfs.io/ipfs/",
409
+ "https://cloudflare-ipfs.com/ipfs/"
410
+ ];
411
+
412
+ // src/ipfs.ts
413
+ function cidFromUri(uriOrCid) {
414
+ return uriOrCid.replace(/^ipfs:\/\//, "");
415
+ }
416
+ async function fetchIpfsText(uriOrCid) {
417
+ const cid = cidFromUri(uriOrCid);
418
+ for (const gateway of IPFS_GATEWAYS) {
419
+ try {
420
+ const res = await fetch(`${gateway}${cid}`);
421
+ if (res.ok) return res.text();
422
+ } catch {
423
+ }
424
+ }
425
+ throw new Error(`Failed to fetch IPFS content: ${uriOrCid}`);
426
+ }
427
+ async function fetchIpfsJson(uriOrCid) {
428
+ const text = await fetchIpfsText(uriOrCid);
429
+ return JSON.parse(text);
430
+ }
431
+ function isPinningConfigured() {
432
+ return Boolean(process.env["PINATA_JWT"] || process.env["PINATA_API_KEY"] && process.env["PINATA_SECRET"]);
433
+ }
434
+ async function pinText(content, filename = "result.md") {
435
+ const jwt = process.env["PINATA_JWT"];
436
+ const apiKey = process.env["PINATA_API_KEY"];
437
+ const apiSecret = process.env["PINATA_SECRET"];
438
+ const blob = new Blob([content], { type: "text/plain" });
439
+ if (jwt) {
440
+ const form = new FormData();
441
+ form.append("file", blob, filename);
442
+ const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
443
+ method: "POST",
444
+ headers: { Authorization: `Bearer ${jwt}` },
445
+ body: form
446
+ });
447
+ if (!res.ok) {
448
+ const text = await res.text().catch(() => "");
449
+ throw new Error(`Pinata v2 error ${res.status}: ${text}`);
450
+ }
451
+ const data = await res.json();
452
+ return `ipfs://${data.IpfsHash}`;
453
+ }
454
+ if (apiKey && apiSecret) {
455
+ const form = new FormData();
456
+ form.append("file", blob, filename);
457
+ const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
458
+ method: "POST",
459
+ headers: {
460
+ pinata_api_key: apiKey,
461
+ pinata_secret_api_key: apiSecret
462
+ },
463
+ body: form
464
+ });
465
+ if (!res.ok) {
466
+ const text = await res.text().catch(() => "");
467
+ throw new Error(`Pinata v1 error ${res.status}: ${text}`);
468
+ }
469
+ const data = await res.json();
470
+ return `ipfs://${data.IpfsHash}`;
471
+ }
472
+ throw new Error("Set PINATA_JWT (preferred) or PINATA_API_KEY + PINATA_SECRET to pin to IPFS");
473
+ }
474
+
475
+ // src/ArcBountyAgent.ts
476
+ var arcTestnet = defineChain({
477
+ id: ARC_TESTNET_CHAIN_ID,
478
+ name: "Arc Testnet",
479
+ nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 6 },
480
+ rpcUrls: { default: { http: [ARC_TESTNET_RPC] } }
481
+ });
482
+ var ArcBountyAgent = class {
483
+ constructor(config) {
484
+ this._agentId = null;
485
+ const rpcUrl = config.rpcUrl ?? ARC_TESTNET_RPC;
486
+ this.chain = defineChain({ ...arcTestnet, rpcUrls: { default: { http: [rpcUrl] } } });
487
+ this.account = privateKeyToAccount(config.privateKey);
488
+ this.metadataURI = config.metadataURI ?? "";
489
+ const rawAdapter = config.bountyAdapterAddress ?? process.env["BOUNTY_ADAPTER_ADDRESS"];
490
+ if (!rawAdapter) {
491
+ throw new Error(
492
+ "ArcBountyAgent: bountyAdapterAddress is required (constructor option or BOUNTY_ADAPTER_ADDRESS env). See agent-sdk/.env.example. Source of truth: contracts/DEPLOYMENTS.md."
493
+ );
494
+ }
495
+ if (!isAddress(rawAdapter) || rawAdapter.toLowerCase() === ZERO_ADDRESS.toLowerCase()) {
496
+ throw new Error(`ArcBountyAgent: invalid bountyAdapterAddress: ${rawAdapter}`);
497
+ }
498
+ this.bountyAdapter = rawAdapter;
499
+ this.publicClient = createPublicClient({ chain: this.chain, transport: http(rpcUrl) });
500
+ this.walletClient = createWalletClient({ account: this.account, chain: this.chain, transport: http(rpcUrl) });
501
+ }
502
+ get address() {
503
+ return this.account.address;
504
+ }
505
+ // ─── Identity ───────────────────────────────────────────────────────────────
506
+ async register() {
507
+ const existing = await this._findExistingAgentId();
508
+ if (existing !== null) {
509
+ this._agentId = existing;
510
+ return existing;
511
+ }
512
+ const hash = await this.walletClient.writeContract({
513
+ address: CONTRACTS.IDENTITY_REGISTRY,
514
+ abi: IDENTITY_REGISTRY_ABI,
515
+ functionName: "register",
516
+ args: [this.metadataURI],
517
+ chain: null,
518
+ account: this.account
519
+ });
520
+ const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
521
+ const agentId = this._agentIdFromReceiptLogs(receipt.logs);
522
+ if (agentId === null) throw new Error("Registration succeeded but agentId not found in events");
523
+ this._agentId = agentId;
524
+ return agentId;
525
+ }
526
+ /** Pull the minted tokenId from a Transfer(from=0x0, to=self) log in a receipt. */
527
+ _agentIdFromReceiptLogs(logs) {
528
+ const TRANSFER_SIG = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
529
+ const me = this.account.address.toLowerCase().slice(2).padStart(64, "0");
530
+ for (const log of logs) {
531
+ if (log.address.toLowerCase() !== CONTRACTS.IDENTITY_REGISTRY.toLowerCase()) continue;
532
+ if (log.topics.length < 4) continue;
533
+ if (log.topics[0]?.toLowerCase() !== TRANSFER_SIG) continue;
534
+ if (log.topics[1]?.toLowerCase() !== "0x" + "0".repeat(64)) continue;
535
+ if (log.topics[2]?.toLowerCase() !== "0x" + me) continue;
536
+ return BigInt(log.topics[3]);
537
+ }
538
+ return null;
539
+ }
540
+ get agentId() {
541
+ if (this._agentId === null) throw new Error("Agent not registered. Call register() first.");
542
+ return this._agentId;
543
+ }
544
+ setAgentId(id) {
545
+ this._agentId = id;
546
+ }
547
+ // ─── Browse bounties ────────────────────────────────────────────────────────
548
+ async listOpenBounties(filter = {}) {
549
+ const {
550
+ category = "",
551
+ agentOnly,
552
+ humanOnly,
553
+ maxReward,
554
+ minReward,
555
+ offset = 0,
556
+ limit = 50
557
+ } = filter;
558
+ const jobIds = await this.publicClient.readContract({
559
+ address: this.bountyAdapter,
560
+ abi: BOUNTY_ADAPTER_ABI,
561
+ functionName: "getOpenBounties",
562
+ args: [category, BigInt(offset), BigInt(limit)]
563
+ });
564
+ const metas = await Promise.all(jobIds.map((jobId) => this.getBounty(jobId)));
565
+ return metas.filter((m) => {
566
+ if (agentOnly === true && !m.agentOnly) return false;
567
+ if (humanOnly === true && !m.humanOnly) return false;
568
+ if (agentOnly === false && m.agentOnly) return false;
569
+ if (humanOnly === false && m.humanOnly) return false;
570
+ if (maxReward !== void 0 && m.reward > this._parseUsdc(maxReward)) return false;
571
+ if (minReward !== void 0 && m.reward < this._parseUsdc(minReward)) return false;
572
+ return true;
573
+ });
574
+ }
575
+ async getBounty(jobId) {
576
+ const raw = await this.publicClient.readContract({
577
+ address: this.bountyAdapter,
578
+ abi: BOUNTY_ADAPTER_ABI,
579
+ functionName: "getBountyMeta",
580
+ args: [jobId]
581
+ });
582
+ return raw;
583
+ }
584
+ async getBountyDescription(jobId) {
585
+ const meta = await this.getBounty(jobId);
586
+ return fetchIpfsText(meta.ipfsDescHash);
587
+ }
588
+ async getMyBounties() {
589
+ const jobIds = await this.publicClient.readContract({
590
+ address: this.bountyAdapter,
591
+ abi: BOUNTY_ADAPTER_ABI,
592
+ functionName: "getMyAssignedBounties",
593
+ args: [this.address]
594
+ });
595
+ return Promise.all(jobIds.map((id) => this.getBounty(id)));
596
+ }
597
+ async getPostedBounties() {
598
+ const jobIds = await this.publicClient.readContract({
599
+ address: this.bountyAdapter,
600
+ abi: BOUNTY_ADAPTER_ABI,
601
+ functionName: "getMyPostedBounties",
602
+ args: [this.address]
603
+ });
604
+ return Promise.all(jobIds.map((id) => this.getBounty(id)));
605
+ }
606
+ // ─── Post a bounty ──────────────────────────────────────────────────────────
607
+ async createBounty(opts) {
608
+ if (opts.agentOnly && opts.humanOnly) {
609
+ throw new Error("agentOnly and humanOnly are mutually exclusive");
610
+ }
611
+ if (!opts.descriptionCid && !opts.descriptionText) {
612
+ throw new Error("Provide either descriptionCid or descriptionText");
613
+ }
614
+ const reward = this._parseUsdc(opts.rewardUsdc);
615
+ const deadline = this._resolveDeadline(opts.deadline);
616
+ const descCid = opts.descriptionCid ?? await pinText(opts.descriptionText);
617
+ await this._ensureUsdcAllowance(reward);
618
+ const hash = await this.walletClient.writeContract({
619
+ address: this.bountyAdapter,
620
+ abi: BOUNTY_ADAPTER_ABI,
621
+ functionName: "createBounty",
622
+ args: [{
623
+ provider: opts.provider ?? ZERO_ADDRESS,
624
+ reward,
625
+ deadline,
626
+ ipfsDescHash: descCid,
627
+ category: opts.category,
628
+ tags: opts.tags ?? [],
629
+ agentOnly: opts.agentOnly ?? false,
630
+ humanOnly: opts.humanOnly ?? false
631
+ }],
632
+ chain: null,
633
+ account: this.account
634
+ });
635
+ const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
636
+ let jobId;
637
+ for (const log of receipt.logs) {
638
+ if (log.address.toLowerCase() !== this.bountyAdapter.toLowerCase()) continue;
639
+ try {
640
+ const decoded = decodeEventLog({
641
+ abi: BOUNTY_ADAPTER_ABI,
642
+ data: log.data,
643
+ topics: log.topics
644
+ });
645
+ if (decoded.eventName === "BountyCreated") {
646
+ jobId = decoded.args.jobId;
647
+ break;
648
+ }
649
+ } catch {
650
+ }
651
+ }
652
+ return { hash, jobId };
653
+ }
654
+ // ─── Take / submit ──────────────────────────────────────────────────────────
655
+ async takeBounty(jobId) {
656
+ const agentId = this._agentId ?? 0n;
657
+ const hash = await this.walletClient.writeContract({
658
+ address: this.bountyAdapter,
659
+ abi: BOUNTY_ADAPTER_ABI,
660
+ functionName: "takeBounty",
661
+ args: [jobId, agentId],
662
+ chain: null,
663
+ account: this.account
664
+ });
665
+ await this._waitForTx(hash);
666
+ return { hash };
667
+ }
668
+ async submitWork(jobId, options) {
669
+ if (!options.text && !options.cid) {
670
+ throw new Error("Provide either text or cid");
671
+ }
672
+ const cid = options.cid ?? await pinText(options.text);
673
+ const hash = await this.walletClient.writeContract({
674
+ address: this.bountyAdapter,
675
+ abi: BOUNTY_ADAPTER_ABI,
676
+ functionName: "submitWork",
677
+ args: [jobId, cid],
678
+ chain: null,
679
+ account: this.account
680
+ });
681
+ await this._waitForTx(hash);
682
+ return { hash };
683
+ }
684
+ // ─── Poster cycle ───────────────────────────────────────────────────────────
685
+ // These let a protocol/DAO agent run the full poster side end-to-end.
686
+ /** Approve a submission and pay the worker. Records on-chain reputation. */
687
+ async approveBounty(jobId, reputationScore = 95) {
688
+ return this._writeAdapter("approveBounty", [jobId, reputationScore]);
689
+ }
690
+ /**
691
+ * Permissionless payout after APPROVAL_TIMEOUT (14d) from submission.
692
+ * Use this from a watchdog agent to unstick ghosted posters.
693
+ */
694
+ async autoApprove(jobId) {
695
+ return this._writeAdapter("autoApprove", [jobId]);
696
+ }
697
+ /** Propose rejection. Triggers a 48h challenge window for the worker. */
698
+ async rejectBounty(jobId, evidence) {
699
+ const cid = await this._resolveEvidenceCid(evidence);
700
+ return this._writeAdapter("rejectBounty", [jobId, cid]);
701
+ }
702
+ /** After the challenge window expires unchallenged, anyone may finalize. */
703
+ async finalizeRejection(jobId) {
704
+ return this._writeAdapter("finalizeRejection", [jobId]);
705
+ }
706
+ /** Cancel a bounty (only valid before takeBounty). Full USDC refund. */
707
+ async cancelBounty(jobId) {
708
+ return this._writeAdapter("cancelBounty", [jobId]);
709
+ }
710
+ /** Permissionless expiry after deadline. Refunds poster if no submission. */
711
+ async expireBounty(jobId) {
712
+ return this._writeAdapter("expireBounty", [jobId]);
713
+ }
714
+ /** Arbitrator-only ruling. `payProvider` true → worker wins, false → refund. */
715
+ async resolveDispute(jobId, payProvider, ruling, reputationPenalty = 0) {
716
+ const cid = await this._resolveEvidenceCid(ruling);
717
+ return this._writeAdapter("resolveDispute", [jobId, payProvider, cid, reputationPenalty]);
718
+ }
719
+ /** After 48h with no response, anyone may claim the default ruling. */
720
+ async claimDefaultRuling(jobId) {
721
+ return this._writeAdapter("claimDefaultRuling", [jobId]);
722
+ }
723
+ // ─── Dispute flow (worker-side) ─────────────────────────────────────────────
724
+ /** Worker challenges a pending rejection — flips bounty into dispute with worker as initiator. */
725
+ async challengeRejection(jobId, evidence) {
726
+ const cid = await this._resolveEvidenceCid(evidence);
727
+ const hash = await this.walletClient.writeContract({
728
+ address: this.bountyAdapter,
729
+ abi: BOUNTY_ADAPTER_ABI,
730
+ functionName: "challengeRejection",
731
+ args: [jobId, cid],
732
+ chain: null,
733
+ account: this.account
734
+ });
735
+ await this._waitForTx(hash);
736
+ return { hash };
737
+ }
738
+ /** Open a dispute (either party — after submission, before resolution). */
739
+ async disputeBounty(jobId, evidence) {
740
+ const cid = await this._resolveEvidenceCid(evidence);
741
+ const hash = await this.walletClient.writeContract({
742
+ address: this.bountyAdapter,
743
+ abi: BOUNTY_ADAPTER_ABI,
744
+ functionName: "disputeBounty",
745
+ args: [jobId, cid],
746
+ chain: null,
747
+ account: this.account
748
+ });
749
+ await this._waitForTx(hash);
750
+ return { hash };
751
+ }
752
+ /** Respond to an open dispute (only the non-initiator may call). */
753
+ async respondToDispute(jobId, evidence) {
754
+ const cid = await this._resolveEvidenceCid(evidence);
755
+ const hash = await this.walletClient.writeContract({
756
+ address: this.bountyAdapter,
757
+ abi: BOUNTY_ADAPTER_ABI,
758
+ functionName: "respondToDispute",
759
+ args: [jobId, cid],
760
+ chain: null,
761
+ account: this.account
762
+ });
763
+ await this._waitForTx(hash);
764
+ return { hash };
765
+ }
766
+ // ─── Expire stale bounties ──────────────────────────────────────────────────
767
+ async expireStale(category = "", limit = 100) {
768
+ const jobIds = await this.publicClient.readContract({
769
+ address: this.bountyAdapter,
770
+ abi: BOUNTY_ADAPTER_ABI,
771
+ functionName: "getOpenBounties",
772
+ args: [category, 0n, BigInt(limit)]
773
+ });
774
+ const now = BigInt(Math.floor(Date.now() / 1e3));
775
+ const expired = [];
776
+ for (const jobId of jobIds) {
777
+ const meta = await this.getBounty(jobId);
778
+ if (meta.deadline < now) {
779
+ try {
780
+ const hash = await this.walletClient.writeContract({
781
+ address: this.bountyAdapter,
782
+ abi: BOUNTY_ADAPTER_ABI,
783
+ functionName: "expireBounty",
784
+ args: [jobId],
785
+ chain: null,
786
+ account: this.account
787
+ });
788
+ await this._waitForTx(hash);
789
+ expired.push(jobId);
790
+ } catch {
791
+ }
792
+ }
793
+ }
794
+ return expired;
795
+ }
796
+ // ─── Reputation ─────────────────────────────────────────────────────────────
797
+ async getReputation(agentId) {
798
+ const id = agentId ?? this.agentId;
799
+ try {
800
+ const raw = await this.publicClient.readContract({
801
+ address: this.bountyAdapter,
802
+ abi: BOUNTY_ADAPTER_ABI,
803
+ functionName: "getAgentReputation",
804
+ args: [id]
805
+ });
806
+ return raw;
807
+ } catch {
808
+ return { averageScore: 0n, totalFeedbacks: 0n, totalJobs: 0n };
809
+ }
810
+ }
811
+ async getAgentInfo() {
812
+ const id = this.agentId;
813
+ const reputation = await this.getReputation(id);
814
+ return {
815
+ agentId: id,
816
+ address: this.address,
817
+ metadataURI: this.metadataURI,
818
+ reputation
819
+ };
820
+ }
821
+ // ─── USDC helpers ────────────────────────────────────────────────────────────
822
+ async usdcBalance() {
823
+ return this.publicClient.readContract({
824
+ address: CONTRACTS.USDC,
825
+ abi: ERC20_ABI,
826
+ functionName: "balanceOf",
827
+ args: [this.address]
828
+ });
829
+ }
830
+ formatUsdc(raw) {
831
+ return (Number(raw) / 10 ** USDC_DECIMALS).toFixed(2);
832
+ }
833
+ // ─── Event subscriptions ────────────────────────────────────────────────────
834
+ /**
835
+ * Watch `BountyCreated` events and invoke `onMatch` for each new bounty that
836
+ * passes the filter. Returns an `unwatch()` function — call it to stop.
837
+ *
838
+ * Idempotency: each jobId is delivered to `onMatch` at most once per process
839
+ * lifetime, even if the chain emits a duplicate event (re-org, RPC retry).
840
+ * If you need durable dedup across restarts, persist `seenJobIds` yourself.
841
+ */
842
+ subscribeToNewBounties(filter, onMatch) {
843
+ const seen = /* @__PURE__ */ new Set();
844
+ const unwatch = this.publicClient.watchContractEvent({
845
+ address: this.bountyAdapter,
846
+ abi: BOUNTY_ADAPTER_ABI,
847
+ eventName: "BountyCreated",
848
+ onLogs: async (logs) => {
849
+ for (const log of logs) {
850
+ const args = log.args;
851
+ const jobId = args?.jobId;
852
+ if (jobId === void 0) continue;
853
+ const key = jobId.toString();
854
+ if (seen.has(key)) continue;
855
+ seen.add(key);
856
+ try {
857
+ const meta = await this.getBounty(jobId);
858
+ if (!this._matchesFilter(meta, filter)) continue;
859
+ await onMatch(meta);
860
+ } catch (err) {
861
+ console.error(`[ArcBountyAgent] onMatch error for #${key}:`, err);
862
+ }
863
+ }
864
+ },
865
+ pollingInterval: 4e3
866
+ });
867
+ return unwatch;
868
+ }
869
+ _matchesFilter(m, f) {
870
+ if (f.category && m.category !== f.category) return false;
871
+ if (f.agentOnly === true && !m.agentOnly) return false;
872
+ if (f.humanOnly === true && !m.humanOnly) return false;
873
+ if (f.agentOnly === false && m.agentOnly) return false;
874
+ if (f.humanOnly === false && m.humanOnly) return false;
875
+ if (f.maxReward !== void 0 && m.reward > this._parseUsdc(f.maxReward)) return false;
876
+ if (f.minReward !== void 0 && m.reward < this._parseUsdc(f.minReward)) return false;
877
+ return true;
878
+ }
879
+ // ─── Autonomous loop ────────────────────────────────────────────────────────
880
+ async runOnce(filter, runTask) {
881
+ const bounties = await this.listOpenBounties(filter);
882
+ if (bounties.length === 0) return null;
883
+ const bounty = bounties[0];
884
+ console.log(`[ArcBountyAgent] Taking bounty #${bounty.jobId} ($${this.formatUsdc(bounty.reward)} USDC)`);
885
+ await this.takeBounty(bounty.jobId);
886
+ const description = await fetchIpfsText(bounty.ipfsDescHash);
887
+ console.log(`[ArcBountyAgent] Running task for bounty #${bounty.jobId}\u2026`);
888
+ const result = await runTask(description, bounty);
889
+ await this.submitWork(bounty.jobId, { text: result });
890
+ console.log(`[ArcBountyAgent] Work submitted for bounty #${bounty.jobId}. Waiting for approval.`);
891
+ return bounty.jobId;
892
+ }
893
+ // ─── Internal ───────────────────────────────────────────────────────────────
894
+ async _waitForTx(hash) {
895
+ await this.publicClient.waitForTransactionReceipt({ hash });
896
+ }
897
+ /**
898
+ * Write to BountyAdapter with the canonical (chain, account) tuple. All
899
+ * mutating helpers funnel through here so future changes (gas estimation,
900
+ * retry, paymaster) land in one place.
901
+ */
902
+ async _writeAdapter(functionName, args) {
903
+ const hash = await this.walletClient.writeContract({
904
+ address: this.bountyAdapter,
905
+ abi: BOUNTY_ADAPTER_ABI,
906
+ functionName,
907
+ args,
908
+ chain: null,
909
+ account: this.account
910
+ });
911
+ await this._waitForTx(hash);
912
+ return { hash };
913
+ }
914
+ /**
915
+ * Best-effort idempotency check: scan a bounded recent window for a
916
+ * Transfer(0x0 → self) on the registry. Arc's public RPC caps eth_getLogs to
917
+ * a 10,000-block range per call (confirmed empirically — a single wider
918
+ * request errors outright), so we page backward in 10k chunks up to a total
919
+ * lookback ceiling instead of issuing one oversized request. A chunk/network
920
+ * error aborts the whole scan and falls back to "register again", which is
921
+ * acceptable — worst case we mint a redundant identity, not lose data.
922
+ */
923
+ async _findExistingAgentId() {
924
+ const CHUNK = 10000n;
925
+ const TOTAL_LOOKBACK = 500000n;
926
+ try {
927
+ const head = await this.publicClient.getBlockNumber();
928
+ const floor = head > TOTAL_LOOKBACK ? head - TOTAL_LOOKBACK : 0n;
929
+ for (let to = head; to > floor; to -= CHUNK) {
930
+ const from = to - CHUNK + 1n > floor ? to - CHUNK + 1n : floor;
931
+ const logs = await this.publicClient.getLogs({
932
+ address: CONTRACTS.IDENTITY_REGISTRY,
933
+ event: IDENTITY_REGISTRY_ABI[2],
934
+ // Transfer event
935
+ args: { from: ZERO_ADDRESS, to: this.address },
936
+ fromBlock: from,
937
+ toBlock: to
938
+ });
939
+ if (logs.length > 0) {
940
+ const last = logs[logs.length - 1];
941
+ return last.args.tokenId;
942
+ }
943
+ if (from === floor) break;
944
+ }
945
+ return null;
946
+ } catch {
947
+ return null;
948
+ }
949
+ }
950
+ async _ensureUsdcAllowance(amount) {
951
+ const current = await this.publicClient.readContract({
952
+ address: CONTRACTS.USDC,
953
+ abi: ERC20_ABI,
954
+ functionName: "allowance",
955
+ args: [this.address, this.bountyAdapter]
956
+ });
957
+ if (current >= amount) return;
958
+ const hash = await this.walletClient.writeContract({
959
+ address: CONTRACTS.USDC,
960
+ abi: ERC20_ABI,
961
+ functionName: "approve",
962
+ args: [this.bountyAdapter, amount],
963
+ chain: null,
964
+ account: this.account
965
+ });
966
+ await this._waitForTx(hash);
967
+ }
968
+ async _resolveEvidenceCid(e) {
969
+ if (!e.text && !e.cid) throw new Error("Provide either text or cid");
970
+ return e.cid ?? await pinText(e.text);
971
+ }
972
+ _resolveDeadline(d) {
973
+ if (d instanceof Date) return BigInt(Math.floor(d.getTime() / 1e3));
974
+ if (d < 1e9) return BigInt(Math.floor(Date.now() / 1e3) + d);
975
+ return BigInt(d);
976
+ }
977
+ _parseUsdc(dollars) {
978
+ return BigInt(Math.round(dollars * 10 ** USDC_DECIMALS));
979
+ }
980
+ };
981
+
982
+ // src/metadata.ts
983
+ function validateAgentMetadata(m) {
984
+ if (typeof m !== "object" || m === null) throw new Error("metadata must be an object");
985
+ const o = m;
986
+ if (typeof o.name !== "string" || o.name.length === 0)
987
+ throw new Error("metadata.name must be a non-empty string");
988
+ if (typeof o.description !== "string")
989
+ throw new Error("metadata.description must be a string");
990
+ if (o.capabilities !== void 0 && !Array.isArray(o.capabilities))
991
+ throw new Error("metadata.capabilities must be an array of strings");
992
+ if (o.arcbounty !== void 0) {
993
+ const a = o.arcbounty;
994
+ if (a.min_reputation !== void 0 && (typeof a.min_reputation !== "number" || a.min_reputation < 0 || a.min_reputation > 100))
995
+ throw new Error("metadata.arcbounty.min_reputation must be a number in [0, 100]");
996
+ if (a.preferred_categories !== void 0) {
997
+ if (!Array.isArray(a.preferred_categories)) throw new Error("preferred_categories must be an array");
998
+ for (const c of a.preferred_categories) {
999
+ if (typeof c !== "string" || !["dev", "design", "content", "data", "other"].includes(c))
1000
+ throw new Error(`preferred_categories: invalid category ${String(c)}`);
1001
+ }
1002
+ }
1003
+ for (const k of ["min_reward_usdc", "max_reward_usdc"]) {
1004
+ const v = a[k];
1005
+ if (v !== void 0 && (typeof v !== "number" || v < 0)) {
1006
+ throw new Error(`metadata.arcbounty.${k} must be a non-negative number`);
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+ async function pinAgentMetadata(meta) {
1012
+ validateAgentMetadata(meta);
1013
+ return pinText(JSON.stringify(meta, null, 2), "agent.json");
1014
+ }
1015
+ export {
1016
+ ARC_TESTNET_CHAIN_ID,
1017
+ ARC_TESTNET_RPC,
1018
+ ArcBountyAgent,
1019
+ BOUNTY_ADAPTER_ABI,
1020
+ CONTRACTS,
1021
+ ERC20_ABI,
1022
+ IDENTITY_REGISTRY_ABI,
1023
+ fetchIpfsJson,
1024
+ fetchIpfsText,
1025
+ isPinningConfigured,
1026
+ pinAgentMetadata,
1027
+ pinText,
1028
+ validateAgentMetadata
1029
+ };