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.
- 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/config.d.ts.map +1 -1
- package/dist/commands/config.js +11 -1
- package/dist/commands/config.js.map +1 -1
- 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/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +205 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/wallet.d.ts.map +1 -1
- package/dist/commands/wallet.js +192 -58
- 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 +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +35 -3
- package/dist/config.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +359 -220
- package/dist/daemon/index.js.map +1 -1
- package/dist/endpoint-notify.d.ts +9 -1
- package/dist/endpoint-notify.d.ts.map +1 -1
- package/dist/endpoint-notify.js +116 -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 +4 -0
- package/dist/program.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +45 -34
- package/dist/repl.js.map +1 -1
- package/dist/ui/format.d.ts.map +1 -1
- package/dist/ui/format.js +2 -0
- package/dist/ui/format.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/backup.ts +117 -0
- package/src/commands/config.ts +12 -2
- package/src/commands/discover.ts +74 -21
- package/src/commands/doctor.ts +172 -0
- package/src/commands/wallet.ts +194 -57
- package/src/commands/watch.ts +207 -10
- package/src/config.ts +48 -2
- package/src/daemon/index.ts +297 -152
- package/src/endpoint-notify.ts +86 -3
- package/src/index.ts +26 -0
- package/src/program.ts +4 -0
- package/src/repl.ts +53 -42
- package/src/ui/format.ts +1 -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 ─────────────────────────────────────────────"));
|
|
@@ -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
|
-
|
|
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 + "
|
|
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
|
|
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
|
-
.
|
|
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:
|
|
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
|
-
|
|
1043
|
-
// WalletConnect approval already confirmed intent — sending automatically
|
|
1165
|
+
let walletAddress: string;
|
|
1044
1166
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
|
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
|
-
|
|
1391
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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"));
|
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();
|