arc402-cli 0.3.0 → 0.3.2
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/commands/accept.d.ts.map +1 -1
- package/dist/commands/accept.js +26 -1
- package/dist/commands/accept.js.map +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +166 -0
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/arena-handshake.d.ts.map +1 -1
- package/dist/commands/arena-handshake.js +22 -0
- package/dist/commands/arena-handshake.js.map +1 -1
- package/dist/commands/arena.d.ts +3 -0
- package/dist/commands/arena.d.ts.map +1 -0
- package/dist/commands/arena.js +122 -0
- package/dist/commands/arena.js.map +1 -0
- package/dist/commands/deliver.d.ts.map +1 -1
- package/dist/commands/deliver.js +19 -0
- package/dist/commands/deliver.js.map +1 -1
- package/dist/commands/feed.d.ts +10 -0
- package/dist/commands/feed.d.ts.map +1 -0
- package/dist/commands/feed.js +203 -0
- package/dist/commands/feed.js.map +1 -0
- package/dist/commands/hire.d.ts.map +1 -1
- package/dist/commands/hire.js +29 -0
- package/dist/commands/hire.js.map +1 -1
- package/dist/commands/workroom.d.ts.map +1 -1
- package/dist/commands/workroom.js +37 -0
- package/dist/commands/workroom.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +336 -1
- package/dist/daemon/index.js.map +1 -1
- package/dist/endpoint-notify.d.ts +18 -0
- package/dist/endpoint-notify.d.ts.map +1 -0
- package/dist/endpoint-notify.js +43 -0
- package/dist/endpoint-notify.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/accept.ts +31 -1
- package/src/commands/agent.ts +187 -0
- package/src/commands/arena-handshake.ts +33 -0
- package/src/commands/arena.ts +121 -0
- package/src/commands/deliver.ts +19 -0
- package/src/commands/feed.ts +228 -0
- package/src/commands/hire.ts +31 -1
- package/src/commands/workroom.ts +44 -0
- package/src/daemon/index.ts +347 -1
- package/src/endpoint-notify.ts +46 -0
- package/src/index.ts +4 -0
package/src/commands/accept.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { ServiceAgreementClient } from "@arc402/sdk";
|
|
3
|
+
import { ethers } from "ethers";
|
|
3
4
|
import { loadConfig } from "../config";
|
|
4
5
|
import { requireSigner } from "../client";
|
|
5
6
|
import { printSenderInfo, executeContractWriteViaWallet } from "../wallet-router";
|
|
6
7
|
import { SERVICE_AGREEMENT_ABI } from "../abis";
|
|
8
|
+
import { resolveAgentEndpoint, notifyAgent, DEFAULT_REGISTRY_ADDRESS } from "../endpoint-notify";
|
|
7
9
|
|
|
8
10
|
export function registerAcceptCommand(program: Command): void {
|
|
9
11
|
program.command("accept <id>").description("Provider accepts a proposed agreement").action(async (id) => {
|
|
10
12
|
const config = loadConfig();
|
|
11
13
|
if (!config.serviceAgreementAddress) throw new Error("serviceAgreementAddress missing in config");
|
|
12
|
-
const { signer } = await requireSigner(config);
|
|
14
|
+
const { signer, address: signerAddress } = await requireSigner(config);
|
|
13
15
|
printSenderInfo(config);
|
|
16
|
+
|
|
17
|
+
// Read agreement to get client address for endpoint notification
|
|
18
|
+
let clientAddress = "";
|
|
19
|
+
try {
|
|
20
|
+
const prefProvider = new ethers.JsonRpcProvider(config.rpcUrl);
|
|
21
|
+
const saContract = new ethers.Contract(
|
|
22
|
+
config.serviceAgreementAddress,
|
|
23
|
+
["function getAgreement(uint256 id) external view returns (tuple(uint256 id, address client, address provider, string serviceType, string description, uint256 price, address token, uint256 deadline, bytes32 deliverablesHash, uint8 status, uint256 createdAt, uint256 resolvedAt, uint256 verifyWindowEnd, bytes32 committedHash))"],
|
|
24
|
+
prefProvider
|
|
25
|
+
);
|
|
26
|
+
const ag = await saContract.getAgreement(BigInt(id));
|
|
27
|
+
clientAddress = String(ag.client ?? "");
|
|
28
|
+
} catch { /* non-fatal */ }
|
|
29
|
+
|
|
14
30
|
if (config.walletContractAddress) {
|
|
15
31
|
await executeContractWriteViaWallet(
|
|
16
32
|
config.walletContractAddress, signer, config.serviceAgreementAddress,
|
|
@@ -21,5 +37,19 @@ export function registerAcceptCommand(program: Command): void {
|
|
|
21
37
|
await client.accept(BigInt(id));
|
|
22
38
|
}
|
|
23
39
|
console.log(`accepted ${id}`);
|
|
40
|
+
|
|
41
|
+
// Notify client's HTTP endpoint (non-blocking)
|
|
42
|
+
if (clientAddress) {
|
|
43
|
+
try {
|
|
44
|
+
const notifyProvider = new ethers.JsonRpcProvider(config.rpcUrl);
|
|
45
|
+
const registryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress ?? DEFAULT_REGISTRY_ADDRESS;
|
|
46
|
+
const endpoint = await resolveAgentEndpoint(clientAddress, notifyProvider, registryAddress);
|
|
47
|
+
const payload = { agreementId: id, from: signerAddress };
|
|
48
|
+
await notifyAgent(endpoint, "/hire/accepted", payload);
|
|
49
|
+
await notifyAgent(endpoint, "/delivery/accepted", payload);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn(`Warning: could not notify client endpoint: ${err instanceof Error ? err.message : String(err)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
24
54
|
});
|
|
25
55
|
}
|
package/src/commands/agent.ts
CHANGED
|
@@ -413,6 +413,193 @@ export function registerAgentCommands(program: Command): void {
|
|
|
413
413
|
if (!address) throw new Error("No wallet configured");
|
|
414
414
|
await program.parseAsync([process.argv[0], process.argv[1], "agent", "info", address], { from: "user" });
|
|
415
415
|
});
|
|
416
|
+
|
|
417
|
+
// ─── profile (subgraph view) ─────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
agent
|
|
420
|
+
.command("profile <address>")
|
|
421
|
+
.description("Show detailed agent profile from the Arena subgraph")
|
|
422
|
+
.option("--json", "Output as JSON")
|
|
423
|
+
.action(async (address: string, opts: { json?: boolean }) => {
|
|
424
|
+
const normalizedAddr = address.toLowerCase();
|
|
425
|
+
try {
|
|
426
|
+
const data = await agentSubgraphQuery(`{
|
|
427
|
+
agent(id: "${normalizedAddr}") {
|
|
428
|
+
id name serviceType endpoint active
|
|
429
|
+
trustScore { globalScore }
|
|
430
|
+
capabilities(where: { active: true }) { capability active }
|
|
431
|
+
handshakesSent(first: 100, orderBy: timestamp, orderDirection: desc) {
|
|
432
|
+
id to { id name } hsType note timestamp
|
|
433
|
+
}
|
|
434
|
+
handshakesReceived(first: 100, orderBy: timestamp, orderDirection: desc) {
|
|
435
|
+
id from { id name } hsType note timestamp
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
clientAgreements: agreements(where: { client: "${normalizedAddr}", state: 1 }, first: 10) {
|
|
439
|
+
id serviceType price state
|
|
440
|
+
}
|
|
441
|
+
providerAgreements: agreements(where: { provider: "${normalizedAddr}", state: 1 }, first: 10) {
|
|
442
|
+
id serviceType price state
|
|
443
|
+
}
|
|
444
|
+
vouchedFor: vouches(where: { voucher: "${normalizedAddr}", active: true }, first: 100) {
|
|
445
|
+
id newAgent { id name } stakeAmount
|
|
446
|
+
}
|
|
447
|
+
vouchedBy: vouches(where: { newAgent: "${normalizedAddr}", active: true }, first: 100) {
|
|
448
|
+
id voucher { id name } stakeAmount
|
|
449
|
+
}
|
|
450
|
+
}`);
|
|
451
|
+
|
|
452
|
+
const agentData = data["agent"] as Record<string, unknown> | null;
|
|
453
|
+
|
|
454
|
+
if (!agentData) {
|
|
455
|
+
if (opts.json) {
|
|
456
|
+
console.log(JSON.stringify({ error: "Agent not registered", address }));
|
|
457
|
+
} else {
|
|
458
|
+
console.log(chalk.red(`Agent not registered: ${address}`));
|
|
459
|
+
}
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (opts.json) {
|
|
464
|
+
console.log(JSON.stringify(data, null, 2));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const sent = (agentData["handshakesSent"] as Record<string, unknown>[]) ?? [];
|
|
469
|
+
const received = (agentData["handshakesReceived"] as Record<string, unknown>[]) ?? [];
|
|
470
|
+
const sentToIds = new Set(sent.map((h) => (h["to"] as Record<string, string>)["id"]));
|
|
471
|
+
const receivedFromIds = new Set(received.map((h) => (h["from"] as Record<string, string>)["id"]));
|
|
472
|
+
const mutual = [...sentToIds].filter((id) => receivedFromIds.has(id)).length;
|
|
473
|
+
|
|
474
|
+
const trustScore =
|
|
475
|
+
((agentData["trustScore"] as Record<string, unknown> | null)?.["globalScore"] as number | undefined) ?? 0;
|
|
476
|
+
const caps = (agentData["capabilities"] as Record<string, unknown>[]) ?? [];
|
|
477
|
+
|
|
478
|
+
const allAgreements = [
|
|
479
|
+
...((data["clientAgreements"] as unknown[]) ?? []),
|
|
480
|
+
...((data["providerAgreements"] as unknown[]) ?? []),
|
|
481
|
+
] as Record<string, unknown>[];
|
|
482
|
+
|
|
483
|
+
const hsTypeLabels: Record<number, string> = {
|
|
484
|
+
0: "Respected",
|
|
485
|
+
1: "Curious",
|
|
486
|
+
2: "Endorsed",
|
|
487
|
+
3: "Thanked",
|
|
488
|
+
4: "Collaborated",
|
|
489
|
+
5: "Challenged",
|
|
490
|
+
6: "Referred",
|
|
491
|
+
7: "Hello",
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const recentActivity = [
|
|
495
|
+
...sent.slice(0, 3).map((h) => {
|
|
496
|
+
const to = h["to"] as Record<string, string>;
|
|
497
|
+
const label = hsTypeLabels[Number(h["hsType"])] ?? `Type${h["hsType"]}`;
|
|
498
|
+
const note = h["note"] ? ` — "${h["note"]}"` : "";
|
|
499
|
+
return {
|
|
500
|
+
ts: Number(h["timestamp"]),
|
|
501
|
+
line: ` [${profileDate(Number(h["timestamp"]))}] ${label} ${to["name"] || shortAddress(to["id"])}${note}`,
|
|
502
|
+
};
|
|
503
|
+
}),
|
|
504
|
+
...received.slice(0, 3).map((h) => {
|
|
505
|
+
const from = h["from"] as Record<string, string>;
|
|
506
|
+
const label = hsTypeLabels[Number(h["hsType"])] ?? `Type${h["hsType"]}`;
|
|
507
|
+
const note = h["note"] ? ` — "${h["note"]}"` : "";
|
|
508
|
+
return {
|
|
509
|
+
ts: Number(h["timestamp"]),
|
|
510
|
+
line: ` [${profileDate(Number(h["timestamp"]))}] Received ${label} from ${from["name"] || shortAddress(from["id"])}${note}`,
|
|
511
|
+
};
|
|
512
|
+
}),
|
|
513
|
+
]
|
|
514
|
+
.sort((a, b) => b.ts - a.ts)
|
|
515
|
+
.slice(0, 5);
|
|
516
|
+
|
|
517
|
+
const vouchedFor = (data["vouchedFor"] as unknown[]) ?? [];
|
|
518
|
+
const vouchedBy = (data["vouchedBy"] as unknown[]) ?? [];
|
|
519
|
+
|
|
520
|
+
const line = "═".repeat(43);
|
|
521
|
+
console.log(chalk.bold(line));
|
|
522
|
+
console.log(` ${chalk.bold((agentData["name"] as string) || "(unnamed)")}`);
|
|
523
|
+
console.log(` ${agentData["id"]}`);
|
|
524
|
+
console.log(chalk.bold(line));
|
|
525
|
+
console.log();
|
|
526
|
+
console.log(` Service Type: ${agentData["serviceType"]}`);
|
|
527
|
+
console.log(` Endpoint: ${agentData["endpoint"] || chalk.dim("(none)")}`);
|
|
528
|
+
console.log(
|
|
529
|
+
` Status: ${agentData["active"] ? chalk.green("✅ Active") : chalk.red("❌ Inactive")}`,
|
|
530
|
+
);
|
|
531
|
+
console.log(` Trust Score: ${trustScore}`);
|
|
532
|
+
console.log();
|
|
533
|
+
|
|
534
|
+
if (caps.length > 0) {
|
|
535
|
+
console.log(" Capabilities:");
|
|
536
|
+
for (const cap of caps) {
|
|
537
|
+
console.log(` ✓ ${(cap as Record<string, unknown>)["capability"]}`);
|
|
538
|
+
}
|
|
539
|
+
console.log();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log(
|
|
543
|
+
` Handshakes: ${sent.length} sent • ${received.length} received • ${mutual} mutual connections`,
|
|
544
|
+
);
|
|
545
|
+
console.log();
|
|
546
|
+
|
|
547
|
+
if (recentActivity.length > 0) {
|
|
548
|
+
console.log(" Recent Activity:");
|
|
549
|
+
for (const a of recentActivity) {
|
|
550
|
+
console.log(a.line);
|
|
551
|
+
}
|
|
552
|
+
console.log();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
console.log(" Active Agreements:");
|
|
556
|
+
if (allAgreements.length === 0) {
|
|
557
|
+
console.log(" None");
|
|
558
|
+
} else {
|
|
559
|
+
for (const a of allAgreements.slice(0, 5)) {
|
|
560
|
+
console.log(` #${(a["id"] as string).slice(0, 8)} ${a["serviceType"]}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
console.log();
|
|
564
|
+
|
|
565
|
+
console.log(" Vouches:");
|
|
566
|
+
console.log(` Vouched for: ${vouchedFor.length} agents`);
|
|
567
|
+
console.log(` Vouched by: ${vouchedBy.length} agents`);
|
|
568
|
+
} catch (err) {
|
|
569
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
570
|
+
if (opts.json) {
|
|
571
|
+
console.log(JSON.stringify({ error: "Subgraph unavailable", details: msg }));
|
|
572
|
+
} else {
|
|
573
|
+
console.error(chalk.red(`Subgraph unavailable: ${msg}`));
|
|
574
|
+
}
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ─── subgraph helpers (used by agent profile) ────────────────────────────────
|
|
581
|
+
|
|
582
|
+
const AGENT_SUBGRAPH_URL = "https://api.studio.thegraph.com/query/1744310/arc-402/v0.2.0";
|
|
583
|
+
|
|
584
|
+
async function agentSubgraphQuery(query: string): Promise<Record<string, unknown>> {
|
|
585
|
+
const res = await fetch(AGENT_SUBGRAPH_URL, {
|
|
586
|
+
method: "POST",
|
|
587
|
+
headers: { "Content-Type": "application/json" },
|
|
588
|
+
body: JSON.stringify({ query }),
|
|
589
|
+
});
|
|
590
|
+
if (!res.ok) throw new Error(`Subgraph HTTP ${res.status}`);
|
|
591
|
+
const json = (await res.json()) as { data?: Record<string, unknown>; errors?: unknown[] };
|
|
592
|
+
if (json.errors?.length) throw new Error(`Subgraph error: ${JSON.stringify(json.errors[0])}`);
|
|
593
|
+
return json.data ?? {};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function shortAddress(addr: string): string {
|
|
597
|
+
return addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function profileDate(ts: number): string {
|
|
601
|
+
const d = new Date(ts * 1000);
|
|
602
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
416
603
|
}
|
|
417
604
|
|
|
418
605
|
// ─── subdomain claim ──────────────────────────────────────────────────────────
|
|
@@ -2,6 +2,26 @@ import { Command } from "commander";
|
|
|
2
2
|
import { ethers } from "ethers";
|
|
3
3
|
import { loadConfig, getUsdcAddress } from "../config";
|
|
4
4
|
import { requireSigner } from "../client";
|
|
5
|
+
import { AGENT_REGISTRY_ABI } from "../abis";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_REGISTRY_ADDRESS = "0xD5c2851B00090c92Ba7F4723FB548bb30C9B6865";
|
|
8
|
+
|
|
9
|
+
async function pingHandshakeEndpoint(
|
|
10
|
+
agentAddress: string,
|
|
11
|
+
payload: Record<string, unknown>,
|
|
12
|
+
registryAddress: string,
|
|
13
|
+
provider: ethers.Provider
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const registry = new ethers.Contract(registryAddress, AGENT_REGISTRY_ABI, provider);
|
|
16
|
+
const agentData = await registry.getAgent(agentAddress);
|
|
17
|
+
const endpoint = agentData.endpoint as string;
|
|
18
|
+
if (!endpoint) return;
|
|
19
|
+
await fetch(`${endpoint}/handshake`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
body: JSON.stringify(payload),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
5
25
|
|
|
6
26
|
// ─── Handshake Contract ABI (from Handshake.sol) ─────────────────────────────
|
|
7
27
|
|
|
@@ -114,6 +134,19 @@ export function registerArenaHandshakeCommands(program: Command): void {
|
|
|
114
134
|
tx = await handshake.sendHandshake(agentAddress, hsType, opts.note, { value });
|
|
115
135
|
}
|
|
116
136
|
|
|
137
|
+
// Notify recipient's HTTP endpoint (non-blocking)
|
|
138
|
+
const registryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress ?? DEFAULT_REGISTRY_ADDRESS;
|
|
139
|
+
try {
|
|
140
|
+
await pingHandshakeEndpoint(
|
|
141
|
+
agentAddress,
|
|
142
|
+
{ from: myAddress, type: opts.type, note: opts.note, txHash: tx.hash },
|
|
143
|
+
registryAddress,
|
|
144
|
+
provider
|
|
145
|
+
);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn(`Warning: could not notify recipient endpoint: ${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
117
150
|
if (opts.json) {
|
|
118
151
|
console.log(JSON.stringify({ tx: tx.hash, from: myAddress, to: agentAddress, type: opts.type, note: opts.note }));
|
|
119
152
|
} else {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { runFeed, FeedOptions } from "./feed";
|
|
4
|
+
|
|
5
|
+
const SUBGRAPH_URL = "https://api.studio.thegraph.com/query/1744310/arc-402/v0.2.0";
|
|
6
|
+
|
|
7
|
+
async function gql(query: string): Promise<Record<string, unknown>> {
|
|
8
|
+
const res = await fetch(SUBGRAPH_URL, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "Content-Type": "application/json" },
|
|
11
|
+
body: JSON.stringify({ query }),
|
|
12
|
+
});
|
|
13
|
+
if (!res.ok) throw new Error(`Subgraph HTTP ${res.status}`);
|
|
14
|
+
const json = (await res.json()) as { data?: Record<string, unknown>; errors?: unknown[] };
|
|
15
|
+
if (json.errors?.length) throw new Error(`Subgraph error: ${JSON.stringify(json.errors[0])}`);
|
|
16
|
+
return json.data ?? {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function registerArenaCommands(program: Command): void {
|
|
20
|
+
const arena = program.command("arena").description("Arena network commands");
|
|
21
|
+
|
|
22
|
+
// ─── arena stats ───────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
arena
|
|
25
|
+
.command("stats")
|
|
26
|
+
.description("Show Arena network statistics")
|
|
27
|
+
.option("--json", "Output as JSON")
|
|
28
|
+
.action(async (opts) => {
|
|
29
|
+
try {
|
|
30
|
+
const data = await gql(`{
|
|
31
|
+
protocolStats(id: "global") {
|
|
32
|
+
totalAgents
|
|
33
|
+
totalWallets
|
|
34
|
+
totalAgreements
|
|
35
|
+
totalHandshakes
|
|
36
|
+
totalConnections
|
|
37
|
+
totalVouches
|
|
38
|
+
totalCapabilityClaims
|
|
39
|
+
}
|
|
40
|
+
}`);
|
|
41
|
+
|
|
42
|
+
const stats = data["protocolStats"] as Record<string, unknown> | null;
|
|
43
|
+
if (!stats) {
|
|
44
|
+
const msg = "No stats available — subgraph may still be syncing.";
|
|
45
|
+
if (opts.json) {
|
|
46
|
+
console.log(JSON.stringify({ error: msg }));
|
|
47
|
+
} else {
|
|
48
|
+
console.error(chalk.red(msg));
|
|
49
|
+
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Try to count active agreements separately — non-fatal if it fails
|
|
54
|
+
let activeAgreements = 0;
|
|
55
|
+
try {
|
|
56
|
+
const agData = await gql(`{
|
|
57
|
+
proposals: agreements(where: { state: 0 }, first: 1000) { id }
|
|
58
|
+
accepted: agreements(where: { state: 1 }, first: 1000) { id }
|
|
59
|
+
}`);
|
|
60
|
+
activeAgreements =
|
|
61
|
+
((agData["proposals"] as unknown[]) ?? []).length +
|
|
62
|
+
((agData["accepted"] as unknown[]) ?? []).length;
|
|
63
|
+
} catch {
|
|
64
|
+
// ignore — active count just stays 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (opts.json) {
|
|
68
|
+
console.log(JSON.stringify({ ...stats, activeAgreements }, null, 2));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pad = (v: unknown) => String(v ?? 0).padStart(6);
|
|
73
|
+
console.log(chalk.bold("╔══════════════════════════════════════╗"));
|
|
74
|
+
console.log(chalk.bold("║ ARC Arena — Network Stats ║"));
|
|
75
|
+
console.log(chalk.bold("╚══════════════════════════════════════╝"));
|
|
76
|
+
console.log();
|
|
77
|
+
console.log(` Agents ${pad(stats["totalAgents"])} registered`);
|
|
78
|
+
console.log(` Wallets ${pad(stats["totalWallets"])} deployed`);
|
|
79
|
+
console.log(
|
|
80
|
+
` Agreements ${pad(stats["totalAgreements"])} total (${activeAgreements} active)`,
|
|
81
|
+
);
|
|
82
|
+
console.log(` Handshakes ${pad(stats["totalHandshakes"])} sent`);
|
|
83
|
+
console.log(` Connections ${pad(stats["totalConnections"])} unique pairs`);
|
|
84
|
+
console.log(` Vouches ${pad(stats["totalVouches"])} active`);
|
|
85
|
+
console.log(` Capabilities ${pad(stats["totalCapabilityClaims"])} claimed`);
|
|
86
|
+
console.log();
|
|
87
|
+
console.log(chalk.dim(" Subgraph: v0.2.0 · synced"));
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
if (opts.json) {
|
|
91
|
+
console.log(JSON.stringify({ error: "Subgraph unavailable", details: msg }));
|
|
92
|
+
} else {
|
|
93
|
+
console.error(chalk.red(`Subgraph unavailable: ${msg}`));
|
|
94
|
+
}
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ─── arena feed (alias) ─────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
arena
|
|
102
|
+
.command("feed")
|
|
103
|
+
.description("Live terminal feed of recent Arena events (alias for arc402 feed)")
|
|
104
|
+
.option("--limit <n>", "Number of events to show", "20")
|
|
105
|
+
.option("--live", "Poll every 30s for new events")
|
|
106
|
+
.option("--type <type>", "Filter by event type: handshake|hire|fulfill|vouch")
|
|
107
|
+
.option("--json", "Output as JSON")
|
|
108
|
+
.action(async (opts) => {
|
|
109
|
+
try {
|
|
110
|
+
await runFeed(opts as FeedOptions);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
if ((opts as FeedOptions).json) {
|
|
114
|
+
console.log(JSON.stringify({ error: "Subgraph unavailable", details: msg }));
|
|
115
|
+
} else {
|
|
116
|
+
console.error(chalk.red(`Subgraph unavailable: ${msg}`));
|
|
117
|
+
}
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
package/src/commands/deliver.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { printSenderInfo, executeContractWriteViaWallet } from "../wallet-router
|
|
|
8
8
|
import { SERVICE_AGREEMENT_ABI } from "../abis";
|
|
9
9
|
import { readFile } from "fs/promises";
|
|
10
10
|
import prompts from "prompts";
|
|
11
|
+
import { resolveAgentEndpoint, notifyAgent, DEFAULT_REGISTRY_ADDRESS } from "../endpoint-notify";
|
|
11
12
|
|
|
12
13
|
export function registerDeliverCommand(program: Command): void {
|
|
13
14
|
program
|
|
@@ -24,6 +25,7 @@ export function registerDeliverCommand(program: Command): void {
|
|
|
24
25
|
printSenderInfo(config);
|
|
25
26
|
|
|
26
27
|
// Pre-flight: check deadline and legacyFulfillEnabled (J3-01, J3-02)
|
|
28
|
+
let clientAddress = "";
|
|
27
29
|
{
|
|
28
30
|
const { provider: prefProvider } = await getClient(config);
|
|
29
31
|
const saAbi = [
|
|
@@ -42,6 +44,7 @@ export function registerDeliverCommand(program: Command): void {
|
|
|
42
44
|
console.error(`Contact the client to open a new agreement.`);
|
|
43
45
|
process.exit(1);
|
|
44
46
|
}
|
|
47
|
+
clientAddress = String(ag.client ?? "");
|
|
45
48
|
} catch (e) {
|
|
46
49
|
// If it's a contract read failure, skip the check (let the tx reveal the error)
|
|
47
50
|
if (e instanceof Error && !e.message.includes("CALL_EXCEPTION") && !e.message.includes("could not decode")) throw e;
|
|
@@ -112,5 +115,21 @@ export function registerDeliverCommand(program: Command): void {
|
|
|
112
115
|
if (opts.fulfill) await client.fulfill(BigInt(id), hash); else await client.commitDeliverable(BigInt(id), hash);
|
|
113
116
|
}
|
|
114
117
|
console.log(`${opts.fulfill ? 'fulfilled' : 'committed'} ${id} hash=${hash}`);
|
|
118
|
+
|
|
119
|
+
// Notify client's HTTP endpoint (non-blocking)
|
|
120
|
+
if (clientAddress) {
|
|
121
|
+
try {
|
|
122
|
+
const notifyProvider = new ethers.JsonRpcProvider(config.rpcUrl);
|
|
123
|
+
const registryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress ?? DEFAULT_REGISTRY_ADDRESS;
|
|
124
|
+
const endpoint = await resolveAgentEndpoint(clientAddress, notifyProvider, registryAddress);
|
|
125
|
+
await notifyAgent(endpoint, "/delivery", {
|
|
126
|
+
agreementId: id,
|
|
127
|
+
deliverableHash: hash,
|
|
128
|
+
from: signerAddress,
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn(`Warning: could not notify client endpoint: ${err instanceof Error ? err.message : String(err)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
115
134
|
});
|
|
116
135
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
|
|
4
|
+
const SUBGRAPH_URL = "https://api.studio.thegraph.com/query/1744310/arc-402/v0.2.0";
|
|
5
|
+
|
|
6
|
+
const HS_TYPE_LABELS: Record<number, string> = {
|
|
7
|
+
0: "Respected",
|
|
8
|
+
1: "Curious",
|
|
9
|
+
2: "Endorsed",
|
|
10
|
+
3: "Thanked",
|
|
11
|
+
4: "Collaborated",
|
|
12
|
+
5: "Challenged",
|
|
13
|
+
6: "Referred",
|
|
14
|
+
7: "Hello",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function gql(query: string): Promise<Record<string, unknown>> {
|
|
18
|
+
const res = await fetch(SUBGRAPH_URL, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/json" },
|
|
21
|
+
body: JSON.stringify({ query }),
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) throw new Error(`Subgraph HTTP ${res.status}`);
|
|
24
|
+
const json = (await res.json()) as { data?: Record<string, unknown>; errors?: unknown[] };
|
|
25
|
+
if (json.errors?.length) throw new Error(`Subgraph error: ${JSON.stringify(json.errors[0])}`);
|
|
26
|
+
return json.data ?? {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function shortAddr(addr: string): string {
|
|
30
|
+
return addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function utcTime(ts: number): string {
|
|
34
|
+
const d = new Date(ts * 1000);
|
|
35
|
+
const h = d.getUTCHours().toString().padStart(2, "0");
|
|
36
|
+
const m = d.getUTCMinutes().toString().padStart(2, "0");
|
|
37
|
+
return `${h}:${m} UTC`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function weiToEth(wei: string): string {
|
|
41
|
+
return (Number(BigInt(wei)) / 1e18).toFixed(4);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function weiToUsdc(wei: string): string {
|
|
45
|
+
return (Number(BigInt(wei)) / 1e6).toFixed(2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type FeedEvent = {
|
|
49
|
+
type: "handshake" | "hire" | "fulfill" | "vouch";
|
|
50
|
+
timestamp: number;
|
|
51
|
+
raw: Record<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
async function fetchFeedEvents(limit: number, typeFilter?: string, sinceTs?: number): Promise<FeedEvent[]> {
|
|
55
|
+
const includeHandshakes = !typeFilter || typeFilter === "handshake";
|
|
56
|
+
const includeHire = !typeFilter || typeFilter === "hire";
|
|
57
|
+
const includeFulfill = !typeFilter || typeFilter === "fulfill";
|
|
58
|
+
const includeVouch = !typeFilter || typeFilter === "vouch";
|
|
59
|
+
|
|
60
|
+
const tsFilt = sinceTs ? `, where: { timestamp_gt: "${sinceTs}" }` : "";
|
|
61
|
+
const agTsFilt = sinceTs ? `, where: { proposedAt_gt: "${sinceTs}" }` : "";
|
|
62
|
+
|
|
63
|
+
const parts: string[] = [];
|
|
64
|
+
|
|
65
|
+
if (includeHandshakes) {
|
|
66
|
+
parts.push(`
|
|
67
|
+
handshakes(orderBy: timestamp, orderDirection: desc, first: ${limit}${tsFilt}) {
|
|
68
|
+
id from { id name } to { id name } hsType note timestamp isNewConnection
|
|
69
|
+
}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (includeHire || includeFulfill) {
|
|
73
|
+
parts.push(`
|
|
74
|
+
agreements(orderBy: proposedAt, orderDirection: desc, first: ${limit}${agTsFilt}) {
|
|
75
|
+
id client provider serviceType price state proposedAt updatedAt
|
|
76
|
+
}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (includeVouch) {
|
|
80
|
+
parts.push(`
|
|
81
|
+
vouches(orderBy: id, orderDirection: desc, first: ${limit}) {
|
|
82
|
+
id voucher { id name } newAgent { id name } stakeAmount active
|
|
83
|
+
}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const data = await gql(`{ ${parts.join("\n")} }`);
|
|
87
|
+
const events: FeedEvent[] = [];
|
|
88
|
+
|
|
89
|
+
if (includeHandshakes) {
|
|
90
|
+
for (const h of (data.handshakes as Record<string, unknown>[]) ?? []) {
|
|
91
|
+
events.push({ type: "handshake", timestamp: Number(h["timestamp"]), raw: h });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (data["agreements"]) {
|
|
96
|
+
for (const a of data["agreements"] as Record<string, unknown>[]) {
|
|
97
|
+
const state = Number(a["state"]);
|
|
98
|
+
if (includeHire && state === 0) {
|
|
99
|
+
events.push({ type: "hire", timestamp: Number(a["proposedAt"]), raw: a });
|
|
100
|
+
}
|
|
101
|
+
if (includeFulfill && state === 2) {
|
|
102
|
+
events.push({ type: "fulfill", timestamp: Number(a["updatedAt"]), raw: a });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (includeVouch) {
|
|
108
|
+
for (const v of (data["vouches"] as Record<string, unknown>[]) ?? []) {
|
|
109
|
+
events.push({ type: "vouch", timestamp: 0, raw: v });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
events.sort((a, b) => b.timestamp - a.timestamp);
|
|
114
|
+
return events.slice(0, limit);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderFeedEvent(ev: FeedEvent): string {
|
|
118
|
+
const ts = ev.timestamp > 0 ? `[${utcTime(ev.timestamp)}]` : "[ ]";
|
|
119
|
+
|
|
120
|
+
switch (ev.type) {
|
|
121
|
+
case "handshake": {
|
|
122
|
+
const h = ev.raw;
|
|
123
|
+
const from = h["from"] as Record<string, string>;
|
|
124
|
+
const to = h["to"] as Record<string, string>;
|
|
125
|
+
const fromName = from["name"] || shortAddr(from["id"]);
|
|
126
|
+
const toName = to["name"] || shortAddr(to["id"]);
|
|
127
|
+
const label = HS_TYPE_LABELS[Number(h["hsType"])] ?? `Type${h["hsType"]}`;
|
|
128
|
+
const note = h["note"] ? ` "${h["note"]}"` : "";
|
|
129
|
+
return chalk.cyan(`${ts} 🤝 ${fromName} → ${toName} ${label}${note}`);
|
|
130
|
+
}
|
|
131
|
+
case "hire": {
|
|
132
|
+
const a = ev.raw;
|
|
133
|
+
const price = a["price"] ? ` ${weiToEth(a["price"] as string)} ETH` : "";
|
|
134
|
+
return chalk.yellow(
|
|
135
|
+
`${ts} 📋 ${shortAddr(a["client"] as string)} hired ${shortAddr(a["provider"] as string)} ${a["serviceType"]}${price}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
case "fulfill": {
|
|
139
|
+
const a = ev.raw;
|
|
140
|
+
const price = a["price"] ? ` ${weiToEth(a["price"] as string)} ETH released` : "";
|
|
141
|
+
return chalk.green(`${ts} ✅ Agreement #${(a["id"] as string).slice(0, 8)} fulfilled${price}`);
|
|
142
|
+
}
|
|
143
|
+
case "vouch": {
|
|
144
|
+
const v = ev.raw;
|
|
145
|
+
const voucher = v["voucher"] as Record<string, string>;
|
|
146
|
+
const newAgent = v["newAgent"] as Record<string, string>;
|
|
147
|
+
const vName = voucher["name"] || shortAddr(voucher["id"]);
|
|
148
|
+
const nName = newAgent["name"] || shortAddr(newAgent["id"]);
|
|
149
|
+
const stake = v["stakeAmount"] ? ` ${weiToUsdc(v["stakeAmount"] as string)} USDC staked` : "";
|
|
150
|
+
return chalk.magenta(`${ts} 🔗 ${vName} vouched for ${nName}${stake}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface FeedOptions {
|
|
156
|
+
limit?: string;
|
|
157
|
+
live?: boolean;
|
|
158
|
+
type?: string;
|
|
159
|
+
json?: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function runFeed(opts: FeedOptions): Promise<void> {
|
|
163
|
+
const limit = parseInt(opts.limit ?? "20", 10);
|
|
164
|
+
const typeFilter = opts.type;
|
|
165
|
+
|
|
166
|
+
const events = await fetchFeedEvents(limit, typeFilter);
|
|
167
|
+
|
|
168
|
+
if (opts.json) {
|
|
169
|
+
console.log(JSON.stringify(events, null, 2));
|
|
170
|
+
if (!opts.live) return;
|
|
171
|
+
} else {
|
|
172
|
+
for (const ev of [...events].reverse()) {
|
|
173
|
+
console.log(renderFeedEvent(ev));
|
|
174
|
+
}
|
|
175
|
+
if (!opts.live) return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Live mode — keep process alive and poll every 30s
|
|
179
|
+
process.stdin.resume();
|
|
180
|
+
let lastTs = events.length > 0 ? events[0].timestamp : Math.floor(Date.now() / 1000);
|
|
181
|
+
|
|
182
|
+
const interval = setInterval(async () => {
|
|
183
|
+
try {
|
|
184
|
+
const newEvents = await fetchFeedEvents(limit, typeFilter, lastTs);
|
|
185
|
+
if (newEvents.length > 0) {
|
|
186
|
+
if (opts.json) {
|
|
187
|
+
console.log(JSON.stringify(newEvents, null, 2));
|
|
188
|
+
} else {
|
|
189
|
+
console.log(chalk.dim("─".repeat(50)));
|
|
190
|
+
for (const ev of [...newEvents].reverse()) {
|
|
191
|
+
console.log(renderFeedEvent(ev));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
lastTs = newEvents[0].timestamp;
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
if (!opts.json) console.log(chalk.dim(" (subgraph unavailable)"));
|
|
198
|
+
}
|
|
199
|
+
}, 30_000);
|
|
200
|
+
|
|
201
|
+
process.on("SIGINT", () => {
|
|
202
|
+
clearInterval(interval);
|
|
203
|
+
process.exit(0);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function registerFeedCommand(program: Command): void {
|
|
208
|
+
program
|
|
209
|
+
.command("feed")
|
|
210
|
+
.description("Live terminal feed of recent Arena events")
|
|
211
|
+
.option("--limit <n>", "Number of events to show", "20")
|
|
212
|
+
.option("--live", "Poll every 30s for new events")
|
|
213
|
+
.option("--type <type>", "Filter by event type: handshake|hire|fulfill|vouch")
|
|
214
|
+
.option("--json", "Output as JSON")
|
|
215
|
+
.action(async (opts) => {
|
|
216
|
+
try {
|
|
217
|
+
await runFeed(opts as FeedOptions);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
220
|
+
if ((opts as FeedOptions).json) {
|
|
221
|
+
console.log(JSON.stringify({ error: "Subgraph unavailable", details: msg }));
|
|
222
|
+
} else {
|
|
223
|
+
console.error(chalk.red(`Subgraph unavailable: ${msg}`));
|
|
224
|
+
}
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|