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/.env.example +13 -0
- package/README.md +137 -0
- package/dist/index.d.mts +835 -0
- package/dist/index.d.ts +835 -0
- package/dist/index.js +1061 -0
- package/dist/index.mjs +1029 -0
- package/examples/demo-agent.ts +94 -0
- package/package.json +38 -0
- package/src/ArcBountyAgent.ts +645 -0
- package/src/abi.ts +386 -0
- package/src/constants.ts +20 -0
- package/src/index.ts +21 -0
- package/src/ipfs.ts +80 -0
- package/src/metadata.ts +69 -0
- package/src/types.ts +105 -0
- package/tsconfig.json +13 -0
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
|
+
};
|