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