arcbounty-agent-sdk 0.1.0 → 0.3.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/README.md +126 -0
- package/dist/index.d.mts +220 -11
- package/dist/index.d.ts +220 -11
- package/dist/index.js +362 -141
- package/dist/index.mjs +358 -138
- 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/dist/index.mjs
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// src/ArcBountyAgent.ts
|
|
2
2
|
import {
|
|
3
3
|
createPublicClient,
|
|
4
|
-
createWalletClient,
|
|
5
4
|
decodeEventLog,
|
|
6
|
-
http,
|
|
5
|
+
http as http2,
|
|
7
6
|
defineChain,
|
|
8
7
|
isAddress
|
|
9
8
|
} from "viem";
|
|
10
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
11
9
|
|
|
12
10
|
// src/abi.ts
|
|
13
11
|
var BOUNTY_META_TUPLE = {
|
|
@@ -37,7 +35,9 @@ var BOUNTY_META_TUPLE = {
|
|
|
37
35
|
{ name: "disputeRaisedAt", type: "uint256" },
|
|
38
36
|
{ name: "disputeReasonHash", type: "string" },
|
|
39
37
|
{ name: "disputeResponseHash", type: "string" },
|
|
40
|
-
{ name: "disputeRulingHash", type: "string" }
|
|
38
|
+
{ name: "disputeRulingHash", type: "string" },
|
|
39
|
+
{ name: "requireWorkerBond", type: "bool" },
|
|
40
|
+
{ name: "workerBond", type: "uint256" }
|
|
41
41
|
]
|
|
42
42
|
};
|
|
43
43
|
var BOUNTY_ADAPTER_ABI = [
|
|
@@ -57,7 +57,8 @@ var BOUNTY_ADAPTER_ABI = [
|
|
|
57
57
|
{ name: "category", type: "string" },
|
|
58
58
|
{ name: "tags", type: "string[]" },
|
|
59
59
|
{ name: "agentOnly", type: "bool" },
|
|
60
|
-
{ name: "humanOnly", type: "bool" }
|
|
60
|
+
{ name: "humanOnly", type: "bool" },
|
|
61
|
+
{ name: "requireWorkerBond", type: "bool" }
|
|
61
62
|
]
|
|
62
63
|
}],
|
|
63
64
|
outputs: [{ name: "jobId", type: "uint256" }]
|
|
@@ -179,7 +180,27 @@ var BOUNTY_ADAPTER_ABI = [
|
|
|
179
180
|
inputs: [{ name: "jobId", type: "uint256" }],
|
|
180
181
|
outputs: []
|
|
181
182
|
},
|
|
183
|
+
{
|
|
184
|
+
// V3.3 — permissionless neutral 50/50 split if the arbitrator never rules
|
|
185
|
+
// after both parties have submitted evidence (claimDefaultRuling doesn't
|
|
186
|
+
// apply once a response exists). See ARBITRATOR_TIMEOUT (30d).
|
|
187
|
+
name: "claimArbitratorTimeout",
|
|
188
|
+
type: "function",
|
|
189
|
+
stateMutability: "nonpayable",
|
|
190
|
+
inputs: [{ name: "jobId", type: "uint256" }],
|
|
191
|
+
outputs: []
|
|
192
|
+
},
|
|
182
193
|
// ── Read ──
|
|
194
|
+
{
|
|
195
|
+
// Public array getter — needed to scan the full bounty set (expireStale,
|
|
196
|
+
// keeper-style scripts). Not paginated on-chain; callers should bound
|
|
197
|
+
// their own scan via totalBounties().
|
|
198
|
+
name: "allJobIds",
|
|
199
|
+
type: "function",
|
|
200
|
+
stateMutability: "view",
|
|
201
|
+
inputs: [{ name: "", type: "uint256" }],
|
|
202
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
203
|
+
},
|
|
183
204
|
{
|
|
184
205
|
name: "getOpenBounties",
|
|
185
206
|
type: "function",
|
|
@@ -269,6 +290,34 @@ var BOUNTY_ADAPTER_ABI = [
|
|
|
269
290
|
inputs: [],
|
|
270
291
|
outputs: [{ name: "", type: "uint256" }]
|
|
271
292
|
},
|
|
293
|
+
{
|
|
294
|
+
name: "ARBITRATOR_TIMEOUT",
|
|
295
|
+
type: "function",
|
|
296
|
+
stateMutability: "view",
|
|
297
|
+
inputs: [],
|
|
298
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "uniquePosterCount",
|
|
302
|
+
type: "function",
|
|
303
|
+
stateMutability: "view",
|
|
304
|
+
inputs: [{ name: "agentId", type: "uint256" }],
|
|
305
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "WORKER_BOND_BPS",
|
|
309
|
+
type: "function",
|
|
310
|
+
stateMutability: "view",
|
|
311
|
+
inputs: [],
|
|
312
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: "MIN_WORKER_BOND",
|
|
316
|
+
type: "function",
|
|
317
|
+
stateMutability: "view",
|
|
318
|
+
inputs: [],
|
|
319
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
320
|
+
},
|
|
272
321
|
// ── Events ──
|
|
273
322
|
{
|
|
274
323
|
name: "BountyCreated",
|
|
@@ -335,6 +384,42 @@ var BOUNTY_ADAPTER_ABI = [
|
|
|
335
384
|
{ name: "rulingHash", type: "string", indexed: false },
|
|
336
385
|
{ name: "defaultRuling", type: "bool", indexed: false }
|
|
337
386
|
]
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "ArbitratorTimeoutClaimed",
|
|
390
|
+
type: "event",
|
|
391
|
+
inputs: [
|
|
392
|
+
{ name: "jobId", type: "uint256", indexed: true },
|
|
393
|
+
{ name: "posterAmount", type: "uint256", indexed: false },
|
|
394
|
+
{ name: "providerAmount", type: "uint256", indexed: false }
|
|
395
|
+
]
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "WorkerBondPosted",
|
|
399
|
+
type: "event",
|
|
400
|
+
inputs: [
|
|
401
|
+
{ name: "jobId", type: "uint256", indexed: true },
|
|
402
|
+
{ name: "worker", type: "address", indexed: true },
|
|
403
|
+
{ name: "amount", type: "uint256", indexed: false }
|
|
404
|
+
]
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: "WorkerBondRefunded",
|
|
408
|
+
type: "event",
|
|
409
|
+
inputs: [
|
|
410
|
+
{ name: "jobId", type: "uint256", indexed: true },
|
|
411
|
+
{ name: "worker", type: "address", indexed: true },
|
|
412
|
+
{ name: "amount", type: "uint256", indexed: false }
|
|
413
|
+
]
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: "WorkerBondForfeited",
|
|
417
|
+
type: "event",
|
|
418
|
+
inputs: [
|
|
419
|
+
{ name: "jobId", type: "uint256", indexed: true },
|
|
420
|
+
{ name: "poster", type: "address", indexed: true },
|
|
421
|
+
{ name: "amount", type: "uint256", indexed: false }
|
|
422
|
+
]
|
|
338
423
|
}
|
|
339
424
|
];
|
|
340
425
|
var IDENTITY_REGISTRY_ABI = [
|
|
@@ -392,6 +477,61 @@ var ERC20_ABI = [
|
|
|
392
477
|
}
|
|
393
478
|
];
|
|
394
479
|
|
|
480
|
+
// src/signers/viemSigner.ts
|
|
481
|
+
import { createWalletClient, http } from "viem";
|
|
482
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
483
|
+
var ViemSigner = class {
|
|
484
|
+
constructor(privateKey, chain, rpcUrl) {
|
|
485
|
+
this.account = privateKeyToAccount(privateKey);
|
|
486
|
+
this.address = this.account.address;
|
|
487
|
+
this.walletClient = createWalletClient({ account: this.account, chain, transport: http(rpcUrl) });
|
|
488
|
+
}
|
|
489
|
+
async writeContract(params) {
|
|
490
|
+
return this.walletClient.writeContract({
|
|
491
|
+
address: params.address,
|
|
492
|
+
abi: params.abi,
|
|
493
|
+
functionName: params.functionName,
|
|
494
|
+
args: params.args,
|
|
495
|
+
chain: null,
|
|
496
|
+
account: this.account
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// src/signers/circleSigner.ts
|
|
502
|
+
import { encodeFunctionData } from "viem";
|
|
503
|
+
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
|
|
504
|
+
var CircleSigner = class {
|
|
505
|
+
constructor(config) {
|
|
506
|
+
this.address = config.address;
|
|
507
|
+
this.walletId = config.walletId;
|
|
508
|
+
this.client = initiateDeveloperControlledWalletsClient({
|
|
509
|
+
apiKey: config.apiKey,
|
|
510
|
+
entitySecret: config.entitySecret,
|
|
511
|
+
baseUrl: config.baseUrl
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async writeContract(params) {
|
|
515
|
+
const callData = encodeFunctionData({
|
|
516
|
+
abi: params.abi,
|
|
517
|
+
functionName: params.functionName,
|
|
518
|
+
args: params.args
|
|
519
|
+
});
|
|
520
|
+
const created = await this.client.createContractExecutionTransaction({
|
|
521
|
+
walletId: this.walletId,
|
|
522
|
+
contractAddress: params.address,
|
|
523
|
+
callData,
|
|
524
|
+
fee: { type: "level", config: { feeLevel: "MEDIUM" } }
|
|
525
|
+
});
|
|
526
|
+
const txId = created.data?.id;
|
|
527
|
+
if (!txId) {
|
|
528
|
+
throw new Error("CircleSigner: createContractExecutionTransaction did not return a transaction id");
|
|
529
|
+
}
|
|
530
|
+
const result = await this.client.getTransaction({ id: txId, waitForTxHash: true });
|
|
531
|
+
return result.data.transaction.txHash;
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
395
535
|
// src/constants.ts
|
|
396
536
|
var ARC_TESTNET_RPC = "https://rpc.testnet.arc.network";
|
|
397
537
|
var ARC_TESTNET_CHAIN_ID = 5042002;
|
|
@@ -472,6 +612,40 @@ async function pinText(content, filename = "result.md") {
|
|
|
472
612
|
throw new Error("Set PINATA_JWT (preferred) or PINATA_API_KEY + PINATA_SECRET to pin to IPFS");
|
|
473
613
|
}
|
|
474
614
|
|
|
615
|
+
// src/logic.ts
|
|
616
|
+
function parseUsdc(dollars) {
|
|
617
|
+
return BigInt(Math.round(dollars * 10 ** USDC_DECIMALS));
|
|
618
|
+
}
|
|
619
|
+
function resolveDeadline(d, nowSec = Math.floor(Date.now() / 1e3)) {
|
|
620
|
+
if (d instanceof Date) return BigInt(Math.floor(d.getTime() / 1e3));
|
|
621
|
+
if (d < 1e9) return BigInt(nowSec + d);
|
|
622
|
+
return BigInt(d);
|
|
623
|
+
}
|
|
624
|
+
function matchesBountyFilter(m, f) {
|
|
625
|
+
if (f.category && m.category !== f.category) return false;
|
|
626
|
+
if (f.agentOnly === true && !m.agentOnly) return false;
|
|
627
|
+
if (f.humanOnly === true && !m.humanOnly) return false;
|
|
628
|
+
if (f.agentOnly === false && m.agentOnly) return false;
|
|
629
|
+
if (f.humanOnly === false && m.humanOnly) return false;
|
|
630
|
+
if (f.maxReward !== void 0 && m.reward > parseUsdc(f.maxReward)) return false;
|
|
631
|
+
if (f.minReward !== void 0 && m.reward < parseUsdc(f.minReward)) return false;
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
var TRANSFER_SIG = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
635
|
+
var ZERO_TOPIC = "0x" + "0".repeat(64);
|
|
636
|
+
function agentIdFromReceiptLogs(logs, registryAddress, selfAddress) {
|
|
637
|
+
const me = selfAddress.toLowerCase().slice(2).padStart(64, "0");
|
|
638
|
+
for (const log of logs) {
|
|
639
|
+
if (log.address.toLowerCase() !== registryAddress.toLowerCase()) continue;
|
|
640
|
+
if (log.topics.length < 4) continue;
|
|
641
|
+
if (log.topics[0]?.toLowerCase() !== TRANSFER_SIG) continue;
|
|
642
|
+
if (log.topics[1]?.toLowerCase() !== ZERO_TOPIC) continue;
|
|
643
|
+
if (log.topics[2]?.toLowerCase() !== "0x" + me) continue;
|
|
644
|
+
return BigInt(log.topics[3]);
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
475
649
|
// src/ArcBountyAgent.ts
|
|
476
650
|
var arcTestnet = defineChain({
|
|
477
651
|
id: ARC_TESTNET_CHAIN_ID,
|
|
@@ -484,7 +658,7 @@ var ArcBountyAgent = class {
|
|
|
484
658
|
this._agentId = null;
|
|
485
659
|
const rpcUrl = config.rpcUrl ?? ARC_TESTNET_RPC;
|
|
486
660
|
this.chain = defineChain({ ...arcTestnet, rpcUrls: { default: { http: [rpcUrl] } } });
|
|
487
|
-
this.
|
|
661
|
+
this.signer = config.circleWallet ? new CircleSigner(config.circleWallet) : new ViemSigner(config.privateKey, this.chain, rpcUrl);
|
|
488
662
|
this.metadataURI = config.metadataURI ?? "";
|
|
489
663
|
const rawAdapter = config.bountyAdapterAddress ?? process.env["BOUNTY_ADAPTER_ADDRESS"];
|
|
490
664
|
if (!rawAdapter) {
|
|
@@ -496,11 +670,10 @@ var ArcBountyAgent = class {
|
|
|
496
670
|
throw new Error(`ArcBountyAgent: invalid bountyAdapterAddress: ${rawAdapter}`);
|
|
497
671
|
}
|
|
498
672
|
this.bountyAdapter = rawAdapter;
|
|
499
|
-
this.publicClient = createPublicClient({ chain: this.chain, transport:
|
|
500
|
-
this.walletClient = createWalletClient({ account: this.account, chain: this.chain, transport: http(rpcUrl) });
|
|
673
|
+
this.publicClient = createPublicClient({ chain: this.chain, transport: http2(rpcUrl) });
|
|
501
674
|
}
|
|
502
675
|
get address() {
|
|
503
|
-
return this.
|
|
676
|
+
return this.signer.address;
|
|
504
677
|
}
|
|
505
678
|
// ─── Identity ───────────────────────────────────────────────────────────────
|
|
506
679
|
async register() {
|
|
@@ -509,34 +682,18 @@ var ArcBountyAgent = class {
|
|
|
509
682
|
this._agentId = existing;
|
|
510
683
|
return existing;
|
|
511
684
|
}
|
|
512
|
-
const hash = await this.
|
|
685
|
+
const hash = await this.signer.writeContract({
|
|
513
686
|
address: CONTRACTS.IDENTITY_REGISTRY,
|
|
514
687
|
abi: IDENTITY_REGISTRY_ABI,
|
|
515
688
|
functionName: "register",
|
|
516
|
-
args: [this.metadataURI]
|
|
517
|
-
chain: null,
|
|
518
|
-
account: this.account
|
|
689
|
+
args: [this.metadataURI]
|
|
519
690
|
});
|
|
520
691
|
const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
|
|
521
|
-
const agentId =
|
|
692
|
+
const agentId = agentIdFromReceiptLogs(receipt.logs, CONTRACTS.IDENTITY_REGISTRY, this.signer.address);
|
|
522
693
|
if (agentId === null) throw new Error("Registration succeeded but agentId not found in events");
|
|
523
694
|
this._agentId = agentId;
|
|
524
695
|
return agentId;
|
|
525
696
|
}
|
|
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
697
|
get agentId() {
|
|
541
698
|
if (this._agentId === null) throw new Error("Agent not registered. Call register() first.");
|
|
542
699
|
return this._agentId;
|
|
@@ -562,15 +719,7 @@ var ArcBountyAgent = class {
|
|
|
562
719
|
args: [category, BigInt(offset), BigInt(limit)]
|
|
563
720
|
});
|
|
564
721
|
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
|
-
});
|
|
722
|
+
return metas.filter((m) => matchesBountyFilter(m, { agentOnly, humanOnly, maxReward, minReward }));
|
|
574
723
|
}
|
|
575
724
|
async getBounty(jobId) {
|
|
576
725
|
const raw = await this.publicClient.readContract({
|
|
@@ -611,11 +760,11 @@ var ArcBountyAgent = class {
|
|
|
611
760
|
if (!opts.descriptionCid && !opts.descriptionText) {
|
|
612
761
|
throw new Error("Provide either descriptionCid or descriptionText");
|
|
613
762
|
}
|
|
614
|
-
const reward =
|
|
615
|
-
const deadline =
|
|
763
|
+
const reward = parseUsdc(opts.rewardUsdc);
|
|
764
|
+
const deadline = resolveDeadline(opts.deadline);
|
|
616
765
|
const descCid = opts.descriptionCid ?? await pinText(opts.descriptionText);
|
|
617
766
|
await this._ensureUsdcAllowance(reward);
|
|
618
|
-
const hash = await this.
|
|
767
|
+
const hash = await this.signer.writeContract({
|
|
619
768
|
address: this.bountyAdapter,
|
|
620
769
|
abi: BOUNTY_ADAPTER_ABI,
|
|
621
770
|
functionName: "createBounty",
|
|
@@ -627,10 +776,9 @@ var ArcBountyAgent = class {
|
|
|
627
776
|
category: opts.category,
|
|
628
777
|
tags: opts.tags ?? [],
|
|
629
778
|
agentOnly: opts.agentOnly ?? false,
|
|
630
|
-
humanOnly: opts.humanOnly ?? false
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
account: this.account
|
|
779
|
+
humanOnly: opts.humanOnly ?? false,
|
|
780
|
+
requireWorkerBond: opts.requireWorkerBond ?? false
|
|
781
|
+
}]
|
|
634
782
|
});
|
|
635
783
|
const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
|
|
636
784
|
let jobId;
|
|
@@ -654,32 +802,14 @@ var ArcBountyAgent = class {
|
|
|
654
802
|
// ─── Take / submit ──────────────────────────────────────────────────────────
|
|
655
803
|
async takeBounty(jobId) {
|
|
656
804
|
const agentId = this._agentId ?? 0n;
|
|
657
|
-
|
|
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 };
|
|
805
|
+
return this._writeAdapter("takeBounty", [jobId, agentId]);
|
|
667
806
|
}
|
|
668
807
|
async submitWork(jobId, options) {
|
|
669
808
|
if (!options.text && !options.cid) {
|
|
670
809
|
throw new Error("Provide either text or cid");
|
|
671
810
|
}
|
|
672
811
|
const cid = options.cid ?? await pinText(options.text);
|
|
673
|
-
|
|
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 };
|
|
812
|
+
return this._writeAdapter("submitWork", [jobId, cid]);
|
|
683
813
|
}
|
|
684
814
|
// ─── Poster cycle ───────────────────────────────────────────────────────────
|
|
685
815
|
// These let a protocol/DAO agent run the full poster side end-to-end.
|
|
@@ -720,75 +850,67 @@ var ArcBountyAgent = class {
|
|
|
720
850
|
async claimDefaultRuling(jobId) {
|
|
721
851
|
return this._writeAdapter("claimDefaultRuling", [jobId]);
|
|
722
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* V3.3 liveness fallback: if the respondent DID reply (so claimDefaultRuling
|
|
855
|
+
* no longer applies) but the arbitrator never called resolveDispute within
|
|
856
|
+
* ARBITRATOR_TIMEOUT (30d) of disputeRaisedAt, anyone may trigger a neutral
|
|
857
|
+
* 50/50 split between poster and worker. No reputation penalty either way.
|
|
858
|
+
*/
|
|
859
|
+
async claimArbitratorTimeout(jobId) {
|
|
860
|
+
return this._writeAdapter("claimArbitratorTimeout", [jobId]);
|
|
861
|
+
}
|
|
723
862
|
// ─── Dispute flow (worker-side) ─────────────────────────────────────────────
|
|
724
863
|
/** Worker challenges a pending rejection — flips bounty into dispute with worker as initiator. */
|
|
725
864
|
async challengeRejection(jobId, evidence) {
|
|
726
865
|
const cid = await this._resolveEvidenceCid(evidence);
|
|
727
|
-
|
|
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 };
|
|
866
|
+
return this._writeAdapter("challengeRejection", [jobId, cid]);
|
|
737
867
|
}
|
|
738
868
|
/** Open a dispute (either party — after submission, before resolution). */
|
|
739
869
|
async disputeBounty(jobId, evidence) {
|
|
740
870
|
const cid = await this._resolveEvidenceCid(evidence);
|
|
741
|
-
|
|
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 };
|
|
871
|
+
return this._writeAdapter("disputeBounty", [jobId, cid]);
|
|
751
872
|
}
|
|
752
873
|
/** Respond to an open dispute (only the non-initiator may call). */
|
|
753
874
|
async respondToDispute(jobId, evidence) {
|
|
754
875
|
const cid = await this._resolveEvidenceCid(evidence);
|
|
755
|
-
|
|
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 };
|
|
876
|
+
return this._writeAdapter("respondToDispute", [jobId, cid]);
|
|
765
877
|
}
|
|
766
878
|
// ─── Expire stale bounties ──────────────────────────────────────────────────
|
|
879
|
+
/**
|
|
880
|
+
* Scans the full bounty set and calls expireBounty() on anything past its
|
|
881
|
+
* deadline with no submission and not yet resolved. Stops after finding
|
|
882
|
+
* `limit` candidates to expire.
|
|
883
|
+
*
|
|
884
|
+
* NOTE: `getOpenBounties` (used pre-V3.3) can NEVER return a candidate for
|
|
885
|
+
* this — it excludes any bounty whose deadline has already passed by
|
|
886
|
+
* definition (`_isOpenMatch` checks `block.timestamp <= deadline`). This
|
|
887
|
+
* scan walks `allJobIds` directly instead, mirroring the keeper cron route
|
|
888
|
+
* (`frontend/app/api/cron/keeper/route.ts`).
|
|
889
|
+
*/
|
|
767
890
|
async expireStale(category = "", limit = 100) {
|
|
768
|
-
const
|
|
891
|
+
const total = await this.publicClient.readContract({
|
|
769
892
|
address: this.bountyAdapter,
|
|
770
893
|
abi: BOUNTY_ADAPTER_ABI,
|
|
771
|
-
functionName: "
|
|
772
|
-
args: [category, 0n, BigInt(limit)]
|
|
894
|
+
functionName: "totalBounties"
|
|
773
895
|
});
|
|
774
896
|
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
775
897
|
const expired = [];
|
|
776
|
-
for (
|
|
898
|
+
for (let i = 0n; i < total && expired.length < limit; i++) {
|
|
899
|
+
const jobId = await this.publicClient.readContract({
|
|
900
|
+
address: this.bountyAdapter,
|
|
901
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
902
|
+
functionName: "allJobIds",
|
|
903
|
+
args: [i]
|
|
904
|
+
});
|
|
777
905
|
const meta = await this.getBounty(jobId);
|
|
778
|
-
if (meta.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
account: this.account
|
|
787
|
-
});
|
|
788
|
-
await this._waitForTx(hash);
|
|
789
|
-
expired.push(jobId);
|
|
790
|
-
} catch {
|
|
791
|
-
}
|
|
906
|
+
if (meta.resolved) continue;
|
|
907
|
+
if (meta.submittedResultHash.length > 0) continue;
|
|
908
|
+
if (category && meta.category !== category) continue;
|
|
909
|
+
if (meta.deadline >= now) continue;
|
|
910
|
+
try {
|
|
911
|
+
await this._writeAdapter("expireBounty", [jobId]);
|
|
912
|
+
expired.push(jobId);
|
|
913
|
+
} catch {
|
|
792
914
|
}
|
|
793
915
|
}
|
|
794
916
|
return expired;
|
|
@@ -808,6 +930,21 @@ var ArcBountyAgent = class {
|
|
|
808
930
|
return { averageScore: 0n, totalFeedbacks: 0n, totalJobs: 0n };
|
|
809
931
|
}
|
|
810
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* V4 anti-Sybil signal: count of distinct posters who've actually paid out
|
|
935
|
+
* a completed bounty to this agent. Costs N real funded wallets to fake N —
|
|
936
|
+
* unlike the raw ERC-8004 average score, which one alt account can inflate
|
|
937
|
+
* for a few cents. See V4_DESIGN_ANTI_SYBIL.md.
|
|
938
|
+
*/
|
|
939
|
+
async getUniquePosterCount(agentId) {
|
|
940
|
+
const id = agentId ?? this.agentId;
|
|
941
|
+
return this.publicClient.readContract({
|
|
942
|
+
address: this.bountyAdapter,
|
|
943
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
944
|
+
functionName: "uniquePosterCount",
|
|
945
|
+
args: [id]
|
|
946
|
+
});
|
|
947
|
+
}
|
|
811
948
|
async getAgentInfo() {
|
|
812
949
|
const id = this.agentId;
|
|
813
950
|
const reputation = await this.getReputation(id);
|
|
@@ -855,7 +992,7 @@ var ArcBountyAgent = class {
|
|
|
855
992
|
seen.add(key);
|
|
856
993
|
try {
|
|
857
994
|
const meta = await this.getBounty(jobId);
|
|
858
|
-
if (!
|
|
995
|
+
if (!matchesBountyFilter(meta, filter)) continue;
|
|
859
996
|
await onMatch(meta);
|
|
860
997
|
} catch (err) {
|
|
861
998
|
console.error(`[ArcBountyAgent] onMatch error for #${key}:`, err);
|
|
@@ -866,15 +1003,110 @@ var ArcBountyAgent = class {
|
|
|
866
1003
|
});
|
|
867
1004
|
return unwatch;
|
|
868
1005
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1006
|
+
// ─── Dispute watchdog (self-protection) ─────────────────────────────────────
|
|
1007
|
+
/**
|
|
1008
|
+
* Background watchdog over this agent's own assigned bounties. An agent
|
|
1009
|
+
* that only calls `takeBounty`/`submitWork` and then goes idle is exposed
|
|
1010
|
+
* to every counterparty-controlled window in the contract: a poster can
|
|
1011
|
+
* reject a correct submission (48h to challenge), open a dispute the agent
|
|
1012
|
+
* never responds to (48h to respond, then the *other* side wins by
|
|
1013
|
+
* default), or the agent may simply be owed a payout nobody triggered yet
|
|
1014
|
+
* (14d autoApprove / 30d claimArbitratorTimeout). `protect()` polls
|
|
1015
|
+
* `getMyBounties()` and reacts automatically:
|
|
1016
|
+
*
|
|
1017
|
+
* - **Pending rejection, not yet challenged** → calls `onRejection` (if
|
|
1018
|
+
* provided) for evidence and calls `challengeRejection`. Without a
|
|
1019
|
+
* callback, a rejection is only logged, never auto-challenged — silently
|
|
1020
|
+
* auto-disputing every rejection would be its own failure mode.
|
|
1021
|
+
* - **Dispute raised by the other party, not yet responded** → calls
|
|
1022
|
+
* `onDisputeAgainstMe` for evidence and calls `respondToDispute`. Same
|
|
1023
|
+
* caveat: no callback means log-only.
|
|
1024
|
+
* - **Dispute resolved-by-response but arbitrator never ruled (30d)** →
|
|
1025
|
+
* calls `claimArbitratorTimeout` automatically (permissionless, no
|
|
1026
|
+
* evidence needed — this just unsticks the agent's own frozen funds).
|
|
1027
|
+
* - **Submitted, approval window elapsed (14d), poster silent** → calls
|
|
1028
|
+
* `autoApprove` automatically.
|
|
1029
|
+
*
|
|
1030
|
+
* Returns an `unwatch()` function. Errors on any single bounty are logged
|
|
1031
|
+
* and swallowed so one bad case can't kill the whole watchdog.
|
|
1032
|
+
*/
|
|
1033
|
+
protect(options = {}) {
|
|
1034
|
+
const pollingIntervalMs = options.pollingIntervalMs ?? 6e4;
|
|
1035
|
+
let stopped = false;
|
|
1036
|
+
const tick = async () => {
|
|
1037
|
+
if (stopped) return;
|
|
1038
|
+
try {
|
|
1039
|
+
await this._protectOnce(options);
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
console.error("[ArcBountyAgent.protect] tick error:", err);
|
|
1042
|
+
}
|
|
1043
|
+
if (!stopped) timer = setTimeout(tick, pollingIntervalMs);
|
|
1044
|
+
};
|
|
1045
|
+
let timer = setTimeout(tick, 0);
|
|
1046
|
+
return () => {
|
|
1047
|
+
stopped = true;
|
|
1048
|
+
clearTimeout(timer);
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
async _protectOnce(options) {
|
|
1052
|
+
const [rejectionWindow, disputeWindow, approvalTimeout, arbitratorTimeout] = await Promise.all([
|
|
1053
|
+
this.publicClient.readContract({
|
|
1054
|
+
address: this.bountyAdapter,
|
|
1055
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
1056
|
+
functionName: "REJECTION_CHALLENGE_WINDOW"
|
|
1057
|
+
}),
|
|
1058
|
+
this.publicClient.readContract({
|
|
1059
|
+
address: this.bountyAdapter,
|
|
1060
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
1061
|
+
functionName: "DISPUTE_RESPONSE_WINDOW"
|
|
1062
|
+
}),
|
|
1063
|
+
this.publicClient.readContract({
|
|
1064
|
+
address: this.bountyAdapter,
|
|
1065
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
1066
|
+
functionName: "APPROVAL_TIMEOUT"
|
|
1067
|
+
}),
|
|
1068
|
+
this.publicClient.readContract({
|
|
1069
|
+
address: this.bountyAdapter,
|
|
1070
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
1071
|
+
functionName: "ARBITRATOR_TIMEOUT"
|
|
1072
|
+
})
|
|
1073
|
+
]);
|
|
1074
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
1075
|
+
const mine = await this.getMyBounties();
|
|
1076
|
+
for (const meta of mine) {
|
|
1077
|
+
if (meta.resolved) continue;
|
|
1078
|
+
try {
|
|
1079
|
+
if (meta.rejectedAt > 0n && !meta.inDispute && now <= meta.rejectedAt + rejectionWindow) {
|
|
1080
|
+
options.onEvent?.("rejection_pending", meta);
|
|
1081
|
+
if (options.onRejection) {
|
|
1082
|
+
const evidence = await options.onRejection(meta);
|
|
1083
|
+
await this.challengeRejection(meta.jobId, evidence);
|
|
1084
|
+
options.onEvent?.("rejection_challenged", meta);
|
|
1085
|
+
}
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (meta.inDispute && meta.disputeResponseHash.length === 0 && meta.disputeInitiator.toLowerCase() !== this.address.toLowerCase() && now <= meta.disputeRaisedAt + disputeWindow) {
|
|
1089
|
+
options.onEvent?.("dispute_needs_response", meta);
|
|
1090
|
+
if (options.onDisputeAgainstMe) {
|
|
1091
|
+
const evidence = await options.onDisputeAgainstMe(meta);
|
|
1092
|
+
await this.respondToDispute(meta.jobId, evidence);
|
|
1093
|
+
options.onEvent?.("dispute_responded", meta);
|
|
1094
|
+
}
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
if (meta.inDispute && meta.disputeResponseHash.length > 0 && now > meta.disputeRaisedAt + arbitratorTimeout) {
|
|
1098
|
+
await this.claimArbitratorTimeout(meta.jobId);
|
|
1099
|
+
options.onEvent?.("arbitrator_timeout_claimed", meta);
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
if (meta.submittedAt > 0n && meta.rejectedAt === 0n && !meta.inDispute && now > meta.submittedAt + approvalTimeout) {
|
|
1103
|
+
await this.autoApprove(meta.jobId);
|
|
1104
|
+
options.onEvent?.("auto_approved", meta);
|
|
1105
|
+
}
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
console.error(`[ArcBountyAgent.protect] error handling bounty #${meta.jobId}:`, err);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
878
1110
|
}
|
|
879
1111
|
// ─── Autonomous loop ────────────────────────────────────────────────────────
|
|
880
1112
|
async runOnce(filter, runTask) {
|
|
@@ -900,13 +1132,11 @@ var ArcBountyAgent = class {
|
|
|
900
1132
|
* retry, paymaster) land in one place.
|
|
901
1133
|
*/
|
|
902
1134
|
async _writeAdapter(functionName, args) {
|
|
903
|
-
const hash = await this.
|
|
1135
|
+
const hash = await this.signer.writeContract({
|
|
904
1136
|
address: this.bountyAdapter,
|
|
905
1137
|
abi: BOUNTY_ADAPTER_ABI,
|
|
906
1138
|
functionName,
|
|
907
|
-
args
|
|
908
|
-
chain: null,
|
|
909
|
-
account: this.account
|
|
1139
|
+
args
|
|
910
1140
|
});
|
|
911
1141
|
await this._waitForTx(hash);
|
|
912
1142
|
return { hash };
|
|
@@ -955,13 +1185,11 @@ var ArcBountyAgent = class {
|
|
|
955
1185
|
args: [this.address, this.bountyAdapter]
|
|
956
1186
|
});
|
|
957
1187
|
if (current >= amount) return;
|
|
958
|
-
const hash = await this.
|
|
1188
|
+
const hash = await this.signer.writeContract({
|
|
959
1189
|
address: CONTRACTS.USDC,
|
|
960
1190
|
abi: ERC20_ABI,
|
|
961
1191
|
functionName: "approve",
|
|
962
|
-
args: [this.bountyAdapter, amount]
|
|
963
|
-
chain: null,
|
|
964
|
-
account: this.account
|
|
1192
|
+
args: [this.bountyAdapter, amount]
|
|
965
1193
|
});
|
|
966
1194
|
await this._waitForTx(hash);
|
|
967
1195
|
}
|
|
@@ -969,14 +1197,6 @@ var ArcBountyAgent = class {
|
|
|
969
1197
|
if (!e.text && !e.cid) throw new Error("Provide either text or cid");
|
|
970
1198
|
return e.cid ?? await pinText(e.text);
|
|
971
1199
|
}
|
|
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
1200
|
};
|
|
981
1201
|
|
|
982
1202
|
// src/metadata.ts
|