arc402-cli 0.7.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.
@@ -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 ─────────────────────────────────────────────"));
@@ -340,6 +367,9 @@ async function runCompleteOnboardingCeremony(
340
367
  saveConfig(config);
341
368
 
342
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);
343
373
 
344
374
  // ── Step 5: Agent Registration ─────────────────────────────────────────────
345
375
  console.log("\n" + c.dim("── Step 5: Agent Registration ─────────────────────────────────"));
@@ -428,6 +458,9 @@ async function runCompleteOnboardingCeremony(
428
458
  } else {
429
459
  console.log(" " + c.warning + " AgentRegistry address not configured — skipping");
430
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);
431
464
 
432
465
  // ── Step 7: Workroom Init ─────────────────────────────────────────────────
433
466
  console.log("\n" + c.dim("── Step 7: Workroom ────────────────────────────────────────────"));
@@ -523,6 +556,10 @@ async function runCompleteOnboardingCeremony(
523
556
  ? c.white(agentEndpoint) + c.dim(` → localhost:${relayPort}`)
524
557
  : c.dim("—");
525
558
 
559
+ // Clear onboarding progress — ceremony complete
560
+ delete (config as unknown as Record<string, unknown>).onboardingProgress;
561
+ saveConfig(config);
562
+
526
563
  console.log("\n " + c.success + c.white(" Onboarding complete"));
527
564
  renderTree([
528
565
  { label: "Wallet", value: c.white(walletAddress) },
@@ -1066,19 +1103,54 @@ export function registerWalletCommands(program: Command): void {
1066
1103
  ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1067
1104
  : undefined;
1068
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
+
1069
1138
  // ── Step 1: Connect ────────────────────────────────────────────────────
1139
+ const connectPrompt = isResuming
1140
+ ? "Connect wallet to resume onboarding"
1141
+ : "Approve ARC402Wallet deployment — you will be set as owner";
1070
1142
  const { client, session, account } = await connectPhoneWallet(
1071
1143
  config.walletConnectProjectId,
1072
1144
  chainId,
1073
1145
  config,
1074
- { telegramOpts, prompt: "Approve ARC402Wallet deployment — you will be set as owner", hardware: !!opts.hardware }
1146
+ { telegramOpts, prompt: connectPrompt, hardware: !!opts.hardware }
1075
1147
  );
1076
1148
 
1077
1149
  const networkName = chainId === 8453 ? "Base" : "Base Sepolia";
1078
1150
  const shortAddr = `${account.slice(0, 6)}...${account.slice(-5)}`;
1079
1151
  console.log("\n" + c.success + c.white(` Connected: ${shortAddr} on ${networkName}`));
1080
1152
 
1081
- if (telegramOpts) {
1153
+ if (telegramOpts && !isResuming) {
1082
1154
  // Send "connected" message with a deploy confirmation button.
1083
1155
  // TODO: wire up full callback_data round-trip when a persistent bot process is available.
1084
1156
  await sendTelegramMessage({
@@ -1090,48 +1162,57 @@ export function registerWalletCommands(program: Command): void {
1090
1162
  });
1091
1163
  }
1092
1164
 
1093
- // ── Step 2: Confirm & Deploy ───────────────────────────────────────────
1094
- // WalletConnect approval already confirmed intent — sending automatically
1165
+ let walletAddress: string;
1095
1166
 
1096
- console.log("Deploying...");
1097
- const txHash = await sendTransactionWithSession(client, session, account, chainId, {
1098
- to: factoryAddress,
1099
- data: factoryInterface.encodeFunctionData("createWallet", ["0x0000000071727De22E5E9d8BAf0edAc6f37da032"]),
1100
- value: "0x0",
1101
- });
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
1102
1174
 
1103
- console.log(`\nTransaction submitted: ${txHash}`);
1104
- console.log("Waiting for confirmation...");
1105
- const receipt = await provider.waitForTransaction(txHash);
1106
- if (!receipt) {
1107
- console.error("Transaction not confirmed. Check on-chain.");
1108
- process.exit(1);
1109
- }
1110
- let walletAddress: string | null = null;
1111
- const factoryContract = new ethers.Contract(factoryAddress, WALLET_FACTORY_ABI, provider);
1112
- for (const log of receipt.logs) {
1113
- try {
1114
- const parsed = factoryContract.interface.parseLog(log);
1115
- if (parsed?.name === "WalletCreated") {
1116
- walletAddress = parsed.args.walletAddress as string;
1117
- break;
1118
- }
1119
- } catch { /* skip unparseable logs */ }
1120
- }
1121
- if (!walletAddress) {
1122
- console.error("Could not find WalletCreated event in receipt. Check the transaction on-chain.");
1123
- 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
+ ]);
1124
1215
  }
1125
- // ── Step 1 complete: save wallet + owner immediately ─────────────────
1126
- config.walletContractAddress = walletAddress;
1127
- config.ownerAddress = account;
1128
- saveConfig(config);
1129
- try { fs.chmodSync(getConfigPath(), 0o600); } catch { /* best-effort */ }
1130
- console.log("\n " + c.success + c.white(" Wallet deployed"));
1131
- renderTree([
1132
- { label: "Wallet", value: walletAddress },
1133
- { label: "Owner", value: account, last: true },
1134
- ]);
1135
1216
 
1136
1217
  // ── Steps 2–6: Complete onboarding ceremony (same WalletConnect session)
1137
1218
  const sendTxCeremony = async (
@@ -1175,8 +1256,10 @@ export function registerWalletCommands(program: Command): void {
1175
1256
  const guardianWallet = ethers.Wallet.createRandom();
1176
1257
  config.walletContractAddress = walletAddress;
1177
1258
  config.ownerAddress = address;
1178
- config.guardianPrivateKey = guardianWallet.privateKey;
1179
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;
1180
1263
  saveConfig(config);
1181
1264
 
1182
1265
  // Call setGuardian on the deployed wallet
@@ -1206,7 +1289,7 @@ export function registerWalletCommands(program: Command): void {
1206
1289
  { label: "Wallet", value: walletAddress },
1207
1290
  { label: "Guardian", value: guardianWallet.address, last: true },
1208
1291
  ]);
1209
- 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)`);
1210
1293
  console.log(`Your wallet contract is ready for policy enforcement`);
1211
1294
  printOpenShellHint();
1212
1295
  }
@@ -1438,12 +1521,13 @@ export function registerWalletCommands(program: Command): void {
1438
1521
  console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1439
1522
  process.exit(1);
1440
1523
  }
1441
- if (!config.guardianPrivateKey) {
1442
- 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).`);
1443
1527
  process.exit(1);
1444
1528
  }
1445
1529
  const provider = new ethers.JsonRpcProvider(config.rpcUrl);
1446
- const guardianSigner = new ethers.Wallet(config.guardianPrivateKey, provider);
1530
+ const guardianSigner = new ethers.Wallet(guardianKey, provider);
1447
1531
  const walletContract = new ethers.Contract(config.walletContractAddress, ARC402_WALLET_GUARDIAN_ABI, guardianSigner);
1448
1532
 
1449
1533
  let tx;
@@ -1587,12 +1671,13 @@ export function registerWalletCommands(program: Command): void {
1587
1671
  });
1588
1672
 
1589
1673
  await provider.waitForTransaction(txHash);
1590
- config.guardianPrivateKey = guardianWallet.privateKey;
1674
+ saveGuardianKey(guardianWallet.privateKey);
1675
+ if (config.guardianPrivateKey) delete (config as unknown as Record<string, unknown>).guardianPrivateKey;
1591
1676
  config.guardianAddress = guardianWallet.address;
1592
1677
  saveConfig(config);
1593
1678
  console.log("\n" + c.success + c.white(` Guardian set to: ${guardianWallet.address}`));
1594
1679
  console.log(" " + c.dim("Tx:") + " " + c.white(txHash));
1595
- 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)."));
1596
1681
  console.log(" " + c.warning + " " + c.yellow("The guardian key can freeze your wallet. Store it separately from your hot key."));
