arcbounty-agent-sdk 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -2
- package/dist/index.d.mts +232 -11
- package/dist/index.d.ts +232 -11
- package/dist/index.js +392 -143
- package/dist/index.mjs +383 -139
- package/package.json +11 -3
- package/.env.example +0 -13
- package/examples/demo-agent.ts +0 -94
- package/src/ArcBountyAgent.ts +0 -645
- package/src/abi.ts +0 -386
- package/src/constants.ts +0 -20
- package/src/index.ts +0 -21
- package/src/ipfs.ts +0 -80
- package/src/metadata.ts +0 -69
- package/src/types.ts +0 -105
- package/tsconfig.json +0 -13
package/src/ArcBountyAgent.ts
DELETED
|
@@ -1,645 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createPublicClient,
|
|
3
|
-
createWalletClient,
|
|
4
|
-
decodeEventLog,
|
|
5
|
-
http,
|
|
6
|
-
defineChain,
|
|
7
|
-
isAddress,
|
|
8
|
-
type Address,
|
|
9
|
-
type Hash,
|
|
10
|
-
type PublicClient,
|
|
11
|
-
type WalletClient,
|
|
12
|
-
} from "viem";
|
|
13
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
14
|
-
import {
|
|
15
|
-
BOUNTY_ADAPTER_ABI,
|
|
16
|
-
IDENTITY_REGISTRY_ABI,
|
|
17
|
-
ERC20_ABI,
|
|
18
|
-
} from "./abi.js";
|
|
19
|
-
import {
|
|
20
|
-
ARC_TESTNET_RPC,
|
|
21
|
-
ARC_TESTNET_CHAIN_ID,
|
|
22
|
-
CONTRACTS,
|
|
23
|
-
USDC_DECIMALS,
|
|
24
|
-
ZERO_ADDRESS,
|
|
25
|
-
} from "./constants.js";
|
|
26
|
-
import { pinText, fetchIpfsText } from "./ipfs.js";
|
|
27
|
-
import type {
|
|
28
|
-
ArcBountyAgentConfig,
|
|
29
|
-
BountyMeta,
|
|
30
|
-
ReputationScore,
|
|
31
|
-
OpenBountiesFilter,
|
|
32
|
-
CreateBountyOptions,
|
|
33
|
-
SubmitWorkOptions,
|
|
34
|
-
DisputeEvidenceOptions,
|
|
35
|
-
AgentInfo,
|
|
36
|
-
TxResult,
|
|
37
|
-
} from "./types.js";
|
|
38
|
-
|
|
39
|
-
const arcTestnet = defineChain({
|
|
40
|
-
id: ARC_TESTNET_CHAIN_ID,
|
|
41
|
-
name: "Arc Testnet",
|
|
42
|
-
nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 6 },
|
|
43
|
-
rpcUrls: { default: { http: [ARC_TESTNET_RPC] } },
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
export class ArcBountyAgent {
|
|
47
|
-
private readonly publicClient: PublicClient;
|
|
48
|
-
private readonly walletClient: WalletClient;
|
|
49
|
-
private readonly account: ReturnType<typeof privateKeyToAccount>;
|
|
50
|
-
private readonly bountyAdapter: Address;
|
|
51
|
-
private readonly metadataURI: string;
|
|
52
|
-
private readonly chain: ReturnType<typeof defineChain>;
|
|
53
|
-
|
|
54
|
-
private _agentId: bigint | null = null;
|
|
55
|
-
|
|
56
|
-
constructor(config: ArcBountyAgentConfig) {
|
|
57
|
-
const rpcUrl = config.rpcUrl ?? ARC_TESTNET_RPC;
|
|
58
|
-
this.chain = defineChain({ ...arcTestnet, rpcUrls: { default: { http: [rpcUrl] } } });
|
|
59
|
-
|
|
60
|
-
this.account = privateKeyToAccount(config.privateKey);
|
|
61
|
-
this.metadataURI = config.metadataURI ?? "";
|
|
62
|
-
const rawAdapter = config.bountyAdapterAddress ??
|
|
63
|
-
(process.env["BOUNTY_ADAPTER_ADDRESS"] as Address | undefined);
|
|
64
|
-
if (!rawAdapter) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
"ArcBountyAgent: bountyAdapterAddress is required (constructor option or BOUNTY_ADAPTER_ADDRESS env). " +
|
|
67
|
-
"See agent-sdk/.env.example. Source of truth: contracts/DEPLOYMENTS.md.",
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
if (!isAddress(rawAdapter) || rawAdapter.toLowerCase() === ZERO_ADDRESS.toLowerCase()) {
|
|
71
|
-
throw new Error(`ArcBountyAgent: invalid bountyAdapterAddress: ${rawAdapter}`);
|
|
72
|
-
}
|
|
73
|
-
this.bountyAdapter = rawAdapter as Address;
|
|
74
|
-
|
|
75
|
-
this.publicClient = createPublicClient({ chain: this.chain, transport: http(rpcUrl) }) as PublicClient;
|
|
76
|
-
this.walletClient = createWalletClient({ account: this.account, chain: this.chain, transport: http(rpcUrl) });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
get address(): Address {
|
|
80
|
-
return this.account.address;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ─── Identity ───────────────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
async register(): Promise<bigint> {
|
|
86
|
-
const existing = await this._findExistingAgentId();
|
|
87
|
-
if (existing !== null) {
|
|
88
|
-
this._agentId = existing;
|
|
89
|
-
return existing;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const hash = await this.walletClient.writeContract({
|
|
93
|
-
address: CONTRACTS.IDENTITY_REGISTRY,
|
|
94
|
-
abi: IDENTITY_REGISTRY_ABI,
|
|
95
|
-
functionName: "register",
|
|
96
|
-
args: [this.metadataURI],
|
|
97
|
-
chain: null,
|
|
98
|
-
account: this.account,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Decode the agentId straight from the registration receipt — authoritative
|
|
102
|
-
// and avoids a wide getLogs scan that public RPCs reject on long chains.
|
|
103
|
-
const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
|
|
104
|
-
const agentId = this._agentIdFromReceiptLogs(receipt.logs);
|
|
105
|
-
if (agentId === null) throw new Error("Registration succeeded but agentId not found in events");
|
|
106
|
-
|
|
107
|
-
this._agentId = agentId;
|
|
108
|
-
return agentId;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Pull the minted tokenId from a Transfer(from=0x0, to=self) log in a receipt. */
|
|
112
|
-
private _agentIdFromReceiptLogs(logs: readonly { address: string; topics: readonly string[] }[]): bigint | null {
|
|
113
|
-
const TRANSFER_SIG = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
114
|
-
const me = this.account.address.toLowerCase().slice(2).padStart(64, "0");
|
|
115
|
-
for (const log of logs) {
|
|
116
|
-
if (log.address.toLowerCase() !== CONTRACTS.IDENTITY_REGISTRY.toLowerCase()) continue;
|
|
117
|
-
if (log.topics.length < 4) continue;
|
|
118
|
-
if (log.topics[0]?.toLowerCase() !== TRANSFER_SIG) continue;
|
|
119
|
-
if (log.topics[1]?.toLowerCase() !== "0x" + "0".repeat(64)) continue; // from == 0x0
|
|
120
|
-
if (log.topics[2]?.toLowerCase() !== "0x" + me) continue; // to == self
|
|
121
|
-
return BigInt(log.topics[3]!); // tokenId
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
get agentId(): bigint {
|
|
127
|
-
if (this._agentId === null) throw new Error("Agent not registered. Call register() first.");
|
|
128
|
-
return this._agentId;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
setAgentId(id: bigint): void {
|
|
132
|
-
this._agentId = id;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ─── Browse bounties ────────────────────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
async listOpenBounties(filter: OpenBountiesFilter = {}): Promise<BountyMeta[]> {
|
|
138
|
-
const {
|
|
139
|
-
category = "",
|
|
140
|
-
agentOnly,
|
|
141
|
-
humanOnly,
|
|
142
|
-
maxReward,
|
|
143
|
-
minReward,
|
|
144
|
-
offset = 0,
|
|
145
|
-
limit = 50,
|
|
146
|
-
} = filter;
|
|
147
|
-
|
|
148
|
-
const jobIds = await this.publicClient.readContract({
|
|
149
|
-
address: this.bountyAdapter,
|
|
150
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
151
|
-
functionName: "getOpenBounties",
|
|
152
|
-
args: [category, BigInt(offset), BigInt(limit)],
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const metas = await Promise.all(jobIds.map(jobId => this.getBounty(jobId)));
|
|
156
|
-
|
|
157
|
-
return metas.filter(m => {
|
|
158
|
-
if (agentOnly === true && !m.agentOnly) return false;
|
|
159
|
-
if (humanOnly === true && !m.humanOnly) return false;
|
|
160
|
-
if (agentOnly === false && m.agentOnly) return false;
|
|
161
|
-
if (humanOnly === false && m.humanOnly) return false;
|
|
162
|
-
if (maxReward !== undefined && m.reward > this._parseUsdc(maxReward)) return false;
|
|
163
|
-
if (minReward !== undefined && m.reward < this._parseUsdc(minReward)) return false;
|
|
164
|
-
return true;
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async getBounty(jobId: bigint): Promise<BountyMeta> {
|
|
169
|
-
const raw = await this.publicClient.readContract({
|
|
170
|
-
address: this.bountyAdapter,
|
|
171
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
172
|
-
functionName: "getBountyMeta",
|
|
173
|
-
args: [jobId],
|
|
174
|
-
});
|
|
175
|
-
return raw as unknown as BountyMeta;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async getBountyDescription(jobId: bigint): Promise<string> {
|
|
179
|
-
const meta = await this.getBounty(jobId);
|
|
180
|
-
return fetchIpfsText(meta.ipfsDescHash);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async getMyBounties(): Promise<BountyMeta[]> {
|
|
184
|
-
const jobIds = await this.publicClient.readContract({
|
|
185
|
-
address: this.bountyAdapter,
|
|
186
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
187
|
-
functionName: "getMyAssignedBounties",
|
|
188
|
-
args: [this.address],
|
|
189
|
-
});
|
|
190
|
-
return Promise.all(jobIds.map(id => this.getBounty(id)));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async getPostedBounties(): Promise<BountyMeta[]> {
|
|
194
|
-
const jobIds = await this.publicClient.readContract({
|
|
195
|
-
address: this.bountyAdapter,
|
|
196
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
197
|
-
functionName: "getMyPostedBounties",
|
|
198
|
-
args: [this.address],
|
|
199
|
-
});
|
|
200
|
-
return Promise.all(jobIds.map(id => this.getBounty(id)));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ─── Post a bounty ──────────────────────────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
async createBounty(opts: CreateBountyOptions): Promise<{ hash: Hash; jobId?: bigint }> {
|
|
206
|
-
if (opts.agentOnly && opts.humanOnly) {
|
|
207
|
-
throw new Error("agentOnly and humanOnly are mutually exclusive");
|
|
208
|
-
}
|
|
209
|
-
if (!opts.descriptionCid && !opts.descriptionText) {
|
|
210
|
-
throw new Error("Provide either descriptionCid or descriptionText");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const reward = this._parseUsdc(opts.rewardUsdc);
|
|
214
|
-
const deadline = this._resolveDeadline(opts.deadline);
|
|
215
|
-
const descCid = opts.descriptionCid ?? await pinText(opts.descriptionText!);
|
|
216
|
-
|
|
217
|
-
await this._ensureUsdcAllowance(reward);
|
|
218
|
-
|
|
219
|
-
const hash = await this.walletClient.writeContract({
|
|
220
|
-
address: this.bountyAdapter,
|
|
221
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
222
|
-
functionName: "createBounty",
|
|
223
|
-
args: [{
|
|
224
|
-
provider: opts.provider ?? ZERO_ADDRESS,
|
|
225
|
-
reward,
|
|
226
|
-
deadline,
|
|
227
|
-
ipfsDescHash: descCid,
|
|
228
|
-
category: opts.category,
|
|
229
|
-
tags: opts.tags ?? [],
|
|
230
|
-
agentOnly: opts.agentOnly ?? false,
|
|
231
|
-
humanOnly: opts.humanOnly ?? false,
|
|
232
|
-
}],
|
|
233
|
-
chain: null,
|
|
234
|
-
account: this.account,
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
|
|
238
|
-
let jobId: bigint | undefined;
|
|
239
|
-
for (const log of receipt.logs) {
|
|
240
|
-
if (log.address.toLowerCase() !== this.bountyAdapter.toLowerCase()) continue;
|
|
241
|
-
try {
|
|
242
|
-
const decoded = decodeEventLog({
|
|
243
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
244
|
-
data: log.data,
|
|
245
|
-
topics: log.topics,
|
|
246
|
-
});
|
|
247
|
-
if (decoded.eventName === "BountyCreated") {
|
|
248
|
-
jobId = (decoded.args as { jobId: bigint }).jobId;
|
|
249
|
-
break;
|
|
250
|
-
}
|
|
251
|
-
} catch {
|
|
252
|
-
// not a BountyAdapter event we know about
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return { hash, jobId };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ─── Take / submit ──────────────────────────────────────────────────────────
|
|
259
|
-
|
|
260
|
-
async takeBounty(jobId: bigint): Promise<TxResult> {
|
|
261
|
-
const agentId = this._agentId ?? 0n;
|
|
262
|
-
const hash = await this.walletClient.writeContract({
|
|
263
|
-
address: this.bountyAdapter,
|
|
264
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
265
|
-
functionName: "takeBounty",
|
|
266
|
-
args: [jobId, agentId],
|
|
267
|
-
chain: null,
|
|
268
|
-
account: this.account,
|
|
269
|
-
});
|
|
270
|
-
await this._waitForTx(hash);
|
|
271
|
-
return { hash };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async submitWork(jobId: bigint, options: SubmitWorkOptions): Promise<TxResult> {
|
|
275
|
-
if (!options.text && !options.cid) {
|
|
276
|
-
throw new Error("Provide either text or cid");
|
|
277
|
-
}
|
|
278
|
-
const cid = options.cid ?? await pinText(options.text!);
|
|
279
|
-
|
|
280
|
-
const hash = await this.walletClient.writeContract({
|
|
281
|
-
address: this.bountyAdapter,
|
|
282
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
283
|
-
functionName: "submitWork",
|
|
284
|
-
args: [jobId, cid],
|
|
285
|
-
chain: null,
|
|
286
|
-
account: this.account,
|
|
287
|
-
});
|
|
288
|
-
await this._waitForTx(hash);
|
|
289
|
-
return { hash };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// ─── Poster cycle ───────────────────────────────────────────────────────────
|
|
293
|
-
// These let a protocol/DAO agent run the full poster side end-to-end.
|
|
294
|
-
|
|
295
|
-
/** Approve a submission and pay the worker. Records on-chain reputation. */
|
|
296
|
-
async approveBounty(jobId: bigint, reputationScore = 95): Promise<TxResult> {
|
|
297
|
-
return this._writeAdapter("approveBounty", [jobId, reputationScore]);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Permissionless payout after APPROVAL_TIMEOUT (14d) from submission.
|
|
302
|
-
* Use this from a watchdog agent to unstick ghosted posters.
|
|
303
|
-
*/
|
|
304
|
-
async autoApprove(jobId: bigint): Promise<TxResult> {
|
|
305
|
-
return this._writeAdapter("autoApprove", [jobId]);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** Propose rejection. Triggers a 48h challenge window for the worker. */
|
|
309
|
-
async rejectBounty(jobId: bigint, evidence: DisputeEvidenceOptions): Promise<TxResult> {
|
|
310
|
-
const cid = await this._resolveEvidenceCid(evidence);
|
|
311
|
-
return this._writeAdapter("rejectBounty", [jobId, cid]);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** After the challenge window expires unchallenged, anyone may finalize. */
|
|
315
|
-
async finalizeRejection(jobId: bigint): Promise<TxResult> {
|
|
316
|
-
return this._writeAdapter("finalizeRejection", [jobId]);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Cancel a bounty (only valid before takeBounty). Full USDC refund. */
|
|
320
|
-
async cancelBounty(jobId: bigint): Promise<TxResult> {
|
|
321
|
-
return this._writeAdapter("cancelBounty", [jobId]);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** Permissionless expiry after deadline. Refunds poster if no submission. */
|
|
325
|
-
async expireBounty(jobId: bigint): Promise<TxResult> {
|
|
326
|
-
return this._writeAdapter("expireBounty", [jobId]);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/** Arbitrator-only ruling. `payProvider` true → worker wins, false → refund. */
|
|
330
|
-
async resolveDispute(
|
|
331
|
-
jobId: bigint,
|
|
332
|
-
payProvider: boolean,
|
|
333
|
-
ruling: DisputeEvidenceOptions,
|
|
334
|
-
reputationPenalty = 0,
|
|
335
|
-
): Promise<TxResult> {
|
|
336
|
-
const cid = await this._resolveEvidenceCid(ruling);
|
|
337
|
-
return this._writeAdapter("resolveDispute", [jobId, payProvider, cid, reputationPenalty]);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/** After 48h with no response, anyone may claim the default ruling. */
|
|
341
|
-
async claimDefaultRuling(jobId: bigint): Promise<TxResult> {
|
|
342
|
-
return this._writeAdapter("claimDefaultRuling", [jobId]);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// ─── Dispute flow (worker-side) ─────────────────────────────────────────────
|
|
346
|
-
|
|
347
|
-
/** Worker challenges a pending rejection — flips bounty into dispute with worker as initiator. */
|
|
348
|
-
async challengeRejection(jobId: bigint, evidence: DisputeEvidenceOptions): Promise<TxResult> {
|
|
349
|
-
const cid = await this._resolveEvidenceCid(evidence);
|
|
350
|
-
const hash = await this.walletClient.writeContract({
|
|
351
|
-
address: this.bountyAdapter,
|
|
352
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
353
|
-
functionName: "challengeRejection",
|
|
354
|
-
args: [jobId, cid],
|
|
355
|
-
chain: null,
|
|
356
|
-
account: this.account,
|
|
357
|
-
});
|
|
358
|
-
await this._waitForTx(hash);
|
|
359
|
-
return { hash };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/** Open a dispute (either party — after submission, before resolution). */
|
|
363
|
-
async disputeBounty(jobId: bigint, evidence: DisputeEvidenceOptions): Promise<TxResult> {
|
|
364
|
-
const cid = await this._resolveEvidenceCid(evidence);
|
|
365
|
-
const hash = await this.walletClient.writeContract({
|
|
366
|
-
address: this.bountyAdapter,
|
|
367
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
368
|
-
functionName: "disputeBounty",
|
|
369
|
-
args: [jobId, cid],
|
|
370
|
-
chain: null,
|
|
371
|
-
account: this.account,
|
|
372
|
-
});
|
|
373
|
-
await this._waitForTx(hash);
|
|
374
|
-
return { hash };
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/** Respond to an open dispute (only the non-initiator may call). */
|
|
378
|
-
async respondToDispute(jobId: bigint, evidence: DisputeEvidenceOptions): Promise<TxResult> {
|
|
379
|
-
const cid = await this._resolveEvidenceCid(evidence);
|
|
380
|
-
const hash = await this.walletClient.writeContract({
|
|
381
|
-
address: this.bountyAdapter,
|
|
382
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
383
|
-
functionName: "respondToDispute",
|
|
384
|
-
args: [jobId, cid],
|
|
385
|
-
chain: null,
|
|
386
|
-
account: this.account,
|
|
387
|
-
});
|
|
388
|
-
await this._waitForTx(hash);
|
|
389
|
-
return { hash };
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ─── Expire stale bounties ──────────────────────────────────────────────────
|
|
393
|
-
|
|
394
|
-
async expireStale(category = "", limit = 100): Promise<bigint[]> {
|
|
395
|
-
const jobIds = await this.publicClient.readContract({
|
|
396
|
-
address: this.bountyAdapter,
|
|
397
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
398
|
-
functionName: "getOpenBounties",
|
|
399
|
-
args: [category, 0n, BigInt(limit)],
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
403
|
-
const expired: bigint[] = [];
|
|
404
|
-
|
|
405
|
-
for (const jobId of jobIds) {
|
|
406
|
-
const meta = await this.getBounty(jobId);
|
|
407
|
-
if (meta.deadline < now) {
|
|
408
|
-
try {
|
|
409
|
-
const hash = await this.walletClient.writeContract({
|
|
410
|
-
address: this.bountyAdapter,
|
|
411
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
412
|
-
functionName: "expireBounty",
|
|
413
|
-
args: [jobId],
|
|
414
|
-
chain: null,
|
|
415
|
-
account: this.account,
|
|
416
|
-
});
|
|
417
|
-
await this._waitForTx(hash);
|
|
418
|
-
expired.push(jobId);
|
|
419
|
-
} catch {
|
|
420
|
-
// already expired or other error — skip
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return expired;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// ─── Reputation ─────────────────────────────────────────────────────────────
|
|
428
|
-
|
|
429
|
-
async getReputation(agentId?: bigint): Promise<ReputationScore> {
|
|
430
|
-
const id = agentId ?? this.agentId;
|
|
431
|
-
try {
|
|
432
|
-
const raw = await this.publicClient.readContract({
|
|
433
|
-
address: this.bountyAdapter,
|
|
434
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
435
|
-
functionName: "getAgentReputation",
|
|
436
|
-
args: [id],
|
|
437
|
-
});
|
|
438
|
-
return raw as ReputationScore;
|
|
439
|
-
} catch {
|
|
440
|
-
// The live Arc ReputationRegistry reverts for an agent with no feedback
|
|
441
|
-
// yet (freshly registered, zero completed jobs). Treat as a clean slate.
|
|
442
|
-
return { averageScore: 0n, totalFeedbacks: 0n, totalJobs: 0n };
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
async getAgentInfo(): Promise<AgentInfo> {
|
|
447
|
-
const id = this.agentId;
|
|
448
|
-
const reputation = await this.getReputation(id);
|
|
449
|
-
return {
|
|
450
|
-
agentId: id,
|
|
451
|
-
address: this.address,
|
|
452
|
-
metadataURI: this.metadataURI,
|
|
453
|
-
reputation,
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// ─── USDC helpers ────────────────────────────────────────────────────────────
|
|
458
|
-
|
|
459
|
-
async usdcBalance(): Promise<bigint> {
|
|
460
|
-
return this.publicClient.readContract({
|
|
461
|
-
address: CONTRACTS.USDC,
|
|
462
|
-
abi: ERC20_ABI,
|
|
463
|
-
functionName: "balanceOf",
|
|
464
|
-
args: [this.address],
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
formatUsdc(raw: bigint): string {
|
|
469
|
-
return (Number(raw) / 10 ** USDC_DECIMALS).toFixed(2);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// ─── Event subscriptions ────────────────────────────────────────────────────
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Watch `BountyCreated` events and invoke `onMatch` for each new bounty that
|
|
476
|
-
* passes the filter. Returns an `unwatch()` function — call it to stop.
|
|
477
|
-
*
|
|
478
|
-
* Idempotency: each jobId is delivered to `onMatch` at most once per process
|
|
479
|
-
* lifetime, even if the chain emits a duplicate event (re-org, RPC retry).
|
|
480
|
-
* If you need durable dedup across restarts, persist `seenJobIds` yourself.
|
|
481
|
-
*/
|
|
482
|
-
subscribeToNewBounties(
|
|
483
|
-
filter: OpenBountiesFilter,
|
|
484
|
-
onMatch: (meta: BountyMeta) => void | Promise<void>,
|
|
485
|
-
): () => void {
|
|
486
|
-
const seen = new Set<string>();
|
|
487
|
-
const unwatch = this.publicClient.watchContractEvent({
|
|
488
|
-
address: this.bountyAdapter,
|
|
489
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
490
|
-
eventName: "BountyCreated",
|
|
491
|
-
onLogs: async logs => {
|
|
492
|
-
for (const log of logs) {
|
|
493
|
-
const args = (log as { args?: { jobId?: bigint } }).args;
|
|
494
|
-
const jobId = args?.jobId;
|
|
495
|
-
if (jobId === undefined) continue;
|
|
496
|
-
const key = jobId.toString();
|
|
497
|
-
if (seen.has(key)) continue;
|
|
498
|
-
seen.add(key);
|
|
499
|
-
try {
|
|
500
|
-
const meta = await this.getBounty(jobId);
|
|
501
|
-
if (!this._matchesFilter(meta, filter)) continue;
|
|
502
|
-
await onMatch(meta);
|
|
503
|
-
} catch (err) {
|
|
504
|
-
// Swallow per-event errors so one bad bounty doesn't kill the loop.
|
|
505
|
-
console.error(`[ArcBountyAgent] onMatch error for #${key}:`, err);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
},
|
|
509
|
-
pollingInterval: 4_000,
|
|
510
|
-
});
|
|
511
|
-
return unwatch;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
private _matchesFilter(m: BountyMeta, f: OpenBountiesFilter): boolean {
|
|
515
|
-
if (f.category && m.category !== f.category) return false;
|
|
516
|
-
if (f.agentOnly === true && !m.agentOnly) return false;
|
|
517
|
-
if (f.humanOnly === true && !m.humanOnly) return false;
|
|
518
|
-
if (f.agentOnly === false && m.agentOnly) return false;
|
|
519
|
-
if (f.humanOnly === false && m.humanOnly) return false;
|
|
520
|
-
if (f.maxReward !== undefined && m.reward > this._parseUsdc(f.maxReward)) return false;
|
|
521
|
-
if (f.minReward !== undefined && m.reward < this._parseUsdc(f.minReward)) return false;
|
|
522
|
-
return true;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// ─── Autonomous loop ────────────────────────────────────────────────────────
|
|
526
|
-
|
|
527
|
-
async runOnce(
|
|
528
|
-
filter: OpenBountiesFilter,
|
|
529
|
-
runTask: (description: string, meta: BountyMeta) => Promise<string>
|
|
530
|
-
): Promise<bigint | null> {
|
|
531
|
-
const bounties = await this.listOpenBounties(filter);
|
|
532
|
-
if (bounties.length === 0) return null;
|
|
533
|
-
|
|
534
|
-
const bounty = bounties[0]!;
|
|
535
|
-
console.log(`[ArcBountyAgent] Taking bounty #${bounty.jobId} ($${this.formatUsdc(bounty.reward)} USDC)`);
|
|
536
|
-
|
|
537
|
-
await this.takeBounty(bounty.jobId);
|
|
538
|
-
|
|
539
|
-
const description = await fetchIpfsText(bounty.ipfsDescHash);
|
|
540
|
-
console.log(`[ArcBountyAgent] Running task for bounty #${bounty.jobId}…`);
|
|
541
|
-
|
|
542
|
-
const result = await runTask(description, bounty);
|
|
543
|
-
await this.submitWork(bounty.jobId, { text: result });
|
|
544
|
-
|
|
545
|
-
console.log(`[ArcBountyAgent] Work submitted for bounty #${bounty.jobId}. Waiting for approval.`);
|
|
546
|
-
return bounty.jobId;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// ─── Internal ───────────────────────────────────────────────────────────────
|
|
550
|
-
|
|
551
|
-
private async _waitForTx(hash: Hash): Promise<void> {
|
|
552
|
-
await this.publicClient.waitForTransactionReceipt({ hash });
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Write to BountyAdapter with the canonical (chain, account) tuple. All
|
|
557
|
-
* mutating helpers funnel through here so future changes (gas estimation,
|
|
558
|
-
* retry, paymaster) land in one place.
|
|
559
|
-
*/
|
|
560
|
-
private async _writeAdapter(functionName: string, args: readonly unknown[]): Promise<TxResult> {
|
|
561
|
-
const hash = await this.walletClient.writeContract({
|
|
562
|
-
address: this.bountyAdapter,
|
|
563
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
564
|
-
functionName: functionName as never,
|
|
565
|
-
args: args as never,
|
|
566
|
-
chain: null,
|
|
567
|
-
account: this.account,
|
|
568
|
-
});
|
|
569
|
-
await this._waitForTx(hash);
|
|
570
|
-
return { hash };
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Best-effort idempotency check: scan a bounded recent window for a
|
|
575
|
-
* Transfer(0x0 → self) on the registry. Arc's public RPC caps eth_getLogs to
|
|
576
|
-
* a 10,000-block range per call (confirmed empirically — a single wider
|
|
577
|
-
* request errors outright), so we page backward in 10k chunks up to a total
|
|
578
|
-
* lookback ceiling instead of issuing one oversized request. A chunk/network
|
|
579
|
-
* error aborts the whole scan and falls back to "register again", which is
|
|
580
|
-
* acceptable — worst case we mint a redundant identity, not lose data.
|
|
581
|
-
*/
|
|
582
|
-
private async _findExistingAgentId(): Promise<bigint | null> {
|
|
583
|
-
const CHUNK = 10_000n; // Arc RPC's actual eth_getLogs range cap.
|
|
584
|
-
const TOTAL_LOOKBACK = 500_000n;
|
|
585
|
-
try {
|
|
586
|
-
const head = await this.publicClient.getBlockNumber();
|
|
587
|
-
const floor = head > TOTAL_LOOKBACK ? head - TOTAL_LOOKBACK : 0n;
|
|
588
|
-
|
|
589
|
-
for (let to = head; to > floor; to -= CHUNK) {
|
|
590
|
-
const from = to - CHUNK + 1n > floor ? to - CHUNK + 1n : floor;
|
|
591
|
-
const logs = await this.publicClient.getLogs({
|
|
592
|
-
address: CONTRACTS.IDENTITY_REGISTRY,
|
|
593
|
-
event: IDENTITY_REGISTRY_ABI[2], // Transfer event
|
|
594
|
-
args: { from: ZERO_ADDRESS, to: this.address },
|
|
595
|
-
fromBlock: from,
|
|
596
|
-
toBlock: to,
|
|
597
|
-
});
|
|
598
|
-
if (logs.length > 0) {
|
|
599
|
-
const last = logs[logs.length - 1]!;
|
|
600
|
-
return (last.args as { tokenId: bigint }).tokenId;
|
|
601
|
-
}
|
|
602
|
-
if (from === floor) break;
|
|
603
|
-
}
|
|
604
|
-
return null;
|
|
605
|
-
} catch {
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
private async _ensureUsdcAllowance(amount: bigint): Promise<void> {
|
|
611
|
-
const current = await this.publicClient.readContract({
|
|
612
|
-
address: CONTRACTS.USDC,
|
|
613
|
-
abi: ERC20_ABI,
|
|
614
|
-
functionName: "allowance",
|
|
615
|
-
args: [this.address, this.bountyAdapter],
|
|
616
|
-
});
|
|
617
|
-
if (current >= amount) return;
|
|
618
|
-
|
|
619
|
-
const hash = await this.walletClient.writeContract({
|
|
620
|
-
address: CONTRACTS.USDC,
|
|
621
|
-
abi: ERC20_ABI,
|
|
622
|
-
functionName: "approve",
|
|
623
|
-
args: [this.bountyAdapter, amount],
|
|
624
|
-
chain: null,
|
|
625
|
-
account: this.account,
|
|
626
|
-
});
|
|
627
|
-
await this._waitForTx(hash);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
private async _resolveEvidenceCid(e: DisputeEvidenceOptions): Promise<string> {
|
|
631
|
-
if (!e.text && !e.cid) throw new Error("Provide either text or cid");
|
|
632
|
-
return e.cid ?? await pinText(e.text!);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
private _resolveDeadline(d: number | Date): bigint {
|
|
636
|
-
if (d instanceof Date) return BigInt(Math.floor(d.getTime() / 1000));
|
|
637
|
-
// < 1e9 is interpreted as duration-in-seconds from now (~30 years cutoff)
|
|
638
|
-
if (d < 1_000_000_000) return BigInt(Math.floor(Date.now() / 1000) + d);
|
|
639
|
-
return BigInt(d);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
private _parseUsdc(dollars: number): bigint {
|
|
643
|
-
return BigInt(Math.round(dollars * 10 ** USDC_DECIMALS));
|
|
644
|
-
}
|
|
645
|
-
}
|