arc402-cli 0.6.0 → 0.7.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.
Files changed (56) hide show
  1. package/dist/commands/backup.d.ts +3 -0
  2. package/dist/commands/backup.d.ts.map +1 -0
  3. package/dist/commands/backup.js +106 -0
  4. package/dist/commands/backup.js.map +1 -0
  5. package/dist/commands/config.d.ts.map +1 -1
  6. package/dist/commands/config.js +11 -1
  7. package/dist/commands/config.js.map +1 -1
  8. package/dist/commands/discover.d.ts.map +1 -1
  9. package/dist/commands/discover.js +60 -15
  10. package/dist/commands/discover.js.map +1 -1
  11. package/dist/commands/doctor.d.ts +3 -0
  12. package/dist/commands/doctor.d.ts.map +1 -0
  13. package/dist/commands/doctor.js +205 -0
  14. package/dist/commands/doctor.js.map +1 -0
  15. package/dist/commands/wallet.d.ts.map +1 -1
  16. package/dist/commands/wallet.js +192 -58
  17. package/dist/commands/wallet.js.map +1 -1
  18. package/dist/commands/watch.d.ts.map +1 -1
  19. package/dist/commands/watch.js +146 -9
  20. package/dist/commands/watch.js.map +1 -1
  21. package/dist/config.d.ts +9 -0
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +35 -3
  24. package/dist/config.js.map +1 -1
  25. package/dist/daemon/index.d.ts.map +1 -1
  26. package/dist/daemon/index.js +359 -220
  27. package/dist/daemon/index.js.map +1 -1
  28. package/dist/endpoint-notify.d.ts +9 -1
  29. package/dist/endpoint-notify.d.ts.map +1 -1
  30. package/dist/endpoint-notify.js +116 -3
  31. package/dist/endpoint-notify.js.map +1 -1
  32. package/dist/index.js +26 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/program.d.ts.map +1 -1
  35. package/dist/program.js +4 -0
  36. package/dist/program.js.map +1 -1
  37. package/dist/repl.d.ts.map +1 -1
  38. package/dist/repl.js +45 -34
  39. package/dist/repl.js.map +1 -1
  40. package/dist/ui/format.d.ts.map +1 -1
  41. package/dist/ui/format.js +2 -0
  42. package/dist/ui/format.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/commands/backup.ts +117 -0
  45. package/src/commands/config.ts +12 -2
  46. package/src/commands/discover.ts +74 -21
  47. package/src/commands/doctor.ts +172 -0
  48. package/src/commands/wallet.ts +194 -57
  49. package/src/commands/watch.ts +207 -10
  50. package/src/config.ts +48 -2
  51. package/src/daemon/index.ts +297 -152
  52. package/src/endpoint-notify.ts +86 -3
  53. package/src/index.ts +26 -0
  54. package/src/program.ts +4 -0
  55. package/src/repl.ts +53 -42
  56. package/src/ui/format.ts +1 -0
@@ -23,6 +23,27 @@ import { c } from "../ui/colors";
23
23
  import { formatAddress } from "../ui/format";
24
24
 
25
25
  const POLICY_ENGINE_DEFAULT = "0x44102e70c2A366632d98Fe40d892a2501fC7fFF2";
26
+ const GUARDIAN_KEY_PATH = path.join(os.homedir(), ".arc402", "guardian.key");
27
+
28
+ /** Save guardian private key to a restricted standalone file (never to config.json). */
29
+ function saveGuardianKey(privateKey: string): void {
30
+ fs.mkdirSync(path.dirname(GUARDIAN_KEY_PATH), { recursive: true, mode: 0o700 });
31
+ fs.writeFileSync(GUARDIAN_KEY_PATH, privateKey + "\n", { mode: 0o400 });
32
+ }
33
+
34
+ /** Load guardian private key from file, falling back to config for backwards compat. */
35
+ function loadGuardianKey(config: Arc402Config): string | null {
36
+ try {
37
+ return fs.readFileSync(GUARDIAN_KEY_PATH, "utf-8").trim();
38
+ } catch {
39
+ // Migration: if key still in config, migrate it to the file now
40
+ if (config.guardianPrivateKey) {
41
+ saveGuardianKey(config.guardianPrivateKey);
42
+ return config.guardianPrivateKey;
43
+ }
44
+ return null;
45
+ }
46
+ }
26
47
 
27
48
  function parseAmount(raw: string): bigint {
28
49
  const lower = raw.toLowerCase();
@@ -182,6 +203,9 @@ async function runCompleteOnboardingCeremony(
182
203
  );
183
204
  console.log(" " + c.success + " Machine key authorized");
184
205
  }
206
+ // Save progress after machine key step
207
+ config.onboardingProgress = { walletAddress, step: 2, completedSteps: ["machineKey"] };
208
+ saveConfig(config);
185
209
 
186
210
  // ── Step 3: Passkey ───────────────────────────────────────────────────────