1597
1682
  });
1598
1683
 
@@ -2419,9 +2504,10 @@ export function registerWalletCommands(program: Command): void {
2419
2504
  }
2420
2505
  }
2421
2506
 
2422
- // Persist guardian key if generated
2507
+ // Persist guardian key if generated — save to restricted file, not config.json
2423
2508
  if (guardianWallet) {
2424
- config.guardianPrivateKey = guardianWallet.privateKey;
2509
+ saveGuardianKey(guardianWallet.privateKey);
2510
+ if (config.guardianPrivateKey) delete (config as unknown as Record<string, unknown>).guardianPrivateKey;
2425
2511
  config.guardianAddress = guardianWallet.address;
2426
2512
  saveConfig(config);
2427
2513
  }
@@ -2433,7 +2519,7 @@ export function registerWalletCommands(program: Command): void {
2433
2519
  txHashes.forEach((h, i) => console.log(" " + c.dim(`Tx ${i + 1}:`) + " " + c.white(h)));
2434
2520
  }
2435
2521
  if (guardianWallet) {
2436
- 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}`));
2437
2523
  console.log(" " + c.warning + " " + c.yellow("Store the guardian private key separately from your hot key."));
2438
2524
  }
2439
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();
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import * as os from "os";
4
+ import { randomUUID } from "crypto";
4
5
 
5
6
  export interface Arc402Config {
6
7
  network: "base-mainnet" | "base-sepolia";
@@ -40,16 +41,25 @@ export interface Arc402Config {
40
41
  telegramBotToken?: string;
41
42
  telegramChatId?: string;
42
43
  telegramThreadId?: number;
44
+ /** Tracks onboarding progress so `wallet deploy` can resume after interruption. */
45
+ onboardingProgress?: {
46
+ walletAddress: string;
47
+ step: number; // last completed step number (2=machineKey, 3=passkey, 4=policy, 5=agent)
48
+ completedSteps: string[];
49
+ };
43
50
  wcSession?: {
44
51
  topic: string;
45
52
  expiry: number; // Unix timestamp
46
53
  account: string; // Phone wallet address
47
54
  chainId: number;
48
55
  };
56
+ deviceId?: string; // UUID identifying the device this config was created on
57
+ lastCliVersion?: string; // Last CLI version that wrote this config (for upgrade detection)
49
58
  }
50
59
 
51
60
  const CONFIG_DIR = path.join(os.homedir(), ".arc402");
52
61
  const CONFIG_PATH = process.env.ARC402_CONFIG || path.join(CONFIG_DIR, "config.json");
62
+ const DEVICE_ID_PATH = path.join(CONFIG_DIR, "device.id");
53
63
 
54
64
  // WalletConnect project ID — get your own at cloud.walletconnect.com
55
65
  const DEFAULT_WC_PROJECT_ID = "455e9425343b9156fce1428250c9a54a";
@@ -57,7 +67,20 @@ export const getWcProjectId = () => process.env.WC_PROJECT_ID ?? DEFAULT_WC_PROJ
57
67
 
58
68
  export const getConfigPath = () => CONFIG_PATH;
59
69
 
70
+ /** Returns this device's stable UUID, creating it on first call. */
71
+ function getOrCreateDeviceId(): string {
72
+ if (fs.existsSync(DEVICE_ID_PATH)) {
73
+ return fs.readFileSync(DEVICE_ID_PATH, "utf-8").trim();
74
+ }
75
+ const id = randomUUID();
76
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
77
+ fs.writeFileSync(DEVICE_ID_PATH, id, { mode: 0o600 });
78
+ return id;
79
+ }
80
+
60
81
  export function loadConfig(): Arc402Config {
82
+ const thisDeviceId = getOrCreateDeviceId();
83
+
61
84
  if (!fs.existsSync(CONFIG_PATH)) {
62
85
  // Auto-create with Base Mainnet defaults — zero friction
63
86
  const defaults = NETWORK_DEFAULTS["base-mainnet"] ?? {};
@@ -78,13 +101,28 @@ export function loadConfig(): Arc402Config {
78
101
  walletFactoryAddress: defaults.walletFactoryAddress,
79
102
  sessionChannelsAddress: defaults.sessionChannelsAddress,
80
103
  disputeModuleAddress: defaults.disputeModuleAddress,
104
+ deviceId: thisDeviceId,
81
105
  };
82
106
  saveConfig(autoConfig);
83
107
  console.log(`◈ Config auto-created at ${CONFIG_PATH} (Base Mainnet)`);
84
108
  console.log("⚠ Base Mainnet — real funds at risk. Use arc402 config init for testnet.");
85
109
  return autoConfig;
86
110
  }
87
- return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as Arc402Config;
111
+
112
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as Arc402Config;
113
+
114
+ // Multi-device awareness: warn if config was created on a different device
115
+ if (config.deviceId && config.deviceId !== thisDeviceId) {
116
+ console.warn("⚠ This config was created on a different device. Some keys may not work.");
117
+ }
118
+
119
+ // Backfill deviceId if missing (older config)
120
+ if (!config.deviceId) {
121
+ config.deviceId = thisDeviceId;
122
+ saveConfig(config);
123
+ }
124
+
125
+ return config;
88
126
  }
89
127
 
90
128
  export function saveConfig(config: Arc402Config): void {