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/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 import_viem = require("viem");
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, import_viem.defineChain)({
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, import_viem.defineChain)({ ...arcTestnet, rpcUrls: { default: { http: [rpcUrl] } } });
518
- this.account = (0, import_accounts.privateKeyToAccount)(config.privateKey);
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, import_viem.isAddress)(rawAdapter) || rawAdapter.toLowerCase() === ZERO_ADDRESS.toLowerCase()) {
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, import_viem.createPublicClient)({ chain: this.chain, transport: (0, import_viem.http)(rpcUrl) });
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.account.address;
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.walletClient.writeContract({
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 = this._agentIdFromReceiptLogs(receipt.logs);
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 = this._parseUsdc(opts.rewardUsdc);
646
- const deadline = this._resolveDeadline(opts.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.walletClient.writeContract({
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
- chain: null,
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, import_viem.decodeEventLog)({
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
- const hash = await this.walletClient.writeContract({
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
- const hash = await this.walletClient.writeContract({
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
- const hash = await this.walletClient.writeContract({
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
- const hash = await this.walletClient.writeContract({
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
- const hash = await this.walletClient.writeContract({
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 jobIds = await this.publicClient.readContract({
923
+ const total = await this.publicClient.readContract({
800
924
  address: this.bountyAdapter,
801
925
  abi: BOUNTY_ADAPTER_ABI,
802
- functionName: "getOpenBounties",
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 (const jobId of jobIds) {
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.deadline < now) {
810
- try {
811
- const hash = await this.walletClient.writeContract({
812
- address: this.bountyAdapter,
813
- abi: BOUNTY_ADAPTER_ABI,
814
- functionName: "expireBounty",
815
- args: [jobId],
816
- chain: null,
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 (!this._matchesFilter(meta, filter)) continue;
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
- _matchesFilter(m, f) {
901
- if (f.category && m.category !== f.category) return false;
902
- if (f.agentOnly === true && !m.agentOnly) return false;
903
- if (f.humanOnly === true && !m.humanOnly) return false;
904
- if (f.agentOnly === false && m.agentOnly) return false;
905
- if (f.humanOnly === false && m.humanOnly) return false;
906
- if (f.maxReward !== void 0 && m.reward > this._parseUsdc(f.maxReward)) return false;
907
- if (f.minReward !== void 0 && m.reward < this._parseUsdc(f.minReward)) return false;
908
- return true;
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.walletClient.writeContract({
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.walletClient.writeContract({
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