@iamnotdou/ccp 0.1.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/cli.js ADDED
@@ -0,0 +1,1006 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CCP — Containment Certificate Protocol CLI
4
+ *
5
+ * Usage: ccp <command> [options]
6
+ *
7
+ * Commands:
8
+ * status Show full system overview
9
+ * cert:get <certHash> Get certificate details
10
+ * cert:lookup <agentAddress> Lookup active certificate by agent address
11
+ * cert:verify <agentAddress> Verify agent meets containment requirements
12
+ * cert:valid <certHash> Check if certificate is valid
13
+ * cert:publish Publish a new certificate (full flow)
14
+ * cert:revoke <certHash> Revoke a certificate (operator only)
15
+ * reserve:status Show reserve vault status
16
+ * reserve:deposit <amount> Deposit USDC into reserve vault
17
+ * reserve:lock <days> Lock reserve for N days
18
+ * spending:status Show spending limit status
19
+ * spending:pay <to> <amount> Execute a payment through SpendingLimit
20
+ * spending:pay:cosign <to> <amt> Execute payment with Ledger co-sign
21
+ * auditor:status [address] Show auditor info
22
+ * auditor:audit Run containment audit
23
+ * auditor:attest <certHash> Full attest flow (audit + stake + sign)
24
+ * challenge:get <id> Get challenge details
25
+ * challenge:list <certHash> List challenges for a certificate
26
+ * hcs:timeline Show HCS event timeline
27
+ * hcs:create-topic Create a new HCS topic
28
+ * addresses Show all contract addresses
29
+ * actors Show all actor addresses
30
+ * help Show this help message
31
+ */
32
+ import "dotenv/config";
33
+ import { formatUnits, formatEther, parseUnits, parseEther, keccak256, encodePacked, } from "viem";
34
+ import { privateKeyToAccount } from "viem/accounts";
35
+ import { publicClient, getWalletClient } from "./client.js";
36
+ import { addresses, keys, hederaConfig } from "./config.js";
37
+ import { CCPRegistryABI, ReserveVaultABI, SpendingLimitABI, AuditorStakingABI, ChallengeManagerABI, } from "./contracts/index.js";
38
+ import { auditContainment, attestCertificate } from "./auditor/attest.js";
39
+ import { ledgerSignCertificate, executeWithLedgerCosign, } from "./ledger/cosigner.js";
40
+ import { publishCertificatePublished, publishAgentTransaction, publishTransactionBlocked, } from "./hcs/publisher.js";
41
+ import { printEventTimeline } from "./hedera/mirrorNode.js";
42
+ // ─── Helpers ───
43
+ const USDC_DECIMALS = 6;
44
+ function fmt(amount) {
45
+ return formatUnits(amount, USDC_DECIMALS);
46
+ }
47
+ function parse(amount) {
48
+ return parseUnits(amount, USDC_DECIMALS);
49
+ }
50
+ const STATUS_NAMES = ["ACTIVE", "REVOKED", "EXPIRED", "CHALLENGED"];
51
+ const CLASS_NAMES = ["NONE", "C1", "C2", "C3"];
52
+ const CHALLENGE_TYPE_NAMES = [
53
+ "RESERVE_SHORTFALL",
54
+ "CONSTRAINT_BYPASS",
55
+ "FALSE_INDEPENDENCE",
56
+ "INVALID_VERIFICATION",
57
+ "SCOPE_NOT_PERFORMED",
58
+ ];
59
+ const CHALLENGE_STATUS_NAMES = [
60
+ "PENDING",
61
+ "UPHELD",
62
+ "REJECTED",
63
+ "INFORMATIONAL",
64
+ ];
65
+ function line(label, value) {
66
+ console.log(` ${label.padEnd(28)} ${value}`);
67
+ }
68
+ function header(title) {
69
+ console.log(`\n${"━".repeat(50)}`);
70
+ console.log(` ${title}`);
71
+ console.log(`${"━".repeat(50)}\n`);
72
+ }
73
+ function divider() {
74
+ console.log(` ${"─".repeat(46)}`);
75
+ }
76
+ function getActorAddresses() {
77
+ return {
78
+ operator: privateKeyToAccount(keys.operator).address,
79
+ auditor: privateKeyToAccount(keys.auditor).address,
80
+ agent: privateKeyToAccount(keys.agent).address,
81
+ ledger: privateKeyToAccount(keys.ledger).address,
82
+ };
83
+ }
84
+ const ERC20_ABI = [
85
+ {
86
+ name: "balanceOf",
87
+ type: "function",
88
+ inputs: [{ name: "account", type: "address" }],
89
+ outputs: [{ type: "uint256" }],
90
+ stateMutability: "view",
91
+ },
92
+ {
93
+ name: "approve",
94
+ type: "function",
95
+ inputs: [
96
+ { name: "spender", type: "address" },
97
+ { name: "amount", type: "uint256" },
98
+ ],
99
+ outputs: [{ type: "bool" }],
100
+ stateMutability: "nonpayable",
101
+ },
102
+ {
103
+ name: "mint",
104
+ type: "function",
105
+ inputs: [
106
+ { name: "to", type: "address" },
107
+ { name: "amount", type: "uint256" },
108
+ ],
109
+ outputs: [],
110
+ stateMutability: "nonpayable",
111
+ },
112
+ ];
113
+ // ─── Commands ───
114
+ async function cmdStatus() {
115
+ header("CCP Protocol Status");
116
+ const actors = getActorAddresses();
117
+ // Spending Limit
118
+ console.log(" SPENDING LIMIT");
119
+ divider();
120
+ const [maxSingle, maxPeriodic, cosignThreshold, ledgerCosigner, remaining] = await Promise.all([
121
+ publicClient.readContract({
122
+ address: addresses.spendingLimit,
123
+ abi: SpendingLimitABI,
124
+ functionName: "maxSingleAction",
125
+ }),
126
+ publicClient.readContract({
127
+ address: addresses.spendingLimit,
128
+ abi: SpendingLimitABI,
129
+ functionName: "maxPeriodicLoss",
130
+ }),
131
+ publicClient.readContract({
132
+ address: addresses.spendingLimit,
133
+ abi: SpendingLimitABI,
134
+ functionName: "cosignThreshold",
135
+ }),
136
+ publicClient.readContract({
137
+ address: addresses.spendingLimit,
138
+ abi: SpendingLimitABI,
139
+ functionName: "ledgerCosigner",
140
+ }),
141
+ publicClient.readContract({
142
+ address: addresses.spendingLimit,
143
+ abi: SpendingLimitABI,
144
+ functionName: "getRemainingAllowance",
145
+ }),
146
+ ]);
147
+ const [spent, limit, periodEnd] = await publicClient.readContract({
148
+ address: addresses.spendingLimit,
149
+ abi: SpendingLimitABI,
150
+ functionName: "getSpentInPeriod",
151
+ });
152
+ line("Max Single Action", `$${fmt(maxSingle)} USDC`);
153
+ line("Max Periodic Loss", `$${fmt(maxPeriodic)} USDC`);
154
+ line("Cosign Threshold", `$${fmt(cosignThreshold)} USDC`);
155
+ line("Ledger Cosigner", ledgerCosigner);
156
+ line("Period Spent", `$${fmt(spent)} / $${fmt(limit)} USDC`);
157
+ line("Remaining Allowance", `$${fmt(remaining)} USDC`);
158
+ line("Period Ends", new Date(Number(periodEnd) * 1000).toISOString());
159
+ // Reserve Vault
160
+ console.log("\n RESERVE VAULT");
161
+ divider();
162
+ const [reserveBalance, isLocked, lockUntil, statedAmount] = await Promise.all([
163
+ publicClient.readContract({
164
+ address: addresses.reserveVault,
165
+ abi: ReserveVaultABI,
166
+ functionName: "getReserveBalance",
167
+ }),
168
+ publicClient.readContract({
169
+ address: addresses.reserveVault,
170
+ abi: ReserveVaultABI,
171
+ functionName: "isLocked",
172
+ }),
173
+ publicClient.readContract({
174
+ address: addresses.reserveVault,
175
+ abi: ReserveVaultABI,
176
+ functionName: "lockUntil",
177
+ }),
178
+ publicClient.readContract({
179
+ address: addresses.reserveVault,
180
+ abi: ReserveVaultABI,
181
+ functionName: "getStatedAmount",
182
+ }),
183
+ ]);
184
+ line("Reserve Balance", `$${fmt(reserveBalance)} USDC`);
185
+ line("Stated Amount", `$${fmt(statedAmount)} USDC`);
186
+ line("Locked", String(isLocked));
187
+ line("Lock Until", Number(lockUntil) > 0
188
+ ? new Date(Number(lockUntil) * 1000).toISOString()
189
+ : "Not locked");
190
+ // Balances (HBAR + USDC)
191
+ console.log("\n BALANCES (HBAR / USDC)");
192
+ divider();
193
+ for (const [name, addr] of Object.entries(actors)) {
194
+ const [hbar, usdc] = await Promise.all([
195
+ publicClient.getBalance({ address: addr }),
196
+ publicClient.readContract({
197
+ address: addresses.usdc,
198
+ abi: ERC20_ABI,
199
+ functionName: "balanceOf",
200
+ args: [addr],
201
+ }),
202
+ ]);
203
+ line(`${name.charAt(0).toUpperCase() + name.slice(1)}`, `${formatEther(hbar)} HBAR | $${fmt(usdc)} USDC`);
204
+ }
205
+ const slBal = await publicClient.readContract({
206
+ address: addresses.usdc,
207
+ abi: ERC20_ABI,
208
+ functionName: "balanceOf",
209
+ args: [addresses.spendingLimit],
210
+ });
211
+ line("SpendingLimit Contract", `$${fmt(slBal)} USDC`);
212
+ // Active certificate for agent
213
+ console.log("\n AGENT CERTIFICATE");
214
+ divider();
215
+ try {
216
+ const certHash = await publicClient.readContract({
217
+ address: addresses.registry,
218
+ abi: CCPRegistryABI,
219
+ functionName: "getActiveCertificate",
220
+ args: [actors.agent],
221
+ });
222
+ if (certHash ===
223
+ "0x0000000000000000000000000000000000000000000000000000000000000000") {
224
+ line("Status", "No active certificate");
225
+ }
226
+ else {
227
+ line("Cert Hash", certHash);
228
+ const cert = await publicClient.readContract({
229
+ address: addresses.registry,
230
+ abi: CCPRegistryABI,
231
+ functionName: "getCertificate",
232
+ args: [certHash],
233
+ });
234
+ line("Class", CLASS_NAMES[cert.certificateClass] || "Unknown");
235
+ line("Status", STATUS_NAMES[cert.status] || "Unknown");
236
+ line("Containment Bound", `$${fmt(cert.containmentBound)} USDC`);
237
+ line("Expires", new Date(Number(cert.expiresAt) * 1000).toISOString());
238
+ line("Valid", String(await publicClient.readContract({
239
+ address: addresses.registry,
240
+ abi: CCPRegistryABI,
241
+ functionName: "isValid",
242
+ args: [certHash],
243
+ })));
244
+ }
245
+ }
246
+ catch {
247
+ line("Status", "No active certificate");
248
+ }
249
+ console.log("");
250
+ }
251
+ async function cmdCertGet(certHashArg) {
252
+ header("Certificate Details");
253
+ const certHash = certHashArg;
254
+ const cert = (await publicClient.readContract({
255
+ address: addresses.registry,
256
+ abi: CCPRegistryABI,
257
+ functionName: "getCertificate",
258
+ args: [certHash],
259
+ }));
260
+ const isValid = await publicClient.readContract({
261
+ address: addresses.registry,
262
+ abi: CCPRegistryABI,
263
+ functionName: "isValid",
264
+ args: [certHash],
265
+ });
266
+ line("Cert Hash", certHash);
267
+ line("Operator", cert.operator);
268
+ line("Agent", cert.agent);
269
+ line("Class", CLASS_NAMES[cert.certificateClass] || "Unknown");
270
+ line("Status", STATUS_NAMES[cert.status] || "Unknown");
271
+ line("Valid", String(isValid));
272
+ line("Containment Bound", `$${fmt(cert.containmentBound)} USDC`);
273
+ line("Issued At", new Date(Number(cert.issuedAt) * 1000).toISOString());
274
+ line("Expires At", new Date(Number(cert.expiresAt) * 1000).toISOString());
275
+ line("Reserve Vault", cert.reserveVault);
276
+ line("Spending Limit", cert.spendingLimit);
277
+ line("IPFS URI", cert.ipfsUri);
278
+ line("Auditors", cert.auditors?.length || 0);
279
+ if (cert.auditors?.length > 0) {
280
+ for (const auditor of cert.auditors) {
281
+ line(" Auditor", auditor);
282
+ }
283
+ }
284
+ console.log("");
285
+ }
286
+ async function cmdCertLookup(agentAddress) {
287
+ header("Certificate Lookup");
288
+ const certHash = await publicClient.readContract({
289
+ address: addresses.registry,
290
+ abi: CCPRegistryABI,
291
+ functionName: "getActiveCertificate",
292
+ args: [agentAddress],
293
+ });
294
+ if (certHash ===
295
+ "0x0000000000000000000000000000000000000000000000000000000000000000") {
296
+ console.log(" No active certificate found for this agent.\n");
297
+ return;
298
+ }
299
+ line("Agent", agentAddress);
300
+ line("Active Cert Hash", certHash);
301
+ console.log("");
302
+ await cmdCertGet(certHash);
303
+ }
304
+ async function cmdCertVerify(agentAddress, minClass = "1", maxLoss = "100000") {
305
+ header("Certificate Verification");
306
+ const maxLossUsdc = parse(maxLoss);
307
+ const [acceptable, certHash] = await publicClient.readContract({
308
+ address: addresses.registry,
309
+ abi: CCPRegistryABI,
310
+ functionName: "verify",
311
+ args: [agentAddress, parseInt(minClass), maxLossUsdc],
312
+ });
313
+ line("Agent", agentAddress);
314
+ line("Min Class Required", `C${minClass}`);
315
+ line("Max Acceptable Loss", `$${maxLoss} USDC`);
316
+ divider();
317
+ line("Acceptable", String(acceptable));
318
+ line("Cert Hash", certHash.slice(0, 18) + "...");
319
+ if (acceptable) {
320
+ console.log("\n VERIFICATION PASSED\n");
321
+ }
322
+ else {
323
+ console.log("\n VERIFICATION FAILED\n");
324
+ }
325
+ }
326
+ async function cmdCertValid(certHash) {
327
+ const isValid = await publicClient.readContract({
328
+ address: addresses.registry,
329
+ abi: CCPRegistryABI,
330
+ functionName: "isValid",
331
+ args: [certHash],
332
+ });
333
+ header("Certificate Validity");
334
+ line("Cert Hash", certHash);
335
+ line("Valid", String(isValid));
336
+ console.log("");
337
+ }
338
+ async function cmdCertPublish() {
339
+ header("Publish Certificate");
340
+ const operatorWallet = getWalletClient(keys.operator);
341
+ const agentWallet = getWalletClient(keys.agent);
342
+ const containmentBound = 50000000000n; // $50k
343
+ const auditorStake = 1500000000n; // $1.5k
344
+ // Generate cert hash
345
+ const certHash = keccak256(encodePacked(["address", "address", "uint256", "string"], [
346
+ agentWallet.account.address,
347
+ operatorWallet.account.address,
348
+ BigInt(Date.now()),
349
+ "ccp-v0.2",
350
+ ]));
351
+ console.log(` Cert Hash: ${certHash}`);
352
+ console.log(` Agent: ${agentWallet.account.address}`);
353
+ console.log(` Operator: ${operatorWallet.account.address}`);
354
+ console.log(` Containment Bound: $${fmt(containmentBound)} USDC\n`);
355
+ // Phase 1: Auditor attestation
356
+ console.log(" [1/3] Auditor audit + stake + attest...");
357
+ const { signature: auditorSig, auditResult } = await attestCertificate(certHash, addresses.spendingLimit, addresses.reserveVault, containmentBound, auditorStake);
358
+ console.log(` Audit: ${auditResult.certClass}`);
359
+ // Phase 2: Operator signs
360
+ console.log("\n [2/3] Operator signing certificate (Ledger)...");
361
+ const operatorSig = await ledgerSignCertificate(certHash);
362
+ // Phase 3: Publish on-chain
363
+ console.log("\n [3/3] Publishing on-chain...");
364
+ const publishParams = {
365
+ certHash,
366
+ agent: agentWallet.account.address,
367
+ certificateClass: 2,
368
+ expiresAt: Math.floor(Date.now() / 1000) + 60 * 24 * 3600,
369
+ containmentBound,
370
+ reserveVault: addresses.reserveVault,
371
+ spendingLimit: addresses.spendingLimit,
372
+ ipfsUri: "ipfs://QmCCPDemoCertificate",
373
+ };
374
+ const tx = await operatorWallet.writeContract({
375
+ address: addresses.registry,
376
+ abi: CCPRegistryABI,
377
+ functionName: "publish",
378
+ args: [publishParams, operatorSig, [auditorSig]],
379
+ });
380
+ await publicClient.waitForTransactionReceipt({ hash: tx });
381
+ console.log(`\n Certificate published!`);
382
+ line("TX", tx);
383
+ line("Cert Hash", certHash);
384
+ // HCS event (non-fatal)
385
+ try {
386
+ await publishCertificatePublished(certHash, agentWallet.account.address, operatorWallet.account.address, "C2", fmt(containmentBound));
387
+ }
388
+ catch {
389
+ console.log(" [HCS] Event skipped");
390
+ }
391
+ console.log("");
392
+ }
393
+ async function cmdCertRevoke(certHash) {
394
+ header("Revoke Certificate");
395
+ const operatorWallet = getWalletClient(keys.operator);
396
+ const tx = await operatorWallet.writeContract({
397
+ address: addresses.registry,
398
+ abi: CCPRegistryABI,
399
+ functionName: "revoke",
400
+ args: [certHash],
401
+ });
402
+ await publicClient.waitForTransactionReceipt({ hash: tx });
403
+ line("Cert Hash", certHash);
404
+ line("TX", tx);
405
+ console.log(" Certificate revoked.\n");
406
+ }
407
+ async function cmdReserveStatus() {
408
+ header("Reserve Vault Status");
409
+ const [balance, statedAmount, isLocked, lockUntil, operator, reserveAsset] = await Promise.all([
410
+ publicClient.readContract({
411
+ address: addresses.reserveVault,
412
+ abi: ReserveVaultABI,
413
+ functionName: "getReserveBalance",
414
+ }),
415
+ publicClient.readContract({
416
+ address: addresses.reserveVault,
417
+ abi: ReserveVaultABI,
418
+ functionName: "getStatedAmount",
419
+ }),
420
+ publicClient.readContract({
421
+ address: addresses.reserveVault,
422
+ abi: ReserveVaultABI,
423
+ functionName: "isLocked",
424
+ }),
425
+ publicClient.readContract({
426
+ address: addresses.reserveVault,
427
+ abi: ReserveVaultABI,
428
+ functionName: "lockUntil",
429
+ }),
430
+ publicClient.readContract({
431
+ address: addresses.reserveVault,
432
+ abi: ReserveVaultABI,
433
+ functionName: "operator",
434
+ }),
435
+ publicClient.readContract({
436
+ address: addresses.reserveVault,
437
+ abi: ReserveVaultABI,
438
+ functionName: "reserveAsset",
439
+ }),
440
+ ]);
441
+ line("Contract", addresses.reserveVault);
442
+ line("Operator", operator);
443
+ line("Reserve Asset", reserveAsset);
444
+ divider();
445
+ line("Balance", `$${fmt(balance)} USDC`);
446
+ line("Stated Amount", `$${fmt(statedAmount)} USDC`);
447
+ line("Locked", String(isLocked));
448
+ line("Lock Until", Number(lockUntil) > 0
449
+ ? new Date(Number(lockUntil) * 1000).toISOString()
450
+ : "Not locked");
451
+ // Adequacy check for C2 (3x)
452
+ const isAdequateC2 = await publicClient.readContract({
453
+ address: addresses.reserveVault,
454
+ abi: ReserveVaultABI,
455
+ functionName: "isAdequate",
456
+ args: [50000000000n, 30000], // $50k bound, 3x ratio
457
+ });
458
+ line("Adequate for C2 ($50k)", String(isAdequateC2));
459
+ console.log("");
460
+ }
461
+ async function cmdReserveDeposit(amountStr) {
462
+ header("Deposit Reserve");
463
+ const amount = parse(amountStr);
464
+ const operatorWallet = getWalletClient(keys.operator);
465
+ console.log(` Depositing $${amountStr} USDC...\n`);
466
+ // Approve
467
+ const approveTx = await operatorWallet.writeContract({
468
+ address: addresses.usdc,
469
+ abi: ERC20_ABI,
470
+ functionName: "approve",
471
+ args: [addresses.reserveVault, amount],
472
+ });
473
+ await publicClient.waitForTransactionReceipt({ hash: approveTx });
474
+ line("Approve TX", approveTx);
475
+ // Deposit
476
+ const depositTx = await operatorWallet.writeContract({
477
+ address: addresses.reserveVault,
478
+ abi: ReserveVaultABI,
479
+ functionName: "deposit",
480
+ args: [amount],
481
+ });
482
+ await publicClient.waitForTransactionReceipt({ hash: depositTx });
483
+ line("Deposit TX", depositTx);
484
+ const newBalance = await publicClient.readContract({
485
+ address: addresses.reserveVault,
486
+ abi: ReserveVaultABI,
487
+ functionName: "getReserveBalance",
488
+ });
489
+ line("New Balance", `$${fmt(newBalance)} USDC`);
490
+ console.log("");
491
+ }
492
+ async function cmdReserveLock(daysStr) {
493
+ header("Lock Reserve");
494
+ const days = parseInt(daysStr);
495
+ const lockUntil = Math.floor(Date.now() / 1000) + days * 24 * 3600;
496
+ const operatorWallet = getWalletClient(keys.operator);
497
+ const tx = await operatorWallet.writeContract({
498
+ address: addresses.reserveVault,
499
+ abi: ReserveVaultABI,
500
+ functionName: "lock",
501
+ args: [lockUntil],
502
+ });
503
+ await publicClient.waitForTransactionReceipt({ hash: tx });
504
+ line("TX", tx);
505
+ line("Lock Until", new Date(lockUntil * 1000).toISOString());
506
+ line("Days", String(days));
507
+ console.log("");
508
+ }
509
+ async function cmdSpendingStatus() {
510
+ header("Spending Limit Status");
511
+ const [agent, maxSingle, maxPeriodic, cosignThreshold, ledgerCosigner, remaining, periodDuration, spendAsset,] = await Promise.all([
512
+ publicClient.readContract({
513
+ address: addresses.spendingLimit,
514
+ abi: SpendingLimitABI,
515
+ functionName: "agent",
516
+ }),
517
+ publicClient.readContract({
518
+ address: addresses.spendingLimit,
519
+ abi: SpendingLimitABI,
520
+ functionName: "maxSingleAction",
521
+ }),
522
+ publicClient.readContract({
523
+ address: addresses.spendingLimit,
524
+ abi: SpendingLimitABI,
525
+ functionName: "maxPeriodicLoss",
526
+ }),
527
+ publicClient.readContract({
528
+ address: addresses.spendingLimit,
529
+ abi: SpendingLimitABI,
530
+ functionName: "cosignThreshold",
531
+ }),
532
+ publicClient.readContract({
533
+ address: addresses.spendingLimit,
534
+ abi: SpendingLimitABI,
535
+ functionName: "ledgerCosigner",
536
+ }),
537
+ publicClient.readContract({
538
+ address: addresses.spendingLimit,
539
+ abi: SpendingLimitABI,
540
+ functionName: "getRemainingAllowance",
541
+ }),
542
+ publicClient.readContract({
543
+ address: addresses.spendingLimit,
544
+ abi: SpendingLimitABI,
545
+ functionName: "periodDuration",
546
+ }),
547
+ publicClient.readContract({
548
+ address: addresses.spendingLimit,
549
+ abi: SpendingLimitABI,
550
+ functionName: "spendAsset",
551
+ }),
552
+ ]);
553
+ const [spent, limit, periodEnd] = await publicClient.readContract({
554
+ address: addresses.spendingLimit,
555
+ abi: SpendingLimitABI,
556
+ functionName: "getSpentInPeriod",
557
+ });
558
+ line("Contract", addresses.spendingLimit);
559
+ line("Agent", agent);
560
+ line("Ledger Cosigner", ledgerCosigner);
561
+ line("Spend Asset", spendAsset);
562
+ divider();
563
+ line("Max Single Action", `$${fmt(maxSingle)} USDC`);
564
+ line("Max Periodic Loss", `$${fmt(maxPeriodic)} USDC`);
565
+ line("Cosign Threshold", `$${fmt(cosignThreshold)} USDC`);
566
+ line("Period Duration", `${Number(periodDuration) / 3600} hours`);
567
+ divider();
568
+ line("Period Spent", `$${fmt(spent)} / $${fmt(limit)} USDC`);
569
+ line("Remaining Allowance", `$${fmt(remaining)} USDC`);
570
+ line("Period Ends", new Date(Number(periodEnd) * 1000).toISOString());
571
+ // Contract USDC balance
572
+ const contractBal = await publicClient.readContract({
573
+ address: addresses.usdc,
574
+ abi: ERC20_ABI,
575
+ functionName: "balanceOf",
576
+ args: [addresses.spendingLimit],
577
+ });
578
+ line("Contract USDC Balance", `$${fmt(contractBal)} USDC`);
579
+ console.log("");
580
+ }
581
+ async function cmdSpendingPay(to, amountStr) {
582
+ header("Execute Payment (Agent Only)");
583
+ const amount = parse(amountStr);
584
+ const agentWallet = getWalletClient(keys.agent);
585
+ console.log(` Sending $${amountStr} USDC to ${to}\n`);
586
+ const tx = await agentWallet.writeContract({
587
+ address: addresses.spendingLimit,
588
+ abi: SpendingLimitABI,
589
+ functionName: "execute",
590
+ args: [to, amount, "0x"],
591
+ });
592
+ await publicClient.waitForTransactionReceipt({ hash: tx });
593
+ const [spent, limit] = await publicClient.readContract({
594
+ address: addresses.spendingLimit,
595
+ abi: SpendingLimitABI,
596
+ functionName: "getSpentInPeriod",
597
+ });
598
+ line("TX", tx);
599
+ line("Amount", `$${amountStr} USDC`);
600
+ line("Period Spent", `$${fmt(spent)} / $${fmt(limit)} USDC`);
601
+ try {
602
+ await publishAgentTransaction(agentWallet.account.address, to, amountStr, false, fmt(spent), fmt(limit));
603
+ }
604
+ catch {
605
+ console.log(" [HCS] Event skipped");
606
+ }
607
+ console.log("");
608
+ }
609
+ async function cmdSpendingPayCosign(to, amountStr) {
610
+ header("Execute Payment (Ledger Co-Sign)");
611
+ const amount = parse(amountStr);
612
+ const agentWallet = getWalletClient(keys.agent);
613
+ console.log(` Sending $${amountStr} USDC to ${to} (with Ledger co-sign)\n`);
614
+ try {
615
+ const tx = await executeWithLedgerCosign(to, amount, addresses.spendingLimit);
616
+ const [spent, limit] = await publicClient.readContract({
617
+ address: addresses.spendingLimit,
618
+ abi: SpendingLimitABI,
619
+ functionName: "getSpentInPeriod",
620
+ });
621
+ line("TX", tx);
622
+ line("Amount", `$${amountStr} USDC`);
623
+ line("Period Spent", `$${fmt(spent)} / $${fmt(limit)} USDC`);
624
+ try {
625
+ await publishAgentTransaction(agentWallet.account.address, to, amountStr, true, fmt(spent), fmt(limit));
626
+ }
627
+ catch {
628
+ console.log(" [HCS] Event skipped");
629
+ }
630
+ }
631
+ catch (error) {
632
+ console.log(" TRANSACTION BLOCKED");
633
+ console.log(` Reason: ${error.message?.slice(0, 120)}`);
634
+ try {
635
+ const [spent] = await publicClient.readContract({
636
+ address: addresses.spendingLimit,
637
+ abi: SpendingLimitABI,
638
+ functionName: "getSpentInPeriod",
639
+ });
640
+ await publishTransactionBlocked(agentWallet.account.address, amountStr, "EXCEEDS_LIMIT", fmt(spent), "50000");
641
+ }
642
+ catch { }
643
+ }
644
+ console.log("");
645
+ }
646
+ async function cmdAuditorStatus(auditorAddress) {
647
+ header("Auditor Status");
648
+ const addr = auditorAddress || privateKeyToAccount(keys.auditor).address;
649
+ const record = (await publicClient.readContract({
650
+ address: addresses.auditorStaking,
651
+ abi: AuditorStakingABI,
652
+ functionName: "getAuditorRecord",
653
+ args: [addr],
654
+ }));
655
+ const totalStaked = await publicClient.readContract({
656
+ address: addresses.auditorStaking,
657
+ abi: AuditorStakingABI,
658
+ functionName: "getTotalStaked",
659
+ args: [addr],
660
+ });
661
+ line("Auditor Address", addr);
662
+ divider();
663
+ line("Total Attestations", String(record.totalAttestations));
664
+ line("Successful Challenges", String(record.successfulChallenges));
665
+ line("Active Stake", `$${fmt(record.activeStake)} USDC`);
666
+ line("Total Staked", `$${fmt(totalStaked)} USDC`);
667
+ // USDC balance
668
+ const bal = await publicClient.readContract({
669
+ address: addresses.usdc,
670
+ abi: ERC20_ABI,
671
+ functionName: "balanceOf",
672
+ args: [addr],
673
+ });
674
+ line("USDC Balance", `$${fmt(bal)} USDC`);
675
+ console.log("");
676
+ }
677
+ async function cmdAuditorAudit() {
678
+ header("Containment Audit");
679
+ await auditContainment(addresses.spendingLimit, addresses.reserveVault, 50000000000n);
680
+ console.log("");
681
+ }
682
+ async function cmdAuditorAttest(certHashArg) {
683
+ header("Auditor Attestation");
684
+ const certHash = certHashArg;
685
+ const stakeAmount = 1500000000n; // $1.5k
686
+ const { signature, auditResult } = await attestCertificate(certHash, addresses.spendingLimit, addresses.reserveVault, 50000000000n, stakeAmount);
687
+ console.log(`\n Attestation complete.`);
688
+ line("Cert Hash", certHash);
689
+ line("Class", auditResult.certClass);
690
+ line("Signature", signature.slice(0, 30) + "...");
691
+ console.log("");
692
+ }
693
+ async function cmdChallengeGet(challengeIdStr) {
694
+ header("Challenge Details");
695
+ const challengeId = BigInt(challengeIdStr);
696
+ const challenge = (await publicClient.readContract({
697
+ address: addresses.challengeManager,
698
+ abi: ChallengeManagerABI,
699
+ functionName: "getChallenge",
700
+ args: [challengeId],
701
+ }));
702
+ line("Challenge ID", challengeIdStr);
703
+ line("Cert Hash", challenge.certHash);
704
+ line("Challenger", challenge.challenger);
705
+ line("Type", CHALLENGE_TYPE_NAMES[challenge.challengeType] || "Unknown");
706
+ line("Status", CHALLENGE_STATUS_NAMES[challenge.status] || "Unknown");
707
+ line("Bond", `$${fmt(challenge.bond)} USDC`);
708
+ line("Submitted At", new Date(Number(challenge.submittedAt) * 1000).toISOString());
709
+ if (Number(challenge.resolvedAt) > 0) {
710
+ line("Resolved At", new Date(Number(challenge.resolvedAt) * 1000).toISOString());
711
+ }
712
+ console.log("");
713
+ }
714
+ async function cmdChallengeList(certHash) {
715
+ header("Challenges for Certificate");
716
+ const ids = (await publicClient.readContract({
717
+ address: addresses.challengeManager,
718
+ abi: ChallengeManagerABI,
719
+ functionName: "getChallengesByCert",
720
+ args: [certHash],
721
+ }));
722
+ line("Cert Hash", certHash);
723
+ line("Total Challenges", String(ids.length));
724
+ if (ids.length > 0) {
725
+ divider();
726
+ for (const id of ids) {
727
+ const c = (await publicClient.readContract({
728
+ address: addresses.challengeManager,
729
+ abi: ChallengeManagerABI,
730
+ functionName: "getChallenge",
731
+ args: [id],
732
+ }));
733
+ console.log(` #${id} ${CHALLENGE_TYPE_NAMES[c.challengeType] || "?"} — ${CHALLENGE_STATUS_NAMES[c.status] || "?"} — bond: $${fmt(c.bond)} USDC`);
734
+ }
735
+ }
736
+ console.log("");
737
+ }
738
+ async function cmdHcsTimeline() {
739
+ if (!hederaConfig.hcsTopicId) {
740
+ console.log("\n No HCS_TOPIC_ID configured in .env\n");
741
+ return;
742
+ }
743
+ await printEventTimeline(hederaConfig.hcsTopicId);
744
+ }
745
+ async function cmdHcsCreateTopic() {
746
+ header("Create HCS Topic");
747
+ const { createCCPTopic } = await import("./hcs/publisher.js");
748
+ const topicId = await createCCPTopic();
749
+ console.log(`\n Topic created: ${topicId}`);
750
+ console.log(` Add to .env: HCS_TOPIC_ID=${topicId}\n`);
751
+ }
752
+ async function cmdFund(targetName, amountStr) {
753
+ header("Fund Accounts (HBAR)");
754
+ const actors = getActorAddresses();
755
+ const operatorWallet = getWalletClient(keys.operator);
756
+ const amount = parseEther(amountStr || "50");
757
+ const hbarAmount = amountStr || "50";
758
+ // If a specific target is given, fund only that one
759
+ if (targetName) {
760
+ const targetAddr = actors[targetName.toLowerCase()] ||
761
+ (targetName.startsWith("0x") ? targetName : null);
762
+ if (!targetAddr) {
763
+ console.log(` Unknown target: ${targetName}`);
764
+ console.log(` Use: operator, auditor, agent, ledger, or an address\n`);
765
+ return;
766
+ }
767
+ const tx = await operatorWallet.sendTransaction({
768
+ to: targetAddr,
769
+ value: amount,
770
+ });
771
+ line("Sent", `${hbarAmount} HBAR to ${targetName}`);
772
+ line("TX", tx);
773
+ console.log("");
774
+ return;
775
+ }
776
+ // Fund all non-operator actors
777
+ for (const [name, addr] of Object.entries(actors)) {
778
+ if (name === "operator")
779
+ continue;
780
+ try {
781
+ const tx = await operatorWallet.sendTransaction({
782
+ to: addr,
783
+ value: amount,
784
+ });
785
+ line(`${name}`, `${hbarAmount} HBAR sent — ${tx.slice(0, 22)}...`);
786
+ }
787
+ catch (e) {
788
+ line(`${name}`, `FAILED — ${e.message?.slice(0, 60)}`);
789
+ }
790
+ }
791
+ console.log("");
792
+ }
793
+ function cmdAddresses() {
794
+ header("Contract Addresses");
795
+ line("CCPRegistry", addresses.registry);
796
+ line("ReserveVault", addresses.reserveVault);
797
+ line("SpendingLimit", addresses.spendingLimit);
798
+ line("AuditorStaking", addresses.auditorStaking);
799
+ line("FeeEscrow", addresses.feeEscrow);
800
+ line("ChallengeManager", addresses.challengeManager);
801
+ line("USDC (Mock)", addresses.usdc);
802
+ divider();
803
+ line("RPC URL", hederaConfig.rpcUrl);
804
+ line("Chain ID", String(hederaConfig.chainId));
805
+ line("Network", hederaConfig.network);
806
+ if (hederaConfig.hcsTopicId) {
807
+ line("HCS Topic", hederaConfig.hcsTopicId);
808
+ }
809
+ console.log("");
810
+ }
811
+ function cmdActors() {
812
+ header("Actor Addresses");
813
+ const actors = getActorAddresses();
814
+ line("Operator", actors.operator);
815
+ line("Auditor", actors.auditor);
816
+ line("Agent", actors.agent);
817
+ line("Ledger", actors.ledger);
818
+ console.log("");
819
+ }
820
+ function cmdHelp() {
821
+ console.log(`
822
+ CCP — Containment Certificate Protocol
823
+
824
+ Usage: ccp <command> [args]
825
+
826
+ OVERVIEW
827
+ status Full system overview
828
+ addresses Show contract addresses
829
+ actors Show actor addresses
830
+ fund [target] [amount] Send HBAR for gas (default: 50 to all)
831
+
832
+ CERTIFICATES
833
+ cert:get <certHash> Get certificate details
834
+ cert:lookup <agentAddress> Lookup active cert by agent
835
+ cert:verify <agent> [minClass] [maxLoss]
836
+ Verify agent containment
837
+ cert:valid <certHash> Check certificate validity
838
+ cert:publish Publish new certificate (full flow)
839
+ cert:revoke <certHash> Revoke certificate
840
+
841
+ RESERVE VAULT
842
+ reserve:status Show reserve vault info
843
+ reserve:deposit <amount> Deposit USDC (e.g. 150000)
844
+ reserve:lock <days> Lock reserve for N days
845
+
846
+ SPENDING LIMITS
847
+ spending:status Show spending limit details
848
+ spending:pay <to> <amount> Pay (agent-only signature)
849
+ spending:pay:cosign <to> <amount> Pay with Ledger co-sign
850
+
851
+ AUDITOR
852
+ auditor:status [address] Show auditor record
853
+ auditor:audit Run containment audit checks
854
+ auditor:attest <certHash> Full attestation flow
855
+
856
+ CHALLENGES
857
+ challenge:get <id> Get challenge details
858
+ challenge:list <certHash> List challenges for cert
859
+
860
+ HCS (HEDERA CONSENSUS SERVICE)
861
+ hcs:timeline Show event timeline
862
+ hcs:create-topic Create new HCS topic
863
+
864
+ EXAMPLES
865
+ ccp status
866
+ ccp cert:lookup 0x89cFD052...
867
+ ccp spending:pay 0xdead... 500
868
+ ccp spending:pay:cosign 0xdead... 7000
869
+ ccp reserve:status
870
+ ccp hcs:timeline
871
+
872
+ INSTALL
873
+ npm install -g ccp-cli Install globally
874
+ npx ccp-cli status Run without installing
875
+
876
+ ENV
877
+ Copy .env.example to .env and fill in your Hedera testnet credentials.
878
+ The CLI reads from .env in the current working directory.
879
+ `);
880
+ }
881
+ // ─── Main ───
882
+ async function main() {
883
+ const args = process.argv.slice(2);
884
+ const command = args[0];
885
+ if (!command || command === "help" || command === "--help" || command === "-h") {
886
+ cmdHelp();
887
+ return;
888
+ }
889
+ try {
890
+ switch (command) {
891
+ case "status":
892
+ await cmdStatus();
893
+ break;
894
+ case "cert:get":
895
+ if (!args[1])
896
+ throw new Error("Usage: cert:get <certHash>");
897
+ await cmdCertGet(args[1]);
898
+ break;
899
+ case "cert:lookup":
900
+ if (!args[1])
901
+ throw new Error("Usage: cert:lookup <agentAddress>");
902
+ await cmdCertLookup(args[1]);
903
+ break;
904
+ case "cert:verify":
905
+ if (!args[1])
906
+ throw new Error("Usage: cert:verify <agentAddress> [minClass] [maxLoss]");
907
+ await cmdCertVerify(args[1], args[2] || "1", args[3] || "100000");
908
+ break;
909
+ case "cert:valid":
910
+ if (!args[1])
911
+ throw new Error("Usage: cert:valid <certHash>");
912
+ await cmdCertValid(args[1]);
913
+ break;
914
+ case "cert:publish":
915
+ await cmdCertPublish();
916
+ break;
917
+ case "cert:revoke":
918
+ if (!args[1])
919
+ throw new Error("Usage: cert:revoke <certHash>");
920
+ await cmdCertRevoke(args[1]);
921
+ break;
922
+ case "reserve:status":
923
+ await cmdReserveStatus();
924
+ break;
925
+ case "reserve:deposit":
926
+ if (!args[1])
927
+ throw new Error("Usage: reserve:deposit <amount>");
928
+ await cmdReserveDeposit(args[1]);
929
+ break;
930
+ case "reserve:lock":
931
+ if (!args[1])
932
+ throw new Error("Usage: reserve:lock <days>");
933
+ await cmdReserveLock(args[1]);
934
+ break;
935
+ case "spending:status":
936
+ await cmdSpendingStatus();
937
+ break;
938
+ case "spending:pay":
939
+ if (!args[1] || !args[2])
940
+ throw new Error("Usage: spending:pay <to> <amount>");
941
+ await cmdSpendingPay(args[1], args[2]);
942
+ break;
943
+ case "spending:pay:cosign":
944
+ if (!args[1] || !args[2])
945
+ throw new Error("Usage: spending:pay:cosign <to> <amount>");
946
+ await cmdSpendingPayCosign(args[1], args[2]);
947
+ break;
948
+ case "auditor:status":
949
+ await cmdAuditorStatus(args[1]);
950
+ break;
951
+ case "auditor:audit":
952
+ await cmdAuditorAudit();
953
+ break;
954
+ case "auditor:attest":
955
+ if (!args[1])
956
+ throw new Error("Usage: auditor:attest <certHash>");
957
+ await cmdAuditorAttest(args[1]);
958
+ break;
959
+ case "challenge:get":
960
+ if (!args[1])
961
+ throw new Error("Usage: challenge:get <challengeId>");
962
+ await cmdChallengeGet(args[1]);
963
+ break;
964
+ case "challenge:list":
965
+ if (!args[1])
966
+ throw new Error("Usage: challenge:list <certHash>");
967
+ await cmdChallengeList(args[1]);
968
+ break;
969
+ case "hcs:timeline":
970
+ await cmdHcsTimeline();
971
+ break;
972
+ case "hcs:create-topic":
973
+ await cmdHcsCreateTopic();
974
+ break;
975
+ case "fund":
976
+ await cmdFund(args[1], args[2]);
977
+ break;
978
+ case "mcp": {
979
+ // Launch MCP server (stdio transport)
980
+ const { execFileSync } = await import("child_process");
981
+ const { fileURLToPath } = await import("url");
982
+ const { dirname, join } = await import("path");
983
+ const __dirname = dirname(fileURLToPath(import.meta.url));
984
+ const mcpPath = join(__dirname, "mcp.js");
985
+ execFileSync("node", [mcpPath], { stdio: "inherit" });
986
+ break;
987
+ }
988
+ case "addresses":
989
+ cmdAddresses();
990
+ break;
991
+ case "actors":
992
+ cmdActors();
993
+ break;
994
+ default:
995
+ console.error(` Unknown command: ${command}\n`);
996
+ cmdHelp();
997
+ process.exit(1);
998
+ }
999
+ }
1000
+ catch (error) {
1001
+ console.error(`\n Error: ${error.message}\n`);
1002
+ process.exit(1);
1003
+ }
1004
+ }
1005
+ main();
1006
+ //# sourceMappingURL=cli.js.map