187
211
  console.log("\n" + c.dim("── Step 3: Passkey (Face ID / WebAuthn) ──────────────────────"));
@@ -213,6 +237,9 @@ async function runCompleteOnboardingCeremony(
213
237
  console.log(" " + c.success + " Passkey set (via browser)");
214
238
  }
215
239
  }
240
+ // Save progress after passkey step
241
+ config.onboardingProgress = { walletAddress, step: 3, completedSteps: ["machineKey", "passkey"] };
242
+ saveConfig(config);
216
243
 
217
244
  // ── Step 4: Policy ────────────────────────────────────────────────────────
218
245
  console.log("\n" + c.dim("── Step 4: Policy ─────────────────────────────────────────────"));
@@ -267,12 +294,15 @@ async function runCompleteOnboardingCeremony(
267
294
  const guardianInput = (guardianAns.guardian as string | undefined)?.trim() ?? "";
268
295
  if (guardianInput.toLowerCase() === "g") {
269
296
  const generatedGuardian = ethers.Wallet.createRandom();
270
- config.guardianPrivateKey = generatedGuardian.privateKey;
297
+ // Save guardian private key to a separate restricted file, NOT config.json
298
+ const guardianKeyPath = path.join(os.homedir(), ".arc402", "guardian.key");
299
+ fs.mkdirSync(path.dirname(guardianKeyPath), { recursive: true, mode: 0o700 });
300
+ fs.writeFileSync(guardianKeyPath, generatedGuardian.privateKey + "\n", { mode: 0o400 });
301
+ // Only save address (not private key) to config
271
302
  config.guardianAddress = generatedGuardian.address;
272
303
  saveConfig(config);
273
304
  guardianAddress = generatedGuardian.address;
274
- console.log("\n " + c.warning + " IMPORTANT: Save this guardian private key (emergency freeze key):");
275
- console.log(" " + c.dim(generatedGuardian.privateKey));
305
+ console.log("\n " + c.warning + " Guardian key saved to ~/.arc402/guardian.key move offline for security");
276
306
  console.log(" " + c.dim("Address: ") + c.white(generatedGuardian.address) + "\n");
277
307
  const guardianIface = new ethers.Interface(["function setGuardian(address _guardian) external"]);
278
308
  await sendTx(
@@ -337,6 +367,9 @@ async function runCompleteOnboardingCeremony(
337
367
  saveConfig(config);
338
368
 
339
369
  console.log(" " + c.success + " Policy configured");
370
+ // Save progress after policy step
371
+ config.onboardingProgress = { walletAddress, step: 4, completedSteps: ["machineKey", "passkey", "policy"] };
372
+ saveConfig(config);
340
373
 
341
374
  // ── Step 5: Agent Registration ─────────────────────────────────────────────
342
375
  console.log("\n" + c.dim("── Step 5: Agent Registration ─────────────────────────────────"));
@@ -425,6 +458,9 @@ async function runCompleteOnboardingCeremony(
425
458
  } else {
426
459
  console.log(" " + c.warning + " AgentRegistry address not configured — skipping");
427
460
  }
461
+ // Save progress after agent step, then clear on ceremony complete
462
+ config.onboardingProgress = { walletAddress, step: 5, completedSteps: ["machineKey", "passkey", "policy", "agent"] };
463
+ saveConfig(config);
428
464
 
429
465
  // ── Step 7: Workroom Init ─────────────────────────────────────────────────
430
466
  console.log("\n" + c.dim("── Step 7: Workroom ────────────────────────────────────────────"));
@@ -520,6 +556,10 @@ async function runCompleteOnboardingCeremony(
520
556
  ? c.white(agentEndpoint) + c.dim(` → localhost:${relayPort}`)
521
557
  : c.dim("—");
522
558
 
559
+ // Clear onboarding progress — ceremony complete
560
+ delete (config as unknown as Record<string, unknown>).onboardingProgress;
561
+ saveConfig(config);
562
+
523
563
  console.log("\n " + c.success + c.white(" Onboarding complete"));
524
564
  renderTree([
525
565
  { label: "Wallet", value: c.white(walletAddress) },
@@ -686,16 +726,40 @@ export function registerWalletCommands(program: Command): void {
686
726
 
687
727
  // ─── import ────────────────────────────────────────────────────────────────
688
728
 
689
- wallet.command("import <privateKey>")
690
- .description("Import an existing private key")
729
+ wallet.command("import")
730
+ .description("Import an existing private key (use --key-file or stdin prompt)")
691
731
  .option("--network <network>", "Network (base-mainnet or base-sepolia)", "base-sepolia")
692
- .action(async (privateKey, opts) => {
732
+ .option("--key-file <path>", "Read private key from file instead of prompting")
733
+ .action(async (opts) => {
693
734
  const network = opts.network as "base-mainnet" | "base-sepolia";
694
735
  const defaults = NETWORK_DEFAULTS[network];
695
736
  if (!defaults) {
696
737
  console.error(`Unknown network: ${network}. Use base-mainnet or base-sepolia.`);
697
738
  process.exit(1);
698
739
  }
740
+
741
+ let privateKey: string;
742
+ if (opts.keyFile) {
743
+ try {
744
+ privateKey = fs.readFileSync(opts.keyFile, "utf-8").trim();
745
+ } catch (e) {
746
+ console.error(`Cannot read key file: ${e instanceof Error ? e.message : String(e)}`);
747
+ process.exit(1);
748
+ }
749
+ } else {
750
+ // Interactive prompt — hidden input avoids shell history
751
+ const answer = await prompts({
752
+ type: "password",
753
+ name: "key",
754
+ message: "Paste private key (hidden):",
755
+ });
756
+ privateKey = ((answer.key as string | undefined) ?? "").trim();
757
+ if (!privateKey) {
758
+ console.error("No private key entered.");
759
+ process.exit(1);
760
+ }
761
+ }
762
+
699
763
  let imported: ethers.Wallet;
700
764
  try {
701
765
  imported = new ethers.Wallet(privateKey);
@@ -850,8 +914,32 @@ export function registerWalletCommands(program: Command): void {
850
914
  .option("--smart-wallet", "Connect via Base Smart Wallet (Coinbase Wallet SDK) instead of WalletConnect")
851
915
  .option("--hardware", "Hardware wallet mode: show raw wc: URI only (for Ledger Live, Trezor Suite, etc.)")
852
916
  .option("--sponsored", "Use CDP paymaster for gas sponsorship (requires paymasterUrl + cdpKeyName + CDP_PRIVATE_KEY env)")
917
+ .option("--dry-run", "Simulate the deployment ceremony without sending transactions")
853
918
  .action(async (opts) => {
854
919
  const config = loadConfig();
920
+
921
+ if (opts.dryRun) {
922
+ const factoryAddr = config.walletFactoryAddress ?? NETWORK_DEFAULTS[config.network]?.walletFactoryAddress ?? "(not configured)";
923
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
924
+ console.log();
925
+ console.log(" " + c.dim("── Dry run: wallet deploy ──────────────────────────────────────"));
926
+ console.log(" " + c.dim("Network: ") + c.white(config.network));
927
+ console.log(" " + c.dim("Chain ID: ") + c.white(String(chainId)));
928
+ console.log(" " + c.dim("RPC: ") + c.white(config.rpcUrl));
929
+ console.log(" " + c.dim("WalletFactory: ") + c.white(factoryAddr));
930
+ console.log(" " + c.dim("Signing method: ") + c.white(opts.smartWallet ? "Base Smart Wallet" : opts.hardware ? "Hardware (WC URI)" : "WalletConnect"));
931
+ console.log(" " + c.dim("Sponsored: ") + c.white(opts.sponsored ? "yes" : "no"));
932
+ console.log();
933
+ console.log(" " + c.dim("Steps that would run:"));
934
+ console.log(" 1. Connect " + (opts.smartWallet ? "Coinbase Smart Wallet" : "WalletConnect") + " session");
935
+ console.log(" 2. Call WalletFactory.createWallet() → deploy ARC402Wallet");
936
+ console.log(" 3. Save walletContractAddress to config");
937
+ console.log(" 4. Run onboarding ceremony (PolicyEngine, machine key, agent registration)");
938
+ console.log();
939
+ console.log(" " + c.dim("No transactions sent (--dry-run mode)."));
940
+ console.log();
941
+ return;
942
+ }
855
943
  const factoryAddress = config.walletFactoryAddress ?? NETWORK_DEFAULTS[config.network]?.walletFactoryAddress;
856
944
  if (!factoryAddress) {
857
945
  console.error("walletFactoryAddress not found in config or NETWORK_DEFAULTS. Add walletFactoryAddress to your config.");
@@ -1015,19 +1103,54 @@ export function registerWalletCommands(program: Command): void {
1015
1103
  ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1016
1104
  : undefined;
1017
1105
 
1106
+ // ── Resume check ──────────────────────────────────────────────────────
1107
+ const resumeProgress = config.onboardingProgress;
1108
+ const isResuming = !!(
1109
+ resumeProgress?.walletAddress &&
1110
+ resumeProgress.walletAddress === config.walletContractAddress &&
1111
+ config.ownerAddress
1112
+ );
1113
+ if (isResuming) {
1114
+ const stepNames: Record<number, string> = {
1115
+ 2: "machine key", 3: "passkey", 4: "policy setup", 5: "agent registration",
1116
+ };
1117
+ const nextStep = (resumeProgress!.step ?? 1) + 1;
1118
+ console.log(" " + c.dim(`◈ Resuming onboarding from step ${nextStep} (${stepNames[nextStep] ?? "ceremony"})...`));
1119
+ }
1120
+
1121
+ // ── Gas estimation ─────────────────────────────────────────────────────
1122
+ if (!isResuming) {
1123
+ let gasMsg = "~0.003 ETH (6 transactions on Base)";
1124
+ try {
1125
+ const feeData = await provider.getFeeData();
1126
+ const gasPrice = feeData.maxFeePerGas ?? feeData.gasPrice ?? BigInt(1_500_000_000);
1127
+ const deployGas = await provider.estimateGas({
1128
+ to: factoryAddress,
1129
+ data: factoryInterface.encodeFunctionData("createWallet", ["0x0000000071727De22E5E9d8BAf0edAc6f37da032"]),
1130
+ }).catch(() => BigInt(280_000));
1131
+ const ceremonyGas = BigInt(700_000); // ~5 ceremony txs × ~140k each
1132
+ const totalGasEth = parseFloat(ethers.formatEther((deployGas + ceremonyGas) * gasPrice));
1133
+ gasMsg = `~${totalGasEth.toFixed(4)} ETH (6 transactions on Base)`;
1134
+ } catch { /* use default */ }
1135
+ console.log(" " + c.dim(`◈ Estimated gas: ${gasMsg}`));
1136
+ }
1137
+
1018
1138
  // ── Step 1: Connect ────────────────────────────────────────────────────
1139
+ const connectPrompt = isResuming
1140
+ ? "Connect wallet to resume onboarding"
1141
+ : "Approve ARC402Wallet deployment — you will be set as owner";
1019
1142
  const { client, session, account } = await connectPhoneWallet(
1020
1143
  config.walletConnectProjectId,
1021
1144
  chainId,
1022
1145
  config,
1023
- { telegramOpts, prompt: "Approve ARC402Wallet deployment — you will be set as owner", hardware: !!opts.hardware }
1146
+ { telegramOpts, prompt: connectPrompt, hardware: !!opts.hardware }
1024
1147
  );
1025
1148
 
1026
1149
  const networkName = chainId === 8453 ? "Base" : "Base Sepolia";
1027
1150
  const shortAddr = `${account.slice(0, 6)}...${account.slice(-5)}`;
1028
1151
  console.log("\n" + c.success + c.white(` Connected: ${shortAddr} on ${networkName}`));
1029
1152
 
1030
- if (telegramOpts) {
1153
+ if (telegramOpts && !isResuming) {
1031
1154
  // Send "connected" message with a deploy confirmation button.
1032
1155
  // TODO: wire up full callback_data round-trip when a persistent bot process is available.
1033
1156
  await sendTelegramMessage({
@@ -1039,48 +1162,57 @@ export function registerWalletCommands(program: Command): void {
1039
1162
  });
1040
1163
  }
1041
1164
 
1042
- // ── Step 2: Confirm & Deploy ───────────────────────────────────────────
1043
- // WalletConnect approval already confirmed intent — sending automatically
1165
+ let walletAddress: string;
1044
1166
 
1045
- console.log("Deploying...");
1046
- const txHash = await sendTransactionWithSession(client, session, account, chainId, {
1047
- to: factoryAddress,
1048
- data: factoryInterface.encodeFunctionData("createWallet", ["0x0000000071727De22E5E9d8BAf0edAc6f37da032"]),
1049
- value: "0x0",
1050
- });
1167
+ if (isResuming) {
1168
+ // Resume: skip deploy, use existing wallet
1169
+ walletAddress = config.walletContractAddress!;
1170
+ console.log(" " + c.dim(`◈ Using existing wallet: ${walletAddress}`));
1171
+ } else {
1172
+ // ── Step 2: Confirm & Deploy ─────────────────────────────────────────
1173
+ // WalletConnect approval already confirmed intent — sending automatically
1051
1174
 
1052
- console.log(`\nTransaction submitted: ${txHash}`);
1053
- console.log("Waiting for confirmation...");
1054
- const receipt = await provider.waitForTransaction(txHash);
1055
- if (!receipt) {
1056
- console.error("Transaction not confirmed. Check on-chain.");
1057
- process.exit(1);
1058
- }
1059
- let walletAddress: string | null = null;
1060
- const factoryContract = new ethers.Contract(factoryAddress, WALLET_FACTORY_ABI, provider);
1061
- for (const log of receipt.logs) {
1062
- try {
1063
- const parsed = factoryContract.interface.parseLog(log);
1064
- if (parsed?.name === "WalletCreated") {
1065
- walletAddress = parsed.args.walletAddress as string;
1066
- break;
1067
- }
1068
- } catch { /* skip unparseable logs */ }
1069
- }
1070
- if (!walletAddress) {
1071
- console.error("Could not find WalletCreated event in receipt. Check the transaction on-chain.");
1072
- process.exit(1);
1175
+ console.log("Deploying...");
1176
+ const txHash = await sendTransactionWithSession(client, session, account, chainId, {
1177
+ to: factoryAddress,
1178
+ data: factoryInterface.encodeFunctionData("createWallet", ["0x0000000071727De22E5E9d8BAf0edAc6f37da032"]),
1179
+ value: "0x0",
1180
+ });
1181
+
1182
+ console.log(`\nTransaction submitted: ${txHash}`);
1183
+ console.log("Waiting for confirmation...");
1184
+ const receipt = await provider.waitForTransaction(txHash);
1185
+ if (!receipt) {
1186
+ console.error("Transaction not confirmed. Check on-chain.");
1187
+ process.exit(1);
1188
+ }
1189
+ let deployedWallet: string | null = null;
1190
+ const factoryContract = new ethers.Contract(factoryAddress, WALLET_FACTORY_ABI, provider);
1191
+ for (const log of receipt.logs) {
1192
+ try {
1193
+ const parsed = factoryContract.interface.parseLog(log);
1194
+ if (parsed?.name === "WalletCreated") {
1195
+ deployedWallet = parsed.args.walletAddress as string;
1196
+ break;
1197
+ }
1198
+ } catch { /* skip unparseable logs */ }
1199
+ }
1200
+ if (!deployedWallet) {
1201
+ console.error("Could not find WalletCreated event in receipt. Check the transaction on-chain.");
1202
+ process.exit(1);
1203
+ }
1204
+ walletAddress = deployedWallet;
1205
+ // ── Step 1 complete: save wallet + owner immediately ─────────────────
1206
+ config.walletContractAddress = walletAddress;
1207
+ config.ownerAddress = account;
1208
+ saveConfig(config);
1209
+ try { fs.chmodSync(getConfigPath(), 0o600); } catch { /* best-effort */ }
1210
+ console.log("\n " + c.success + c.white(" Wallet deployed"));
1211
+ renderTree([
1212
+ { label: "Wallet", value: walletAddress },
1213
+ { label: "Owner", value: account, last: true },
1214
+ ]);
1073
1215
  }
1074
- // ── Step 1 complete: save wallet + owner immediately ─────────────────
1075
- config.walletContractAddress = walletAddress;
1076
- config.ownerAddress = account;
1077
- saveConfig(config);
1078
- try { fs.chmodSync(getConfigPath(), 0o600); } catch { /* best-effort */ }
1079
- console.log("\n " + c.success + c.white(" Wallet deployed"));
1080
- renderTree([
1081
- { label: "Wallet", value: walletAddress },
1082
- { label: "Owner", value: account, last: true },
1083
- ]);
1084
1216
 
1085
1217
  // ── Steps 2–6: Complete onboarding ceremony (same WalletConnect session)
1086
1218
  const sendTxCeremony = async (
@@ -1124,8 +1256,10 @@ export function registerWalletCommands(program: Command): void {
1124
1256
  const guardianWallet = ethers.Wallet.createRandom();
1125
1257
  config.walletContractAddress = walletAddress;
1126
1258
  config.ownerAddress = address;
1127
- config.guardianPrivateKey = guardianWallet.privateKey;
1128
1259
  config.guardianAddress = guardianWallet.address;
1260
+ // Save key to restricted file — never store in config.json
1261
+ saveGuardianKey(guardianWallet.privateKey);
1262
+ if (config.guardianPrivateKey) delete (config as unknown as Record<string, unknown>).guardianPrivateKey;
1129
1263
  saveConfig(config);
1130
1264
 
1131
1265
  // Call setGuardian on the deployed wallet
@@ -1155,7 +1289,7 @@ export function registerWalletCommands(program: Command): void {
1155
1289
  { label: "Wallet", value: walletAddress },
1156
1290
  { label: "Guardian", value: guardianWallet.address, last: true },
1157
1291
  ]);
1158
- console.log(`Guardian private key saved to config (keep it safe used for emergency freeze only)`);
1292
+ console.log(`Guardian private key saved to ~/.arc402/guardian.key (chmod 400 — keep it safe, used for emergency freeze only)`);
1159
1293
  console.log(`Your wallet contract is ready for policy enforcement`);
1160
1294
  printOpenShellHint();
1161
1295
  }
@@ -1387,12 +1521,13 @@ export function registerWalletCommands(program: Command): void {
1387
1521
  console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1388
1522
  process.exit(1);
1389
1523
  }
1390
- if (!config.guardianPrivateKey) {
1391
- console.error("guardianPrivateKey not set in config. Guardian key was generated during `arc402 wallet deploy`.");
1524
+ const guardianKey = loadGuardianKey(config);
1525
+ if (!guardianKey) {
1526
+ console.error(`Guardian key not found. Expected at ~/.arc402/guardian.key (or guardianPrivateKey in config for legacy setups).`);
1392
1527
  process.exit(1);
1393
1528
  }
1394
1529
  const provider = new ethers.JsonRpcProvider(config.rpcUrl);
1395
- const guardianSigner = new ethers.Wallet(config.guardianPrivateKey, provider);
1530
+ const guardianSigner = new ethers.Wallet(guardianKey, provider);
1396
1531
  const walletContract = new ethers.Contract(config.walletContractAddress, ARC402_WALLET_GUARDIAN_ABI, guardianSigner);
1397
1532
 
1398
1533
  let tx;
@@ -1536,12 +1671,13 @@ export function registerWalletCommands(program: Command): void {
1536
1671
  });
1537
1672
 
1538
1673
  await provider.waitForTransaction(txHash);
1539
- config.guardianPrivateKey = guardianWallet.privateKey;
1674
+ saveGuardianKey(guardianWallet.privateKey);
1675
+ if (config.guardianPrivateKey) delete (config as unknown as Record<string, unknown>).guardianPrivateKey;
1540
1676
  config.guardianAddress = guardianWallet.address;
1541
1677
  saveConfig(config);
1542
1678
  console.log("\n" + c.success + c.white(` Guardian set to: ${guardianWallet.address}`));
1543
1679
  console.log(" " + c.dim("Tx:") + " " + c.white(txHash));
1544
- console.log(" " + c.dim("Guardian private key saved to config."));
1680
+ console.log(" " + c.dim("Guardian private key saved to ~/.arc402/guardian.key (chmod 400)."));
1545
1681
  console.log(" " + c.warning + " " + c.yellow("The guardian key can freeze your wallet. Store it separately from your hot key."));
1546
1682
  });
1547
1683
 
@@ -2368,9 +2504,10 @@ export function registerWalletCommands(program: Command): void {
2368
2504
  }
2369
2505
  }
2370
2506
 
2371
- // Persist guardian key if generated
2507
+ // Persist guardian key if generated — save to restricted file, not config.json
2372
2508
  if (guardianWallet) {
2373
- config.guardianPrivateKey = guardianWallet.privateKey;
2509
+ saveGuardianKey(guardianWallet.privateKey);
2510
+ if (config.guardianPrivateKey) delete (config as unknown as Record<string, unknown>).guardianPrivateKey;
2374
2511
  config.guardianAddress = guardianWallet.address;
2375
2512
  saveConfig(config);
2376
2513
  }
@@ -2382,7 +2519,7 @@ export function registerWalletCommands(program: Command): void {
2382
2519
  txHashes.forEach((h, i) => console.log(" " + c.dim(`Tx ${i + 1}:`) + " " + c.white(h)));
2383
2520
  }
2384
2521
  if (guardianWallet) {
2385
- console.log(" " + c.success + c.dim(` Guardian key saved to config — address: ${guardianWallet.address}`));
2522
+ console.log(" " + c.success + c.dim(` Guardian key saved to ~/.arc402/guardian.key — address: ${guardianWallet.address}`));
2386
2523
  console.log(" " + c.warning + " " + c.yellow("Store the guardian private key separately from your hot key."));
2387
2524
  }
2388
2525
  console.log(c.dim("\nVerify with: arc402 wallet status && arc402 wallet policy show"));
@@ -1,21 +1,218 @@
1
1
  import { Command } from "commander";
2
- import { c } from "../ui/colors";
2
+ import { ethers } from "ethers";
3
3
  import { loadConfig } from "../config";
4
+ import { getClient } from "../client";
5
+ import { c } from "../ui/colors";
6
+
7
+ // ─── Minimal ABIs for event watching ─────────────────────────────────────────
8
+
9
+ const AGENT_REGISTRY_WATCH_ABI = [
10
+ "event AgentRegistered(address indexed wallet, string name, string serviceType, uint256 timestamp)",
11
+ "event AgentUpdated(address indexed wallet, string name, string serviceType)",
12
+ "event AgentDeactivated(address indexed wallet)",
13
+ ];
14
+
15
+ const SERVICE_AGREEMENT_WATCH_ABI = [
16
+ "event AgreementProposed(uint256 indexed id, address indexed client, address indexed provider, string serviceType, uint256 price, address token, uint256 deadline)",
17
+ "event AgreementAccepted(uint256 indexed id, address indexed provider)",
18
+ "event AgreementFulfilled(uint256 indexed id, address indexed provider, bytes32 deliverablesHash)",
19
+ "event AgreementDisputed(uint256 indexed id, address indexed initiator, string reason)",
20
+ "event AgreementCancelled(uint256 indexed id, address indexed client)",
21
+ ];
22
+
23
+ const HANDSHAKE_WATCH_ABI = [
24
+ "event HandshakeSent(uint256 indexed handshakeId, address indexed from, address indexed to, uint8 hsType, address token, uint256 amount, string note, uint256 timestamp)",
25
+ ];
26
+
27
+ const DISPUTE_MODULE_WATCH_ABI = [
28
+ "event DisputeRaised(uint256 indexed agreementId, address indexed initiator, string reason)",
29
+ "event DisputeResolved(uint256 indexed agreementId, bool favorProvider, string resolution)",
30
+ ];
31
+
32
+ const HS_TYPE_LABELS: Record<number, string> = {
33
+ 0: "Respected",
34
+ 1: "Curious",
35
+ 2: "Endorsed",
36
+ 3: "Thanked",
37
+ 4: "Collaborated",
38
+ 5: "Challenged",
39
+ 6: "Referred",
40
+ 7: "Hello",
41
+ };
42
+
43
+ function shortAddr(addr: string): string {
44
+ return addr.length > 10 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr;
45
+ }
46
+
47
+ function nowHHMM(): string {
48
+ const d = new Date();
49
+ const h = d.getHours().toString().padStart(2, "0");
50
+ const m = d.getMinutes().toString().padStart(2, "0");
51
+ return `${h}:${m}`;
52
+ }
53
+
54
+ function printEvent(label: string, detail: string, status?: "ok" | "warn" | "err"): void {
55
+ const ts = c.dim(`[${nowHHMM()}]`);
56
+ const col = status === "ok" ? c.green : status === "err" ? c.red : status === "warn" ? c.yellow : c.white;
57
+ process.stdout.write(` ${ts} ${col(label)} ${c.dim(detail)}\n`);
58
+ }
4
59
 
5
60
  export function registerWatchCommand(program: Command): void {
6
61
  program
7
62
  .command("watch")
8
- .description("Watch wallet activity in real-time")
63
+ .description("Watch wallet activity in real-time (live onchain event feed)")
9
64
  .action(async () => {
10
65
  const config = loadConfig();
11
- const wallet = config.walletContractAddress ?? "(no wallet)";
12
- const shortWallet = wallet.length > 10
13
- ? `${wallet.slice(0, 6)}...${wallet.slice(-4)}`
14
- : wallet;
15
- const line = "─".repeat(20);
16
-
17
- console.log(`${c.mark} ARC-402 Watching ${c.white(shortWallet)} ${c.dim(line)}`);
18
- console.log(`${c.dim("···")} ${c.dim("waiting")}`);
66
+ const { provider } = await getClient(config);
67
+ const myWallet = (config.walletContractAddress ?? "").toLowerCase();
68
+ const shortMe = myWallet ? shortAddr(config.walletContractAddress!) : "(no wallet)";
69
+
70
+ const line = "─".repeat(22);
71
+ console.log(`\n ${c.mark} ${c.white("ARC-402 Live Feed")} ${c.dim(line)}`);
72
+ console.log(` ${c.dim("Watching")} ${c.brightCyan(shortMe)} ${c.dim("on")} ${c.dim(config.network)}`);
73
+ console.log(` ${c.dim("Ctrl+C to exit")}\n`);
74
+
75
+ // ── Build contract instances ───────────────────────────────────────────
76
+
77
+ const contractLabels: string[] = [];
78
+
79
+ if (config.agentRegistryAddress) contractLabels.push("AgentRegistry");
80
+ if (config.serviceAgreementAddress) contractLabels.push("ServiceAgreement");
81
+ if (config.handshakeAddress) contractLabels.push("Handshake");
82
+ if (config.disputeModuleAddress) contractLabels.push("DisputeModule");
83
+
84
+ if (contractLabels.length === 0) {
85
+ console.log(` ${c.warning} No contract addresses configured. Run arc402 config init.`);
86
+ process.exit(1);
87
+ }
88
+
89
+ console.log(` ${c.dim(`Monitoring ${contractLabels.length} contract${contractLabels.length !== 1 ? "s" : ""}: ${contractLabels.join(", ")}`)}\n`);
90
+
91
+ // ── Helpers ────────────────────────────────────────────────────────────
92
+
93
+ function isMe(addr: string): boolean {
94
+ return myWallet !== "" && addr.toLowerCase() === myWallet;
95
+ }
96
+
97
+ function fmtAddr(addr: string): string {
98
+ return isMe(addr) ? c.brightCyan("you") : c.dim(shortAddr(addr));
99
+ }
100
+
101
+ // ── AgentRegistry ──────────────────────────────────────────────────────
102
+
103
+ if (config.agentRegistryAddress) {
104
+ const reg = new ethers.Contract(config.agentRegistryAddress, AGENT_REGISTRY_WATCH_ABI, provider);
105
+
106
+ reg.on("AgentRegistered", (wallet: string, name: string, serviceType: string) => {
107
+ printEvent(`Agent registered: ${name}`, `${fmtAddr(wallet)} ${c.dim(serviceType)}`, "ok");
108
+ });
109
+
110
+ reg.on("AgentUpdated", (wallet: string, name: string, serviceType: string) => {
111
+ printEvent(`Agent updated: ${name}`, `${fmtAddr(wallet)} ${c.dim(serviceType)}`);
112
+ });
113
+
114
+ reg.on("AgentDeactivated", (wallet: string) => {
115
+ printEvent(`Agent deactivated`, fmtAddr(wallet), "warn");
116
+ });
117
+ }
118
+
119
+ // ── ServiceAgreement ───────────────────────────────────────────────────
120
+
121
+ if (config.serviceAgreementAddress) {
122
+ const sa = new ethers.Contract(config.serviceAgreementAddress, SERVICE_AGREEMENT_WATCH_ABI, provider);
123
+
124
+ sa.on("AgreementProposed", (id: bigint, client: string, agentProvider: string, serviceType: string) => {
125
+ const involved = isMe(client) || isMe(agentProvider);
126
+ printEvent(
127
+ `Agreement #${id} proposed`,
128
+ `${fmtAddr(client)} → ${fmtAddr(agentProvider)} ${c.dim(serviceType)}`,
129
+ involved ? "ok" : undefined
130
+ );
131
+ });
132
+
133
+ sa.on("AgreementAccepted", (id: bigint, agentProvider: string) => {
134
+ printEvent(`Agreement #${id} → ${c.green("ACCEPTED")}`, fmtAddr(agentProvider));
135
+ });
136
+
137
+ sa.on("AgreementFulfilled", (id: bigint, agentProvider: string, deliverablesHash: string) => {
138
+ printEvent(
139
+ `Agreement #${id} → ${c.green("DELIVERED")}`,
140
+ `${fmtAddr(agentProvider)} ${c.dim(deliverablesHash.slice(0, 10) + "...")}`,
141
+ "ok"
142
+ );
143
+ });
144
+
145
+ sa.on("AgreementDisputed", (id: bigint, initiator: string, reason: string) => {
146
+ printEvent(
147
+ `Agreement #${id} → ${c.red("DISPUTED")}`,
148
+ `${fmtAddr(initiator)} ${c.dim(reason.slice(0, 40))}`,
149
+ "err"
150
+ );
151
+ });
152
+
153
+ sa.on("AgreementCancelled", (id: bigint, client: string) => {
154
+ printEvent(`Agreement #${id} → ${c.yellow("CANCELLED")}`, fmtAddr(client), "warn");
155
+ });
156
+ }
157
+
158
+ // ── Handshake ──────────────────────────────────────────────────────────
159
+
160
+ if (config.handshakeAddress) {
161
+ const hs = new ethers.Contract(config.handshakeAddress, HANDSHAKE_WATCH_ABI, provider);
162
+
163
+ hs.on("HandshakeSent", (_id: bigint, from: string, to: string, hsType: number, _token: string, _amount: bigint, note: string) => {
164
+ const typeLabel = HS_TYPE_LABELS[hsType] ?? `type ${hsType}`;
165
+ const toMe = isMe(to);
166
+ const noteStr = note ? ` ${c.dim(`(${note.slice(0, 30)})`)}` : "";
167
+ printEvent(
168
+ `Handshake from ${fmtAddr(from)} → ${fmtAddr(to)}`,
169
+ `${c.dim(typeLabel)}${noteStr}`,
170
+ toMe ? "ok" : undefined
171
+ );
172
+ });
173
+ }
174
+
175
+ // ── DisputeModule ──────────────────────────────────────────────────────
176
+
177
+ if (config.disputeModuleAddress) {
178
+ const dm = new ethers.Contract(config.disputeModuleAddress, DISPUTE_MODULE_WATCH_ABI, provider);
179
+
180
+ dm.on("DisputeRaised", (agreementId: bigint, initiator: string, reason: string) => {
181
+ printEvent(
182
+ `Dispute raised on #${agreementId}`,
183
+ `${fmtAddr(initiator)} ${c.dim(reason.slice(0, 40))}`,
184
+ "err"
185
+ );
186
+ });
187
+
188
+ dm.on("DisputeResolved", (agreementId: bigint, favorProvider: boolean, resolution: string) => {
189
+ printEvent(
190
+ `Dispute #${agreementId} → ${c.green("RESOLVED")}`,
191
+ `${c.dim(favorProvider ? "provider wins" : "client wins")} ${c.dim(resolution.slice(0, 30))}`,
192
+ "ok"
193
+ );
194
+ });
195
+ }
196
+
197
+ // ── Block heartbeat (shows feed is alive) ──────────────────────────────
198
+
199
+ let lastBlock = 0;
200
+ provider.on("block", (blockNumber: number) => {
201
+ if (blockNumber > lastBlock) {
202
+ lastBlock = blockNumber;
203
+ if (blockNumber % 10 === 0) {
204
+ process.stdout.write(` ${c.dim(`· block ${blockNumber}`)}\n`);
205
+ }
206
+ }
207
+ });
208
+
209
+ // ── Clean exit ─────────────────────────────────────────────────────────
210
+
211
+ process.on("SIGINT", () => {
212
+ console.log(`\n ${c.dim("Feed stopped.")}`);
213
+ provider.removeAllListeners();
214
+ process.exit(0);
215
+ });
19
216
 
20
217
  // Keep process alive
21
218
  process.stdin.resume();