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