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/dist/index.js
CHANGED
|
@@ -30,15 +30,18 @@ __export(index_exports, {
|
|
|
30
30
|
fetchIpfsJson: () => fetchIpfsJson,
|
|
31
31
|
fetchIpfsText: () => fetchIpfsText,
|
|
32
32
|
isPinningConfigured: () => isPinningConfigured,
|
|
33
|
+
matchesBountyFilter: () => matchesBountyFilter,
|
|
34
|
+
parseUsdc: () => parseUsdc,
|
|
33
35
|
pinAgentMetadata: () => pinAgentMetadata,
|
|
34
36
|
pinText: () => pinText,
|
|
35
|
-
|
|
37
|
+
resolveDeadline: () => resolveDeadline,
|
|
38
|
+
validateAgentMetadata: () => validateAgentMetadata,
|
|
39
|
+
workerBondFor: () => workerBondFor
|
|
36
40
|
});
|
|
37
41
|
module.exports = __toCommonJS(index_exports);
|
|
38
42
|
|
|
39
43
|
// src/ArcBountyAgent.ts
|
|
40
|
-
var
|
|
41
|
-
var import_accounts = require("viem/accounts");
|
|
44
|
+
var import_viem3 = require("viem");
|
|
42
45
|
|
|
43
46
|
// src/abi.ts
|
|
44
47
|
var BOUNTY_META_TUPLE = {
|
|
@@ -68,7 +71,9 @@ var BOUNTY_META_TUPLE = {
|
|
|
68
71
|
{ name: "disputeRaisedAt", type: "uint256" },
|
|
69
72
|
{ name: "disputeReasonHash", type: "string" },
|
|
70
73
|
{ name: "disputeResponseHash", type: "string" },
|
|
71
|
-
{ name: "disputeRulingHash", type: "string" }
|
|
74
|
+
{ name: "disputeRulingHash", type: "string" },
|
|
75
|
+
{ name: "requireWorkerBond", type: "bool" },
|
|
76
|
+
{ name: "workerBond", type: "uint256" }
|
|
72
77
|
]
|
|
73
78
|
};
|
|
74
79
|
var BOUNTY_ADAPTER_ABI = [
|
|
@@ -88,7 +93,8 @@ var BOUNTY_ADAPTER_ABI = [
|
|
|
88
93
|
{ name: "category", type: "string" },
|
|
89
94
|
{ name: "tags", type: "string[]" },
|
|
90
95
|
{ name: "agentOnly", type: "bool" },
|
|
91
|
-
{ name: "humanOnly", type: "bool" }
|
|
96
|
+
{ name: "humanOnly", type: "bool" },
|
|
97
|
+
{ name: "requireWorkerBond", type: "bool" }
|
|
92
98
|
]
|
|
93
99
|
}],
|
|
94
100
|
outputs: [{ name: "jobId", type: "uint256" }]
|
|
@@ -210,7 +216,27 @@ var BOUNTY_ADAPTER_ABI = [
|
|
|
210
216
|
inputs: [{ name: "jobId", type: "uint256" }],
|
|
211
217
|
outputs: []
|
|
212
218
|
},
|
|
219
|
+
{
|
|
220
|
+
// V3.3 — permissionless neutral 50/50 split if the arbitrator never rules
|
|
221
|
+
// after both parties have submitted evidence (claimDefaultRuling doesn't
|
|
222
|
+
// apply once a response exists). See ARBITRATOR_TIMEOUT (30d).
|
|
223
|
+
name: "claimArbitratorTimeout",
|
|
224
|
+
type: "function",
|
|
225
|
+
stateMutability: "nonpayable",
|
|
226
|
+
inputs: [{ name: "jobId", type: "uint256" }],
|
|
227
|
+
outputs: []
|
|
228
|
+
},
|
|
213
229
|
// ── Read ──
|
|
230
|
+
{
|
|
231
|
+
// Public array getter — needed to scan the full bounty set (expireStale,
|
|
232
|
+
// keeper-style scripts). Not paginated on-chain; callers should bound
|
|
233
|
+
// their own scan via totalBounties().
|
|
234
|
+
name: "allJobIds",
|
|
235
|
+
type: "function",
|
|
236
|
+
stateMutability: "view",
|
|
237
|
+
inputs: [{ name: "", type: "uint256" }],
|
|
238
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
239
|
+
},
|
|
214
240
|
{
|
|
215
241
|
name: "getOpenBounties",
|
|
216
242
|
type: "function",
|
|
@@ -300,6 +326,34 @@ var BOUNTY_ADAPTER_ABI = [
|
|
|
300
326
|
inputs: [],
|
|
301
327
|
outputs: [{ name: "", type: "uint256" }]
|
|
302
328
|
},
|
|
329
|
+
{
|
|
330
|
+
name: "ARBITRATOR_TIMEOUT",
|
|
331
|
+
type: "function",
|
|
332
|
+
stateMutability: "view",
|
|
333
|
+
inputs: [],
|
|
334
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "uniquePosterCount",
|
|
338
|
+
type: "function",
|
|
339
|
+
stateMutability: "view",
|
|
340
|
+
inputs: [{ name: "agentId", type: "uint256" }],
|
|
341
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: "WORKER_BOND_BPS",
|
|
345
|
+
type: "function",
|
|
346
|
+
stateMutability: "view",
|
|
347
|
+
inputs: [],
|
|
348
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "MIN_WORKER_BOND",
|
|
352
|
+
type: "function",
|
|
353
|
+
stateMutability: "view",
|
|
354
|
+
inputs: [],
|
|
355
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
356
|
+
},
|
|
303
357
|
// ── Events ──
|
|
304
358
|
{
|
|
305
359
|
name: "BountyCreated",
|
|
@@ -366,6 +420,42 @@ var BOUNTY_ADAPTER_ABI = [
|
|
|
366
420
|
{ name: "rulingHash", type: "string", indexed: false },
|
|
367
421
|
{ name: "defaultRuling", type: "bool", indexed: false }
|
|
368
422
|
]
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "ArbitratorTimeoutClaimed",
|
|
426
|
+
type: "event",
|
|
427
|
+
inputs: [
|
|
428
|
+
{ name: "jobId", type: "uint256", indexed: true },
|
|
429
|
+
{ name: "posterAmount", type: "uint256", indexed: false },
|
|
430
|
+
{ name: "providerAmount", type: "uint256", indexed: false }
|
|
431
|
+
]
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "WorkerBondPosted",
|
|
435
|
+
type: "event",
|
|
436
|
+
inputs: [
|
|
437
|
+
{ name: "jobId", type: "uint256", indexed: true },
|
|
438
|
+
{ name: "worker", type: "address", indexed: true },
|
|
439
|
+
{ name: "amount", type: "uint256", indexed: false }
|
|
440
|
+
]
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: "WorkerBondRefunded",
|
|
444
|
+
type: "event",
|
|
445
|
+
inputs: [
|
|
446
|
+
{ name: "jobId", type: "uint256", indexed: true },
|
|
447
|
+
{ name: "worker", type: "address", indexed: true },
|
|
448
|
+
{ name: "amount", type: "uint256", indexed: false }
|
|
449
|
+
]
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: "WorkerBondForfeited",
|
|
453
|
+
type: "event",
|
|
454
|
+
inputs: [
|
|
455
|
+
{ name: "jobId", type: "uint256", indexed: true },
|
|
456
|
+
{ name: "poster", type: "address", indexed: true },
|
|
457
|
+
{ name: "amount", type: "uint256", indexed: false }
|
|
458
|
+
]
|
|
369
459
|
}
|
|
370
460
|
];
|
|
371
461
|
var IDENTITY_REGISTRY_ABI = [
|
|
@@ -423,6 +513,61 @@ var ERC20_ABI = [
|
|
|
423
513
|
}
|
|
424
514
|
];
|
|
425
515
|
|
|
516
|
+
// src/signers/viemSigner.ts
|
|
517
|
+
var import_viem = require("viem");
|
|
518
|
+
var import_accounts = require("viem/accounts");
|
|
519
|
+
var ViemSigner = class {
|
|
520
|
+
constructor(privateKey, chain, rpcUrl) {
|
|
521
|
+
this.account = (0, import_accounts.privateKeyToAccount)(privateKey);
|
|
522
|
+
this.address = this.account.address;
|
|
523
|
+
this.walletClient = (0, import_viem.createWalletClient)({ account: this.account, chain, transport: (0, import_viem.http)(rpcUrl) });
|
|
524
|
+
}
|
|
525
|
+
async writeContract(params) {
|
|
526
|
+
return this.walletClient.writeContract({
|
|
527
|
+
address: params.address,
|
|
528
|
+
abi: params.abi,
|
|
529
|
+
functionName: params.functionName,
|
|
530
|
+
args: params.args,
|
|
531
|
+
chain: null,
|
|
532
|
+
account: this.account
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// src/signers/circleSigner.ts
|
|
538
|
+
var import_viem2 = require("viem");
|
|
539
|
+
var import_developer_controlled_wallets = require("@circle-fin/developer-controlled-wallets");
|
|
540
|
+
var CircleSigner = class {
|
|
541
|
+
constructor(config) {
|
|
542
|
+
this.address = config.address;
|
|
543
|
+
this.walletId = config.walletId;
|
|
544
|
+
this.client = (0, import_developer_controlled_wallets.initiateDeveloperControlledWalletsClient)({
|
|
545
|
+
apiKey: config.apiKey,
|
|
546
|
+
entitySecret: config.entitySecret,
|
|
547
|
+
baseUrl: config.baseUrl
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
async writeContract(params) {
|
|
551
|
+
const callData = (0, import_viem2.encodeFunctionData)({
|
|
552
|
+
abi: params.abi,
|
|
553
|
+
functionName: params.functionName,
|
|
554
|
+
args: params.args
|
|
555
|
+
});
|
|
556
|
+
const created = await this.client.createContractExecutionTransaction({
|
|
557
|
+
walletId: this.walletId,
|
|
558
|
+
contractAddress: params.address,
|
|
559
|
+
callData,
|
|
560
|
+
fee: { type: "level", config: { feeLevel: "MEDIUM" } }
|
|
561
|
+
});
|
|
562
|
+
const txId = created.data?.id;
|
|
563
|
+
if (!txId) {
|
|
564
|
+
throw new Error("CircleSigner: createContractExecutionTransaction did not return a transaction id");
|
|
565
|
+
}
|
|
566
|
+
const result = await this.client.getTransaction({ id: txId, waitForTxHash: true });
|
|
567
|
+
return result.data.transaction.txHash;
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
426
571
|
// src/constants.ts
|
|
427
572
|
var ARC_TESTNET_RPC = "https://rpc.testnet.arc.network";
|
|
428
573
|
var ARC_TESTNET_CHAIN_ID = 5042002;
|
|
@@ -503,8 +648,46 @@ async function pinText(content, filename = "result.md") {
|
|
|
503
648
|
throw new Error("Set PINATA_JWT (preferred) or PINATA_API_KEY + PINATA_SECRET to pin to IPFS");
|
|
504
649
|
}
|
|
505
650
|
|
|
651
|
+
// src/logic.ts
|
|
652
|
+
function parseUsdc(dollars) {
|
|
653
|
+
return BigInt(Math.round(dollars * 10 ** USDC_DECIMALS));
|
|
654
|
+
}
|
|
655
|
+
function resolveDeadline(d, nowSec = Math.floor(Date.now() / 1e3)) {
|
|
656
|
+
if (d instanceof Date) return BigInt(Math.floor(d.getTime() / 1e3));
|
|
657
|
+
if (d < 1e9) return BigInt(nowSec + d);
|
|
658
|
+
return BigInt(d);
|
|
659
|
+
}
|
|
660
|
+
function workerBondFor(reward, bondBps = 1500n, minBond = 500000n) {
|
|
661
|
+
const pct = reward * bondBps / 10000n;
|
|
662
|
+
return pct > minBond ? pct : minBond;
|
|
663
|
+
}
|
|
664
|
+
function matchesBountyFilter(m, f) {
|
|
665
|
+
if (f.category && m.category !== f.category) return false;
|
|
666
|
+
if (f.agentOnly === true && !m.agentOnly) return false;
|
|
667
|
+
if (f.humanOnly === true && !m.humanOnly) return false;
|
|
668
|
+
if (f.agentOnly === false && m.agentOnly) return false;
|
|
669
|
+
if (f.humanOnly === false && m.humanOnly) return false;
|
|
670
|
+
if (f.maxReward !== void 0 && m.reward > parseUsdc(f.maxReward)) return false;
|
|
671
|
+
if (f.minReward !== void 0 && m.reward < parseUsdc(f.minReward)) return false;
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
var TRANSFER_SIG = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
675
|
+
var ZERO_TOPIC = "0x" + "0".repeat(64);
|
|
676
|
+
function agentIdFromReceiptLogs(logs, registryAddress, selfAddress) {
|
|
677
|
+
const me = selfAddress.toLowerCase().slice(2).padStart(64, "0");
|
|
678
|
+
for (const log of logs) {
|
|
679
|
+
if (log.address.toLowerCase() !== registryAddress.toLowerCase()) continue;
|
|
680
|
+
if (log.topics.length < 4) continue;
|
|
681
|
+
if (log.topics[0]?.toLowerCase() !== TRANSFER_SIG) continue;
|
|
682
|
+
if (log.topics[1]?.toLowerCase() !== ZERO_TOPIC) continue;
|
|
683
|
+
if (log.topics[2]?.toLowerCase() !== "0x" + me) continue;
|
|
684
|
+
return BigInt(log.topics[3]);
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
506
689
|
// src/ArcBountyAgent.ts
|
|
507
|
-
var arcTestnet = (0,
|
|
690
|
+
var arcTestnet = (0, import_viem3.defineChain)({
|
|
508
691
|
id: ARC_TESTNET_CHAIN_ID,
|
|
509
692
|
name: "Arc Testnet",
|
|
510
693
|
nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 6 },
|
|
@@ -514,8 +697,8 @@ var ArcBountyAgent = class {
|
|
|
514
697
|
constructor(config) {
|
|
515
698
|
this._agentId = null;
|
|
516
699
|
const rpcUrl = config.rpcUrl ?? ARC_TESTNET_RPC;
|
|
517
|
-
this.chain = (0,
|
|
518
|
-
this.
|
|
700
|
+
this.chain = (0, import_viem3.defineChain)({ ...arcTestnet, rpcUrls: { default: { http: [rpcUrl] } } });
|
|
701
|
+
this.signer = config.circleWallet ? new CircleSigner(config.circleWallet) : new ViemSigner(config.privateKey, this.chain, rpcUrl);
|
|
519
702
|
this.metadataURI = config.metadataURI ?? "";
|
|
520
703
|
const rawAdapter = config.bountyAdapterAddress ?? process.env["BOUNTY_ADAPTER_ADDRESS"];
|
|
521
704
|
if (!rawAdapter) {
|
|
@@ -523,15 +706,14 @@ var ArcBountyAgent = class {
|
|
|
523
706
|
"ArcBountyAgent: bountyAdapterAddress is required (constructor option or BOUNTY_ADAPTER_ADDRESS env). See agent-sdk/.env.example. Source of truth: contracts/DEPLOYMENTS.md."
|
|
524
707
|
);
|
|
525
708
|
}
|
|
526
|
-
if (!(0,
|
|
709
|
+
if (!(0, import_viem3.isAddress)(rawAdapter) || rawAdapter.toLowerCase() === ZERO_ADDRESS.toLowerCase()) {
|
|
527
710
|
throw new Error(`ArcBountyAgent: invalid bountyAdapterAddress: ${rawAdapter}`);
|
|
528
711
|
}
|
|
529
712
|
this.bountyAdapter = rawAdapter;
|
|
530
|
-
this.publicClient = (0,
|
|
531
|
-
this.walletClient = (0, import_viem.createWalletClient)({ account: this.account, chain: this.chain, transport: (0, import_viem.http)(rpcUrl) });
|
|
713
|
+
this.publicClient = (0, import_viem3.createPublicClient)({ chain: this.chain, transport: (0, import_viem3.http)(rpcUrl) });
|
|
532
714
|
}
|
|
533
715
|
get address() {
|
|
534
|
-
return this.
|
|
716
|
+
return this.signer.address;
|
|
535
717
|
}
|
|
536
718
|
// ─── Identity ───────────────────────────────────────────────────────────────
|
|
537
719
|
async register() {
|
|
@@ -540,34 +722,18 @@ var ArcBountyAgent = class {
|
|
|
540
722
|
this._agentId = existing;
|
|
541
723
|
return existing;
|
|
542
724
|
}
|
|
543
|
-
const hash = await this.
|
|
725
|
+
const hash = await this.signer.writeContract({
|
|
544
726
|
address: CONTRACTS.IDENTITY_REGISTRY,
|
|
545
727
|
abi: IDENTITY_REGISTRY_ABI,
|
|
546
728
|
functionName: "register",
|
|
547
|
-
args: [this.metadataURI]
|
|
548
|
-
chain: null,
|
|
549
|
-
account: this.account
|
|
729
|
+
args: [this.metadataURI]
|
|
550
730
|
});
|
|
551
731
|
const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
|
|
552
|
-
const agentId =
|
|
732
|
+
const agentId = agentIdFromReceiptLogs(receipt.logs, CONTRACTS.IDENTITY_REGISTRY, this.signer.address);
|
|
553
733
|
if (agentId === null) throw new Error("Registration succeeded but agentId not found in events");
|
|
554
734
|
this._agentId = agentId;
|
|
555
735
|
return agentId;
|
|
556
736
|
}
|
|
557
|
-
/** Pull the minted tokenId from a Transfer(from=0x0, to=self) log in a receipt. */
|
|
558
|
-
_agentIdFromReceiptLogs(logs) {
|
|
559
|
-
const TRANSFER_SIG = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
560
|
-
const me = this.account.address.toLowerCase().slice(2).padStart(64, "0");
|
|
561
|
-
for (const log of logs) {
|
|
562
|
-
if (log.address.toLowerCase() !== CONTRACTS.IDENTITY_REGISTRY.toLowerCase()) continue;
|
|
563
|
-
if (log.topics.length < 4) continue;
|
|
564
|
-
if (log.topics[0]?.toLowerCase() !== TRANSFER_SIG) continue;
|
|
565
|
-
if (log.topics[1]?.toLowerCase() !== "0x" + "0".repeat(64)) continue;
|
|
566
|
-
if (log.topics[2]?.toLowerCase() !== "0x" + me) continue;
|
|
567
|
-
return BigInt(log.topics[3]);
|
|
568
|
-
}
|
|
569
|
-
return null;
|
|
570
|
-
}
|
|
571
737
|
get agentId() {
|
|
572
738
|
if (this._agentId === null) throw new Error("Agent not registered. Call register() first.");
|
|
573
739
|
return this._agentId;
|
|
@@ -593,15 +759,7 @@ var ArcBountyAgent = class {
|
|
|
593
759
|
args: [category, BigInt(offset), BigInt(limit)]
|
|
594
760
|
});
|
|
595
761
|
const metas = await Promise.all(jobIds.map((jobId) => this.getBounty(jobId)));
|
|
596
|
-
return metas.filter((m) => {
|
|
597
|
-
if (agentOnly === true && !m.agentOnly) return false;
|
|
598
|
-
if (humanOnly === true && !m.humanOnly) return false;
|
|
599
|
-
if (agentOnly === false && m.agentOnly) return false;
|
|
600
|
-
if (humanOnly === false && m.humanOnly) return false;
|
|
601
|
-
if (maxReward !== void 0 && m.reward > this._parseUsdc(maxReward)) return false;
|
|
602
|
-
if (minReward !== void 0 && m.reward < this._parseUsdc(minReward)) return false;
|
|
603
|
-
return true;
|
|
604
|
-
});
|
|
762
|
+
return metas.filter((m) => matchesBountyFilter(m, { agentOnly, humanOnly, maxReward, minReward }));
|
|
605
763
|
}
|
|
606
764
|
async getBounty(jobId) {
|
|
607
765
|
const raw = await this.publicClient.readContract({
|
|
@@ -642,11 +800,11 @@ var ArcBountyAgent = class {
|
|
|
642
800
|
if (!opts.descriptionCid && !opts.descriptionText) {
|
|
643
801
|
throw new Error("Provide either descriptionCid or descriptionText");
|
|
644
802
|
}
|
|
645
|
-
const reward =
|
|
646
|
-
const deadline =
|
|
803
|
+
const reward = parseUsdc(opts.rewardUsdc);
|
|
804
|
+
const deadline = resolveDeadline(opts.deadline);
|
|
647
805
|
const descCid = opts.descriptionCid ?? await pinText(opts.descriptionText);
|
|
648
806
|
await this._ensureUsdcAllowance(reward);
|
|
649
|
-
const hash = await this.
|
|
807
|
+
const hash = await this.signer.writeContract({
|
|
650
808
|
address: this.bountyAdapter,
|
|
651
809
|
abi: BOUNTY_ADAPTER_ABI,
|
|
652
810
|
functionName: "createBounty",
|
|
@@ -658,17 +816,16 @@ var ArcBountyAgent = class {
|
|
|
658
816
|
category: opts.category,
|
|
659
817
|
tags: opts.tags ?? [],
|
|
660
818
|
agentOnly: opts.agentOnly ?? false,
|
|
661
|
-
humanOnly: opts.humanOnly ?? false
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
account: this.account
|
|
819
|
+
humanOnly: opts.humanOnly ?? false,
|
|
820
|
+
requireWorkerBond: opts.requireWorkerBond ?? false
|
|
821
|
+
}]
|
|
665
822
|
});
|
|
666
823
|
const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
|
|
667
824
|
let jobId;
|
|
668
825
|
for (const log of receipt.logs) {
|
|
669
826
|
if (log.address.toLowerCase() !== this.bountyAdapter.toLowerCase()) continue;
|
|
670
827
|
try {
|
|
671
|
-
const decoded = (0,
|
|
828
|
+
const decoded = (0, import_viem3.decodeEventLog)({
|
|
672
829
|
abi: BOUNTY_ADAPTER_ABI,
|
|
673
830
|
data: log.data,
|
|
674
831
|
topics: log.topics
|
|
@@ -685,32 +842,30 @@ var ArcBountyAgent = class {
|
|
|
685
842
|
// ─── Take / submit ──────────────────────────────────────────────────────────
|
|
686
843
|
async takeBounty(jobId) {
|
|
687
844
|
const agentId = this._agentId ?? 0n;
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
845
|
+
const meta = await this.getBounty(jobId);
|
|
846
|
+
if (meta.requireWorkerBond) {
|
|
847
|
+
const [bondBps, minBond] = await Promise.all([
|
|
848
|
+
this.publicClient.readContract({
|
|
849
|
+
address: this.bountyAdapter,
|
|
850
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
851
|
+
functionName: "WORKER_BOND_BPS"
|
|
852
|
+
}),
|
|
853
|
+
this.publicClient.readContract({
|
|
854
|
+
address: this.bountyAdapter,
|
|
855
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
856
|
+
functionName: "MIN_WORKER_BOND"
|
|
857
|
+
})
|
|
858
|
+
]);
|
|
859
|
+
await this._ensureUsdcAllowance(workerBondFor(meta.reward, bondBps, minBond));
|
|
860
|
+
}
|
|
861
|
+
return this._writeAdapter("takeBounty", [jobId, agentId]);
|
|
698
862
|
}
|
|
699
863
|
async submitWork(jobId, options) {
|
|
700
864
|
if (!options.text && !options.cid) {
|
|
701
865
|
throw new Error("Provide either text or cid");
|
|
702
866
|
}
|
|
703
867
|
const cid = options.cid ?? await pinText(options.text);
|
|
704
|
-
|
|
705
|
-
address: this.bountyAdapter,
|
|
706
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
707
|
-
functionName: "submitWork",
|
|
708
|
-
args: [jobId, cid],
|
|
709
|
-
chain: null,
|
|
710
|
-
account: this.account
|
|
711
|
-
});
|
|
712
|
-
await this._waitForTx(hash);
|
|
713
|
-
return { hash };
|
|
868
|
+
return this._writeAdapter("submitWork", [jobId, cid]);
|
|
714
869
|
}
|
|
715
870
|
// ─── Poster cycle ───────────────────────────────────────────────────────────
|
|
716
871
|
// These let a protocol/DAO agent run the full poster side end-to-end.
|
|
@@ -751,75 +906,67 @@ var ArcBountyAgent = class {
|
|
|
751
906
|
async claimDefaultRuling(jobId) {
|
|
752
907
|
return this._writeAdapter("claimDefaultRuling", [jobId]);
|
|
753
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* V3.3 liveness fallback: if the respondent DID reply (so claimDefaultRuling
|
|
911
|
+
* no longer applies) but the arbitrator never called resolveDispute within
|
|
912
|
+
* ARBITRATOR_TIMEOUT (30d) of disputeRaisedAt, anyone may trigger a neutral
|
|
913
|
+
* 50/50 split between poster and worker. No reputation penalty either way.
|
|
914
|
+
*/
|
|
915
|
+
async claimArbitratorTimeout(jobId) {
|
|
916
|
+
return this._writeAdapter("claimArbitratorTimeout", [jobId]);
|
|
917
|
+
}
|
|
754
918
|
// ─── Dispute flow (worker-side) ─────────────────────────────────────────────
|
|
755
919
|
/** Worker challenges a pending rejection — flips bounty into dispute with worker as initiator. */
|
|
756
920
|
async challengeRejection(jobId, evidence) {
|
|
757
921
|
const cid = await this._resolveEvidenceCid(evidence);
|
|
758
|
-
|
|
759
|
-
address: this.bountyAdapter,
|
|
760
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
761
|
-
functionName: "challengeRejection",
|
|
762
|
-
args: [jobId, cid],
|
|
763
|
-
chain: null,
|
|
764
|
-
account: this.account
|
|
765
|
-
});
|
|
766
|
-
await this._waitForTx(hash);
|
|
767
|
-
return { hash };
|
|
922
|
+
return this._writeAdapter("challengeRejection", [jobId, cid]);
|
|
768
923
|
}
|
|
769
924
|
/** Open a dispute (either party — after submission, before resolution). */
|
|
770
925
|
async disputeBounty(jobId, evidence) {
|
|
771
926
|
const cid = await this._resolveEvidenceCid(evidence);
|
|
772
|
-
|
|
773
|
-
address: this.bountyAdapter,
|
|
774
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
775
|
-
functionName: "disputeBounty",
|
|
776
|
-
args: [jobId, cid],
|
|
777
|
-
chain: null,
|
|
778
|
-
account: this.account
|
|
779
|
-
});
|
|
780
|
-
await this._waitForTx(hash);
|
|
781
|
-
return { hash };
|
|
927
|
+
return this._writeAdapter("disputeBounty", [jobId, cid]);
|
|
782
928
|
}
|
|
783
929
|
/** Respond to an open dispute (only the non-initiator may call). */
|
|
784
930
|
async respondToDispute(jobId, evidence) {
|
|
785
931
|
const cid = await this._resolveEvidenceCid(evidence);
|
|
786
|
-
|
|
787
|
-
address: this.bountyAdapter,
|
|
788
|
-
abi: BOUNTY_ADAPTER_ABI,
|
|
789
|
-
functionName: "respondToDispute",
|
|
790
|
-
args: [jobId, cid],
|
|
791
|
-
chain: null,
|
|
792
|
-
account: this.account
|
|
793
|
-
});
|
|
794
|
-
await this._waitForTx(hash);
|
|
795
|
-
return { hash };
|
|
932
|
+
return this._writeAdapter("respondToDispute", [jobId, cid]);
|
|
796
933
|
}
|
|
797
934
|
// ─── Expire stale bounties ──────────────────────────────────────────────────
|
|
935
|
+
/**
|
|
936
|
+
* Scans the full bounty set and calls expireBounty() on anything past its
|
|
937
|
+
* deadline with no submission and not yet resolved. Stops after finding
|
|
938
|
+
* `limit` candidates to expire.
|
|
939
|
+
*
|
|
940
|
+
* NOTE: `getOpenBounties` (used pre-V3.3) can NEVER return a candidate for
|
|
941
|
+
* this — it excludes any bounty whose deadline has already passed by
|
|
942
|
+
* definition (`_isOpenMatch` checks `block.timestamp <= deadline`). This
|
|
943
|
+
* scan walks `allJobIds` directly instead, mirroring the keeper cron route
|
|
944
|
+
* (`frontend/app/api/cron/keeper/route.ts`).
|
|
945
|
+
*/
|
|
798
946
|
async expireStale(category = "", limit = 100) {
|
|
799
|
-
const
|
|
947
|
+
const total = await this.publicClient.readContract({
|
|
800
948
|
address: this.bountyAdapter,
|
|
801
949
|
abi: BOUNTY_ADAPTER_ABI,
|
|
802
|
-
functionName: "
|
|
803
|
-
args: [category, 0n, BigInt(limit)]
|
|
950
|
+
functionName: "totalBounties"
|
|
804
951
|
});
|
|
805
952
|
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
806
953
|
const expired = [];
|
|
807
|
-
for (
|
|
954
|
+
for (let i = 0n; i < total && expired.length < limit; i++) {
|
|
955
|
+
const jobId = await this.publicClient.readContract({
|
|
956
|
+
address: this.bountyAdapter,
|
|
957
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
958
|
+
functionName: "allJobIds",
|
|
959
|
+
args: [i]
|
|
960
|
+
});
|
|
808
961
|
const meta = await this.getBounty(jobId);
|
|
809
|
-
if (meta.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
account: this.account
|
|
818
|
-
});
|
|
819
|
-
await this._waitForTx(hash);
|
|
820
|
-
expired.push(jobId);
|
|
821
|
-
} catch {
|
|
822
|
-
}
|
|
962
|
+
if (meta.resolved) continue;
|
|
963
|
+
if (meta.submittedResultHash.length > 0) continue;
|
|
964
|
+
if (category && meta.category !== category) continue;
|
|
965
|
+
if (meta.deadline >= now) continue;
|
|
966
|
+
try {
|
|
967
|
+
await this._writeAdapter("expireBounty", [jobId]);
|
|
968
|
+
expired.push(jobId);
|
|
969
|
+
} catch {
|
|
823
970
|
}
|
|
824
971
|
}
|
|
825
972
|
return expired;
|
|
@@ -839,6 +986,21 @@ var ArcBountyAgent = class {
|
|
|
839
986
|
return { averageScore: 0n, totalFeedbacks: 0n, totalJobs: 0n };
|
|
840
987
|
}
|
|
841
988
|
}
|
|
989
|
+
/**
|
|
990
|
+
* V4 anti-Sybil signal: count of distinct posters who've actually paid out
|
|
991
|
+
* a completed bounty to this agent. Costs N real funded wallets to fake N —
|
|
992
|
+
* unlike the raw ERC-8004 average score, which one alt account can inflate
|
|
993
|
+
* for a few cents. See V4_DESIGN_ANTI_SYBIL.md.
|
|
994
|
+
*/
|
|
995
|
+
async getUniquePosterCount(agentId) {
|
|
996
|
+
const id = agentId ?? this.agentId;
|
|
997
|
+
return this.publicClient.readContract({
|
|
998
|
+
address: this.bountyAdapter,
|
|
999
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
1000
|
+
functionName: "uniquePosterCount",
|
|
1001
|
+
args: [id]
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
842
1004
|
async getAgentInfo() {
|
|
843
1005
|
const id = this.agentId;
|
|
844
1006
|
const reputation = await this.getReputation(id);
|
|
@@ -886,7 +1048,7 @@ var ArcBountyAgent = class {
|
|
|
886
1048
|
seen.add(key);
|
|
887
1049
|
try {
|
|
888
1050
|
const meta = await this.getBounty(jobId);
|
|
889
|
-
if (!
|
|
1051
|
+
if (!matchesBountyFilter(meta, filter)) continue;
|
|
890
1052
|
await onMatch(meta);
|
|
891
1053
|
} catch (err) {
|
|
892
1054
|
console.error(`[ArcBountyAgent] onMatch error for #${key}:`, err);
|
|
@@ -897,15 +1059,110 @@ var ArcBountyAgent = class {
|
|
|
897
1059
|
});
|
|
898
1060
|
return unwatch;
|
|
899
1061
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1062
|
+
// ─── Dispute watchdog (self-protection) ─────────────────────────────────────
|
|
1063
|
+
/**
|
|
1064
|
+
* Background watchdog over this agent's own assigned bounties. An agent
|
|
1065
|
+
* that only calls `takeBounty`/`submitWork` and then goes idle is exposed
|
|
1066
|
+
* to every counterparty-controlled window in the contract: a poster can
|
|
1067
|
+
* reject a correct submission (48h to challenge), open a dispute the agent
|
|
1068
|
+
* never responds to (48h to respond, then the *other* side wins by
|
|
1069
|
+
* default), or the agent may simply be owed a payout nobody triggered yet
|
|
1070
|
+
* (14d autoApprove / 30d claimArbitratorTimeout). `protect()` polls
|
|
1071
|
+
* `getMyBounties()` and reacts automatically:
|
|
1072
|
+
*
|
|
1073
|
+
* - **Pending rejection, not yet challenged** → calls `onRejection` (if
|
|
1074
|
+
* provided) for evidence and calls `challengeRejection`. Without a
|
|
1075
|
+
* callback, a rejection is only logged, never auto-challenged — silently
|
|
1076
|
+
* auto-disputing every rejection would be its own failure mode.
|
|
1077
|
+
* - **Dispute raised by the other party, not yet responded** → calls
|
|
1078
|
+
* `onDisputeAgainstMe` for evidence and calls `respondToDispute`. Same
|
|
1079
|
+
* caveat: no callback means log-only.
|
|
1080
|
+
* - **Dispute resolved-by-response but arbitrator never ruled (30d)** →
|
|
1081
|
+
* calls `claimArbitratorTimeout` automatically (permissionless, no
|
|
1082
|
+
* evidence needed — this just unsticks the agent's own frozen funds).
|
|
1083
|
+
* - **Submitted, approval window elapsed (14d), poster silent** → calls
|
|
1084
|
+
* `autoApprove` automatically.
|
|
1085
|
+
*
|
|
1086
|
+
* Returns an `unwatch()` function. Errors on any single bounty are logged
|
|
1087
|
+
* and swallowed so one bad case can't kill the whole watchdog.
|
|
1088
|
+
*/
|
|
1089
|
+
protect(options = {}) {
|
|
1090
|
+
const pollingIntervalMs = options.pollingIntervalMs ?? 6e4;
|
|
1091
|
+
let stopped = false;
|
|
1092
|
+
const tick = async () => {
|
|
1093
|
+
if (stopped) return;
|
|
1094
|
+
try {
|
|
1095
|
+
await this._protectOnce(options);
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
console.error("[ArcBountyAgent.protect] tick error:", err);
|
|
1098
|
+
}
|
|
1099
|
+
if (!stopped) timer = setTimeout(tick, pollingIntervalMs);
|
|
1100
|
+
};
|
|
1101
|
+
let timer = setTimeout(tick, 0);
|
|
1102
|
+
return () => {
|
|
1103
|
+
stopped = true;
|
|
1104
|
+
clearTimeout(timer);
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
async _protectOnce(options) {
|
|
1108
|
+
const [rejectionWindow, disputeWindow, approvalTimeout, arbitratorTimeout] = await Promise.all([
|
|
1109
|
+
this.publicClient.readContract({
|
|
1110
|
+
address: this.bountyAdapter,
|
|
1111
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
1112
|
+
functionName: "REJECTION_CHALLENGE_WINDOW"
|
|
1113
|
+
}),
|
|
1114
|
+
this.publicClient.readContract({
|
|
1115
|
+
address: this.bountyAdapter,
|
|
1116
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
1117
|
+
functionName: "DISPUTE_RESPONSE_WINDOW"
|
|
1118
|
+
}),
|
|
1119
|
+
this.publicClient.readContract({
|
|
1120
|
+
address: this.bountyAdapter,
|
|
1121
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
1122
|
+
functionName: "APPROVAL_TIMEOUT"
|
|
1123
|
+
}),
|
|
1124
|
+
this.publicClient.readContract({
|
|
1125
|
+
address: this.bountyAdapter,
|
|
1126
|
+
abi: BOUNTY_ADAPTER_ABI,
|
|
1127
|
+
functionName: "ARBITRATOR_TIMEOUT"
|
|
1128
|
+
})
|
|
1129
|
+
]);
|
|
1130
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
1131
|
+
const mine = await this.getMyBounties();
|
|
1132
|
+
for (const meta of mine) {
|
|
1133
|
+
if (meta.resolved) continue;
|
|
1134
|
+
try {
|
|
1135
|
+
if (meta.rejectedAt > 0n && !meta.inDispute && now <= meta.rejectedAt + rejectionWindow) {
|
|
1136
|
+
options.onEvent?.("rejection_pending", meta);
|
|
1137
|
+
if (options.onRejection) {
|
|
1138
|
+
const evidence = await options.onRejection(meta);
|
|
1139
|
+
await this.challengeRejection(meta.jobId, evidence);
|
|
1140
|
+
options.onEvent?.("rejection_challenged", meta);
|
|
1141
|
+
}
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
if (meta.inDispute && meta.disputeResponseHash.length === 0 && meta.disputeInitiator.toLowerCase() !== this.address.toLowerCase() && now <= meta.disputeRaisedAt + disputeWindow) {
|
|
1145
|
+
options.onEvent?.("dispute_needs_response", meta);
|
|
1146
|
+
if (options.onDisputeAgainstMe) {
|
|
1147
|
+
const evidence = await options.onDisputeAgainstMe(meta);
|
|
1148
|
+
await this.respondToDispute(meta.jobId, evidence);
|
|
1149
|
+
options.onEvent?.("dispute_responded", meta);
|
|
1150
|
+
}
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (meta.inDispute && meta.disputeResponseHash.length > 0 && now > meta.disputeRaisedAt + arbitratorTimeout) {
|
|
1154
|
+
await this.claimArbitratorTimeout(meta.jobId);
|
|
1155
|
+
options.onEvent?.("arbitrator_timeout_claimed", meta);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (meta.submittedAt > 0n && meta.rejectedAt === 0n && !meta.inDispute && now > meta.submittedAt + approvalTimeout) {
|
|
1159
|
+
await this.autoApprove(meta.jobId);
|
|
1160
|
+
options.onEvent?.("auto_approved", meta);
|
|
1161
|
+
}
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
console.error(`[ArcBountyAgent.protect] error handling bounty #${meta.jobId}:`, err);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
909
1166
|
}
|
|
910
1167
|
// ─── Autonomous loop ────────────────────────────────────────────────────────
|
|
911
1168
|
async runOnce(filter, runTask) {
|
|
@@ -931,13 +1188,11 @@ var ArcBountyAgent = class {
|
|
|
931
1188
|
* retry, paymaster) land in one place.
|
|
932
1189
|
*/
|
|
933
1190
|
async _writeAdapter(functionName, args) {
|
|
934
|
-
const hash = await this.
|
|
1191
|
+
const hash = await this.signer.writeContract({
|
|
935
1192
|
address: this.bountyAdapter,
|
|
936
1193
|
abi: BOUNTY_ADAPTER_ABI,
|
|
937
1194
|
functionName,
|
|
938
|
-
args
|
|
939
|
-
chain: null,
|
|
940
|
-
account: this.account
|
|
1195
|
+
args
|
|
941
1196
|
});
|
|
942
1197
|
await this._waitForTx(hash);
|
|
943
1198
|
return { hash };
|
|
@@ -986,13 +1241,11 @@ var ArcBountyAgent = class {
|
|
|
986
1241
|
args: [this.address, this.bountyAdapter]
|
|
987
1242
|
});
|
|
988
1243
|
if (current >= amount) return;
|
|
989
|
-
const hash = await this.
|
|
1244
|
+
const hash = await this.signer.writeContract({
|
|
990
1245
|
address: CONTRACTS.USDC,
|
|
991
1246
|
abi: ERC20_ABI,
|
|
992
1247
|
functionName: "approve",
|
|
993
|
-
args: [this.bountyAdapter, amount]
|
|
994
|
-
chain: null,
|
|
995
|
-
account: this.account
|
|
1248
|
+
args: [this.bountyAdapter, amount]
|
|
996
1249
|
});
|
|
997
1250
|
await this._waitForTx(hash);
|
|
998
1251
|
}
|
|
@@ -1000,14 +1253,6 @@ var ArcBountyAgent = class {
|
|
|
1000
1253
|
if (!e.text && !e.cid) throw new Error("Provide either text or cid");
|
|
1001
1254
|
return e.cid ?? await pinText(e.text);
|
|
1002
1255
|
}
|
|
1003
|
-
_resolveDeadline(d) {
|
|
1004
|
-
if (d instanceof Date) return BigInt(Math.floor(d.getTime() / 1e3));
|
|
1005
|
-
if (d < 1e9) return BigInt(Math.floor(Date.now() / 1e3) + d);
|
|
1006
|
-
return BigInt(d);
|
|
1007
|
-
}
|
|
1008
|
-
_parseUsdc(dollars) {
|
|
1009
|
-
return BigInt(Math.round(dollars * 10 ** USDC_DECIMALS));
|
|
1010
|
-
}
|
|
1011
1256
|
};
|
|
1012
1257
|
|
|
1013
1258
|
// src/metadata.ts
|
|
@@ -1055,7 +1300,11 @@ async function pinAgentMetadata(meta) {
|
|
|
1055
1300
|
fetchIpfsJson,
|
|
1056
1301
|
fetchIpfsText,
|
|
1057
1302
|
isPinningConfigured,
|
|
1303
|
+
matchesBountyFilter,
|
|
1304
|
+
parseUsdc,
|
|
1058
1305
|
pinAgentMetadata,
|
|
1059
1306
|
pinText,
|
|
1060
|
-
|
|
1307
|
+
resolveDeadline,
|
|
1308
|
+
validateAgentMetadata,
|
|
1309
|
+
workerBondFor
|
|
1061
1310
|
});
|