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