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.
- package/dist/commands/backup.d.ts +3 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +106 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/discover.d.ts.map +1 -1
- package/dist/commands/discover.js +60 -15
- package/dist/commands/discover.js.map +1 -1
- package/dist/commands/wallet.d.ts.map +1 -1
- package/dist/commands/wallet.js +136 -52
- package/dist/commands/wallet.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +146 -9
- package/dist/commands/watch.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +25 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +65 -12
- package/dist/daemon/index.js.map +1 -1
- package/dist/endpoint-notify.d.ts +2 -1
- package/dist/endpoint-notify.d.ts.map +1 -1
- package/dist/endpoint-notify.js +12 -3
- package/dist/endpoint-notify.js.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +2 -0
- package/dist/program.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/backup.ts +117 -0
- package/src/commands/discover.ts +74 -21
- package/src/commands/wallet.ts +137 -51
- package/src/commands/watch.ts +207 -10
- package/src/config.ts +39 -1
- package/src/daemon/index.ts +63 -12
- package/src/endpoint-notify.ts +13 -3
- package/src/index.ts +26 -0
- package/src/program.ts +2 -0
package/src/commands/wallet.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
1094
|
-
// WalletConnect approval already confirmed intent — sending automatically
|
|
1165
|
+
let walletAddress: string;
|
|
1095
1166
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
|
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
|
-
|
|
1442
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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"));
|
package/src/commands/watch.ts
CHANGED
|
@@ -1,21 +1,218 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import {
|
|
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
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const line = "─".repeat(
|
|
16
|
-
|
|
17
|
-
console.log(
|
|
18
|
-
console.log(
|
|
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
|
-
|
|
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 {
|