arcbounty-agent-sdk 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -2
- package/dist/index.d.mts +232 -11
- package/dist/index.d.ts +232 -11
- package/dist/index.js +392 -143
- package/dist/index.mjs +383 -139
- package/package.json +11 -3
- package/.env.example +0 -13
- package/examples/demo-agent.ts +0 -94
- package/src/ArcBountyAgent.ts +0 -645
- package/src/abi.ts +0 -386
- package/src/constants.ts +0 -20
- package/src/index.ts +0 -21
- package/src/ipfs.ts +0 -80
- package/src/metadata.ts +0 -69
- package/src/types.ts +0 -105
- package/tsconfig.json +0 -13
package/dist/index.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.
|
|
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:
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
615
|
-
const 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.
|
|
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
|
-
|
|
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
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
911
|
+
const total = await this.publicClient.readContract({
|
|
769
912
|
address: this.bountyAdapter,
|
|
770
913
|
abi: BOUNTY_ADAPTER_ABI,
|
|
771
|
-
functionName: "
|
|
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 (
|
|
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.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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 (!
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1270
|
+
resolveDeadline,
|
|
1271
|
+
validateAgentMetadata,
|
|
1272
|
+
workerBondFor
|
|
1029
1273
|
};
|