@elytro/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +89 -0
  2. package/dist/index.js +599 -576
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1375,10 +1375,11 @@ var AccountService = class {
1375
1375
  * Multiple accounts per chain are allowed — each gets a unique
1376
1376
  * CREATE2 index, producing a different contract address.
1377
1377
  *
1378
- * @param chainId - Required. The target chain.
1379
- * @param alias - Optional. Human-readable name. Auto-generated if omitted.
1378
+ * @param chainId - Required. The target chain.
1379
+ * @param alias - Optional. Human-readable name. Auto-generated if omitted.
1380
+ * @param securityIntent - Optional. Security intent (email, dailyLimit) to execute during activate.
1380
1381
  */
1381
- async createAccount(chainId, alias) {
1382
+ async createAccount(chainId, alias, securityIntent) {
1382
1383
  const owner = this.keyring.currentOwner;
1383
1384
  if (!owner) {
1384
1385
  throw new Error("Keyring is locked. Unlock first.");
@@ -1396,7 +1397,8 @@ var AccountService = class {
1396
1397
  owner,
1397
1398
  index,
1398
1399
  isDeployed: false,
1399
- isRecoveryEnabled: false
1400
+ isRecoveryEnabled: false,
1401
+ ...securityIntent && { securityIntent }
1400
1402
  };
1401
1403
  this.state.accounts.push(account);
1402
1404
  this.state.currentAddress = address2;
@@ -1467,6 +1469,45 @@ var AccountService = class {
1467
1469
  balance
1468
1470
  };
1469
1471
  }
1472
+ // ─── Security Intent / Status ─────────────────────────────
1473
+ /**
1474
+ * Patch the temporary security intent (e.g. store emailBindingId).
1475
+ * Only valid before activate — intent is deleted after activate.
1476
+ */
1477
+ async updateSecurityIntent(address2, chainId, patch) {
1478
+ const account = this.findAccount(address2, chainId);
1479
+ account.securityIntent = { ...account.securityIntent, ...patch };
1480
+ await this.persist();
1481
+ }
1482
+ /**
1483
+ * Called after activate succeeds with hook install:
1484
+ * 1. Write persistent SecurityStatus
1485
+ * 2. Delete temporary SecurityIntent (consumed)
1486
+ */
1487
+ async finalizeSecurityIntent(address2, chainId) {
1488
+ const account = this.findAccount(address2, chainId);
1489
+ account.securityStatus = { hookInstalled: true };
1490
+ delete account.securityIntent;
1491
+ await this.persist();
1492
+ }
1493
+ /**
1494
+ * Delete security intent without writing status.
1495
+ * Used if activate succeeds but hook batching failed (deploy-only).
1496
+ */
1497
+ async clearSecurityIntent(address2, chainId) {
1498
+ const account = this.findAccount(address2, chainId);
1499
+ delete account.securityIntent;
1500
+ await this.persist();
1501
+ }
1502
+ findAccount(address2, chainId) {
1503
+ const account = this.state.accounts.find(
1504
+ (a) => a.address.toLowerCase() === address2.toLowerCase() && a.chainId === chainId
1505
+ );
1506
+ if (!account) {
1507
+ throw new Error(`Account ${address2} on chain ${chainId} not found.`);
1508
+ }
1509
+ return account;
1510
+ }
1470
1511
  // ─── Import / Export ────────────────────────────────────────────
1471
1512
  async importAccounts(accounts) {
1472
1513
  let imported = 0;
@@ -1787,9 +1828,9 @@ var SecurityHookService = class {
1787
1828
  throw err;
1788
1829
  }
1789
1830
  }
1790
- isAuthError(error2) {
1791
- if (!error2 || typeof error2 !== "object") return false;
1792
- const msg = String(error2.message ?? "").toLowerCase();
1831
+ isAuthError(error) {
1832
+ if (!error || typeof error !== "object") return false;
1833
+ const msg = String(error.message ?? "").toLowerCase();
1793
1834
  return msg.includes("forbidden") || msg.includes("unauthorized") || msg.includes("session") || msg.includes("expired") || msg.includes("failed to authenticate");
1794
1835
  }
1795
1836
  // ─── On-chain Hook Status ────────────────────────────────────
@@ -2229,12 +2270,30 @@ async function createAppContext() {
2229
2270
  walletClient.initForChain(defaultChain);
2230
2271
  await sdk.initForChain(defaultChain);
2231
2272
  const { loadProvider } = await resolveProvider();
2232
- if (loadProvider && await keyring.isInitialized()) {
2273
+ const isInitialized = await keyring.isInitialized();
2274
+ if (isInitialized) {
2275
+ if (!loadProvider) {
2276
+ throw new Error(
2277
+ "Wallet is initialized but no secret provider is available.\n" + (process.platform === "darwin" ? "Keychain access failed. Check macOS Keychain permissions." : "Set the ELYTRO_VAULT_SECRET environment variable.")
2278
+ );
2279
+ }
2233
2280
  const vaultKey = await loadProvider.load();
2234
- if (vaultKey) {
2281
+ if (!vaultKey) {
2282
+ throw new Error(
2283
+ `Wallet is initialized but vault key not found in ${loadProvider.name}.
2284
+ ` + (process.platform === "darwin" ? "The Keychain item may have been deleted. Re-run `elytro init` to create a new wallet,\nor import a backup with `elytro import`." : "Set ELYTRO_VAULT_SECRET to the base64-encoded vault key.")
2285
+ );
2286
+ }
2287
+ try {
2235
2288
  await keyring.unlock(vaultKey);
2289
+ } catch (err) {
2236
2290
  vaultKey.fill(0);
2291
+ throw new Error(
2292
+ `Wallet unlock failed: ${err.message}
2293
+ The vault key may not match the encrypted keyring. Re-run \`elytro init\` or import a backup.`
2294
+ );
2237
2295
  }
2296
+ vaultKey.fill(0);
2238
2297
  }
2239
2298
  const account = new AccountService({
2240
2299
  store,
@@ -2264,23 +2323,6 @@ import ora from "ora";
2264
2323
 
2265
2324
  // src/utils/display.ts
2266
2325
  import chalk from "chalk";
2267
- function heading(text) {
2268
- console.log(chalk.bold.cyan(`
2269
- ${text}
2270
- `));
2271
- }
2272
- function info(label, value) {
2273
- console.log(` ${chalk.gray(label + ":")} ${value}`);
2274
- }
2275
- function success(text) {
2276
- console.log(chalk.green(`\u2714 ${text}`));
2277
- }
2278
- function warn(text) {
2279
- console.log(chalk.yellow(`\u26A0 ${text}`));
2280
- }
2281
- function error(text) {
2282
- console.error(chalk.red(`\u2716 ${text}`));
2283
- }
2284
2326
  function txError(payload) {
2285
2327
  const output = {
2286
2328
  success: false,
@@ -2292,15 +2334,6 @@ function txError(payload) {
2292
2334
  };
2293
2335
  console.error(chalk.red(JSON.stringify(output, null, 2)));
2294
2336
  }
2295
- function table(rows, columns) {
2296
- const header = columns.map((c) => c.label.padEnd(c.width ?? 20)).join(" ");
2297
- console.log(chalk.bold(header));
2298
- console.log(chalk.gray("\u2500".repeat(header.length)));
2299
- for (const row of rows) {
2300
- const line = columns.map((c) => (row[c.key] ?? "").padEnd(c.width ?? 20)).join(" ");
2301
- console.log(line);
2302
- }
2303
- }
2304
2337
  function address(addr) {
2305
2338
  if (addr.length <= 14) return addr;
2306
2339
  return `${addr.slice(0, 8)}...${addr.slice(-6)}`;
@@ -2314,50 +2347,51 @@ function maskApiKeys(url) {
2314
2347
  function sanitizeErrorMessage(message) {
2315
2348
  return message.replace(/https?:\/\/[^\s"']+/gi, (match) => maskApiKeys(match));
2316
2349
  }
2350
+ function outputResult(result) {
2351
+ console.log(JSON.stringify({ success: true, result }, null, 2));
2352
+ }
2353
+ function outputError(code, message, data) {
2354
+ txError({ code, message: sanitizeErrorMessage(message), data });
2355
+ process.exitCode = 1;
2356
+ }
2317
2357
 
2318
2358
  // src/commands/init.ts
2319
2359
  function registerInitCommand(program2, ctx) {
2320
2360
  program2.command("init").description("Initialize a new Elytro wallet").action(async () => {
2321
2361
  if (await ctx.keyring.isInitialized()) {
2322
- warn("Wallet already initialized.");
2323
- info("Data", ctx.store.dataDir);
2324
- info("Hint", "Use `elytro account create` to create a smart account.");
2362
+ outputResult({
2363
+ status: "already_initialized",
2364
+ dataDir: ctx.store.dataDir,
2365
+ hint: "Use `elytro account create` to create a smart account."
2366
+ });
2325
2367
  return;
2326
2368
  }
2327
- heading("Initialize Elytro Wallet");
2328
2369
  const spinner = ora("Setting up wallet...").start();
2329
2370
  try {
2330
2371
  const vaultKey = webcrypto2.getRandomValues(new Uint8Array(32));
2331
2372
  const { initProvider } = await resolveProvider();
2373
+ let providerName = null;
2374
+ let vaultSecretB64 = null;
2332
2375
  if (initProvider) {
2333
2376
  await initProvider.store(vaultKey);
2334
- spinner.text = `Vault key stored in ${initProvider.name}.`;
2377
+ providerName = initProvider.name;
2335
2378
  } else {
2336
- spinner.stop();
2337
- const b64 = Buffer.from(vaultKey).toString("base64");
2338
- console.log("");
2339
- warn("No persistent secret provider available (not on macOS).");
2340
- warn("Save the following vault secret \u2014 it will NOT be shown again:");
2341
- console.log("");
2342
- console.log(` ELYTRO_VAULT_SECRET="${b64}"`);
2343
- console.log("");
2344
- info("Hint", "Set this environment variable before running any elytro command.");
2345
- spinner.start("Creating wallet...");
2379
+ vaultSecretB64 = Buffer.from(vaultKey).toString("base64");
2346
2380
  }
2347
2381
  await ctx.keyring.createNewOwner(vaultKey);
2348
2382
  vaultKey.fill(0);
2349
- spinner.succeed("Wallet initialized.");
2350
- console.log("");
2351
- info("Data", ctx.store.dataDir);
2352
- if (initProvider) {
2353
- info("Secret Provider", initProvider.name);
2354
- }
2355
- console.log("");
2356
- success("Run `elytro account create --chain <chainId>` to create your first smart account.");
2383
+ spinner.stop();
2384
+ outputResult({
2385
+ status: "initialized",
2386
+ dataDir: ctx.store.dataDir,
2387
+ secretProvider: providerName,
2388
+ ...vaultSecretB64 ? { vaultSecret: vaultSecretB64 } : {},
2389
+ ...vaultSecretB64 ? { hint: "Save ELYTRO_VAULT_SECRET \u2014 it will NOT be shown again." } : {},
2390
+ nextStep: "Run `elytro account create --chain <chainId>` to create your first smart account."
2391
+ });
2357
2392
  } catch (err) {
2358
2393
  spinner.fail("Failed to initialize wallet.");
2359
- error(sanitizeErrorMessage(err.message));
2360
- process.exitCode = 1;
2394
+ outputError(-32e3, sanitizeErrorMessage(err.message));
2361
2395
  }
2362
2396
  });
2363
2397
  }
@@ -2380,20 +2414,67 @@ async function askInput(message, defaultValue) {
2380
2414
 
2381
2415
  // src/commands/account.ts
2382
2416
  init_sponsor();
2417
+
2418
+ // src/utils/contracts/securityHook.ts
2419
+ import { encodeFunctionData, parseAbi as parseAbi2, pad, toHex as toHex5 } from "viem";
2420
+ function encodeInstallHook(walletAddress, hookAddress, safetyDelay = DEFAULT_SAFETY_DELAY, capabilityFlags = DEFAULT_CAPABILITY) {
2421
+ const safetyDelayHex = pad(toHex5(safetyDelay), { size: 4 }).slice(2);
2422
+ const hookAndData = hookAddress + safetyDelayHex;
2423
+ const callData = encodeFunctionData({
2424
+ abi: parseAbi2(["function installHook(bytes calldata hookAndData, uint8 capabilityFlags)"]),
2425
+ functionName: "installHook",
2426
+ args: [hookAndData, capabilityFlags]
2427
+ });
2428
+ return { to: walletAddress, value: "0", data: callData };
2429
+ }
2430
+ function encodeUninstallHook(walletAddress, hookAddress) {
2431
+ const callData = encodeFunctionData({
2432
+ abi: parseAbi2(["function uninstallHook(address)"]),
2433
+ functionName: "uninstallHook",
2434
+ args: [hookAddress]
2435
+ });
2436
+ return { to: walletAddress, value: "0", data: callData };
2437
+ }
2438
+ function encodeForcePreUninstall(hookAddress) {
2439
+ const callData = encodeFunctionData({
2440
+ abi: parseAbi2(["function forcePreUninstall()"]),
2441
+ functionName: "forcePreUninstall",
2442
+ args: []
2443
+ });
2444
+ return { to: hookAddress, value: "0", data: callData };
2445
+ }
2446
+
2447
+ // src/commands/account.ts
2448
+ var ERR_INVALID_PARAMS = -32602;
2449
+ var ERR_ACCOUNT_NOT_READY = -32002;
2450
+ var ERR_SPONSOR_FAILED = -32003;
2451
+ var ERR_SEND_FAILED = -32005;
2452
+ var ERR_REVERTED = -32006;
2453
+ var ERR_INTERNAL = -32e3;
2383
2454
  function registerAccountCommand(program2, ctx) {
2384
2455
  const account = program2.command("account").description("Manage smart accounts");
2385
- account.command("create").description("Create a new smart account").requiredOption("-c, --chain <chainId>", "Target chain ID").option("-a, --alias <alias>", "Human-readable alias (default: random)").action(async (opts) => {
2456
+ account.command("create").description("Create a new smart account").requiredOption("-c, --chain <chainId>", "Target chain ID").option("-a, --alias <alias>", "Human-readable alias (default: random)").option("-e, --email <email>", "2FA email for SecurityHook OTP (strongly recommended)").option("-l, --daily-limit <usd>", 'Daily spending limit in USD (e.g. "100")').action(async (opts) => {
2386
2457
  if (!ctx.keyring.isUnlocked) {
2387
- error("Wallet not initialized. Run `elytro init` first.");
2388
- process.exitCode = 1;
2458
+ outputError(ERR_ACCOUNT_NOT_READY, "Wallet not initialized. Run `elytro init` first.");
2389
2459
  return;
2390
2460
  }
2391
2461
  const chainId = Number(opts.chain);
2392
2462
  if (Number.isNaN(chainId)) {
2393
- error("Invalid chain ID.");
2394
- process.exitCode = 1;
2463
+ outputError(ERR_INVALID_PARAMS, "Invalid chain ID.", { chain: opts.chain });
2395
2464
  return;
2396
2465
  }
2466
+ let dailyLimitUsd;
2467
+ if (opts.dailyLimit !== void 0) {
2468
+ dailyLimitUsd = parseFloat(opts.dailyLimit);
2469
+ if (isNaN(dailyLimitUsd) || dailyLimitUsd < 0) {
2470
+ outputError(ERR_INVALID_PARAMS, 'Invalid --daily-limit. Provide a positive number in USD (e.g. "100").');
2471
+ return;
2472
+ }
2473
+ }
2474
+ const securityIntent = opts.email || dailyLimitUsd !== void 0 ? {
2475
+ ...opts.email && { email: opts.email },
2476
+ ...dailyLimitUsd !== void 0 && { dailyLimitUsd }
2477
+ } : void 0;
2397
2478
  const spinner = ora2("Creating smart account...").start();
2398
2479
  try {
2399
2480
  const chainConfig = ctx.chain.chains.find((c) => c.id === chainId);
@@ -2402,7 +2483,7 @@ function registerAccountCommand(program2, ctx) {
2402
2483
  await ctx.sdk.initForChain(chainConfig);
2403
2484
  ctx.walletClient.initForChain(chainConfig);
2404
2485
  }
2405
- const accountInfo = await ctx.account.createAccount(chainId, opts.alias);
2486
+ const accountInfo = await ctx.account.createAccount(chainId, opts.alias, securityIntent);
2406
2487
  spinner.text = "Registering with backend...";
2407
2488
  const { guardianHash, guardianSafePeriod } = ctx.sdk.initDefaults;
2408
2489
  const paddedKey = padHex2(accountInfo.owner, { size: 32 });
@@ -2415,56 +2496,107 @@ function registerAccountCommand(program2, ctx) {
2415
2496
  guardianHash,
2416
2497
  guardianSafePeriod
2417
2498
  );
2418
- spinner.succeed(`Account "${accountInfo.alias}" created.`);
2419
- console.log("");
2420
- info("Alias", accountInfo.alias);
2421
- info("Address", accountInfo.address);
2422
- info("Chain", `${chainName} (${chainId})`);
2423
- info("Status", "Not deployed (run `elytro account activate` to deploy)");
2424
- if (regError) {
2425
- warn(`Backend registration failed: ${regError}`);
2426
- warn("Sponsorship may not work. You can still activate with ETH.");
2499
+ let emailBindingStarted = false;
2500
+ if (opts.email && chainConfig) {
2501
+ spinner.text = "Initiating email binding...";
2502
+ try {
2503
+ const hookService = createHookServiceForAccount(ctx, chainConfig);
2504
+ const bindingResult = await hookService.requestEmailBinding(
2505
+ accountInfo.address,
2506
+ chainId,
2507
+ opts.email
2508
+ );
2509
+ await ctx.account.updateSecurityIntent(accountInfo.address, chainId, {
2510
+ emailBindingId: bindingResult.bindingId
2511
+ });
2512
+ emailBindingStarted = true;
2513
+ } catch {
2514
+ }
2427
2515
  }
2516
+ spinner.stop();
2517
+ outputResult({
2518
+ alias: accountInfo.alias,
2519
+ address: accountInfo.address,
2520
+ chain: chainName,
2521
+ chainId,
2522
+ deployed: false,
2523
+ ...securityIntent ? {
2524
+ security: {
2525
+ ...opts.email ? { email: opts.email, emailBindingStarted } : {},
2526
+ ...dailyLimitUsd !== void 0 ? { dailyLimitUsd } : {},
2527
+ hookPending: true
2528
+ }
2529
+ } : { security: null },
2530
+ ...regError ? { warning: `Backend registration failed: ${regError}` } : {}
2531
+ });
2428
2532
  } catch (err) {
2429
- spinner.fail("Failed to create account.");
2430
- error(sanitizeErrorMessage(err.message));
2431
- process.exitCode = 1;
2533
+ spinner.stop();
2534
+ outputError(ERR_INTERNAL, err.message);
2432
2535
  }
2433
2536
  });
2434
2537
  account.command("activate").description("Deploy the smart contract on-chain").argument("[account]", "Alias or address (default: current)").option("--no-sponsor", "Skip sponsorship check (user pays gas)").action(async (target, opts) => {
2435
2538
  if (!ctx.keyring.isUnlocked) {
2436
- error("Wallet not initialized. Run `elytro init` first.");
2437
- process.exitCode = 1;
2539
+ outputError(ERR_ACCOUNT_NOT_READY, "Wallet not initialized. Run `elytro init` first.");
2438
2540
  return;
2439
2541
  }
2440
2542
  const identifier = target ?? ctx.account.currentAccount?.alias ?? ctx.account.currentAccount?.address;
2441
2543
  if (!identifier) {
2442
- warn("No account selected. Specify an alias/address or create an account first.");
2544
+ outputError(ERR_ACCOUNT_NOT_READY, "No account selected. Specify an alias/address or create an account first.");
2443
2545
  return;
2444
2546
  }
2445
2547
  const accountInfo = ctx.account.resolveAccount(identifier);
2446
2548
  if (!accountInfo) {
2447
- error(`Account "${identifier}" not found.`);
2448
- process.exitCode = 1;
2549
+ outputError(ERR_ACCOUNT_NOT_READY, `Account "${identifier}" not found.`);
2449
2550
  return;
2450
2551
  }
2451
2552
  if (accountInfo.isDeployed) {
2452
- warn(`Account "${accountInfo.alias}" is already deployed.`);
2553
+ outputResult({
2554
+ alias: accountInfo.alias,
2555
+ address: accountInfo.address,
2556
+ status: "already_deployed"
2557
+ });
2453
2558
  return;
2454
2559
  }
2455
2560
  const chainConfig = ctx.chain.chains.find((c) => c.id === accountInfo.chainId);
2456
2561
  const chainName = chainConfig?.name ?? String(accountInfo.chainId);
2457
2562
  if (!chainConfig) {
2458
- error(`Chain ${accountInfo.chainId} not configured.`);
2459
- process.exitCode = 1;
2563
+ outputError(ERR_ACCOUNT_NOT_READY, `Chain ${accountInfo.chainId} not configured.`);
2460
2564
  return;
2461
2565
  }
2462
2566
  await ctx.sdk.initForChain(chainConfig);
2463
2567
  ctx.walletClient.initForChain(chainConfig);
2464
2568
  const spinner = ora2(`Activating "${accountInfo.alias}" on ${chainName}...`).start();
2569
+ const intent = accountInfo.securityIntent;
2570
+ const hookAddress = SECURITY_HOOK_ADDRESS_MAP[accountInfo.chainId];
2571
+ const shouldInstallHook = !!(intent && hookAddress && (intent.email || intent.dailyLimitUsd !== void 0));
2465
2572
  try {
2466
2573
  spinner.text = "Building deployment UserOp...";
2574
+ let deployCallData;
2575
+ let hookBatched = false;
2576
+ if (shouldInstallHook) {
2577
+ const installTx = encodeInstallHook(
2578
+ accountInfo.address,
2579
+ hookAddress,
2580
+ DEFAULT_SAFETY_DELAY,
2581
+ DEFAULT_CAPABILITY
2582
+ );
2583
+ deployCallData = installTx.data;
2584
+ }
2467
2585
  const userOp = await ctx.sdk.createDeployUserOp(accountInfo.owner, accountInfo.index);
2586
+ if (shouldInstallHook && deployCallData) {
2587
+ try {
2588
+ const hookInstallOp = await ctx.sdk.createSendUserOp(accountInfo.address, [
2589
+ {
2590
+ to: accountInfo.address,
2591
+ value: "0",
2592
+ data: deployCallData
2593
+ }
2594
+ ]);
2595
+ userOp.callData = hookInstallOp.callData;
2596
+ hookBatched = true;
2597
+ } catch {
2598
+ }
2599
+ }
2468
2600
  spinner.text = "Fetching gas prices...";
2469
2601
  const feeData = await ctx.sdk.getFeeData(chainConfig);
2470
2602
  userOp.maxFeePerGas = feeData.maxFeePerGas;
@@ -2490,15 +2622,14 @@ function registerAccountCommand(program2, ctx) {
2490
2622
  spinner.text = "Sponsorship unavailable, checking balance...";
2491
2623
  const { ether: balance } = await ctx.walletClient.getBalance(accountInfo.address);
2492
2624
  if (parseFloat(balance) === 0) {
2493
- spinner.fail("Activation failed.");
2494
- error(`Sponsorship failed: ${sponsorError ?? "unknown reason"}`);
2495
- error(
2496
- `Account has no ETH to pay gas. Fund ${accountInfo.address} on ${chainName}, or fix sponsorship.`
2497
- );
2498
- process.exitCode = 1;
2625
+ spinner.stop();
2626
+ outputError(ERR_SPONSOR_FAILED, `Sponsorship failed: ${sponsorError ?? "unknown"}. Account has no ETH to pay gas.`, {
2627
+ account: accountInfo.alias,
2628
+ address: accountInfo.address,
2629
+ chain: chainName
2630
+ });
2499
2631
  return;
2500
2632
  }
2501
- spinner.text = "Proceeding without sponsor (user pays gas)...";
2502
2633
  }
2503
2634
  }
2504
2635
  spinner.text = "Signing UserOperation...";
@@ -2510,25 +2641,37 @@ function registerAccountCommand(program2, ctx) {
2510
2641
  spinner.text = "Waiting for on-chain confirmation...";
2511
2642
  const receipt = await ctx.sdk.waitForReceipt(opHash);
2512
2643
  await ctx.account.markDeployed(accountInfo.address, accountInfo.chainId);
2644
+ const hookInstalled = shouldInstallHook && hookBatched && receipt.success;
2645
+ if (hookInstalled) {
2646
+ await ctx.account.finalizeSecurityIntent(accountInfo.address, accountInfo.chainId);
2647
+ } else if (intent) {
2648
+ await ctx.account.clearSecurityIntent(accountInfo.address, accountInfo.chainId);
2649
+ }
2650
+ spinner.stop();
2513
2651
  if (receipt.success) {
2514
- spinner.succeed(`Account "${accountInfo.alias}" activated!`);
2652
+ outputResult({
2653
+ alias: accountInfo.alias,
2654
+ address: accountInfo.address,
2655
+ chain: chainName,
2656
+ chainId: accountInfo.chainId,
2657
+ transactionHash: receipt.transactionHash,
2658
+ gasCost: `${formatEther2(BigInt(receipt.actualGasCost))} ETH`,
2659
+ sponsored,
2660
+ hookInstalled,
2661
+ ...hookInstalled && intent?.email ? { emailPending: intent.email } : {},
2662
+ ...hookInstalled && intent?.dailyLimitUsd !== void 0 ? { dailyLimitPending: intent.dailyLimitUsd } : {},
2663
+ ...chainConfig.blockExplorer ? { explorer: `${chainConfig.blockExplorer}/tx/${receipt.transactionHash}` } : {}
2664
+ });
2515
2665
  } else {
2516
- spinner.warn(`UserOp included but execution reverted.`);
2517
- }
2518
- console.log("");
2519
- info("Account", accountInfo.alias);
2520
- info("Address", accountInfo.address);
2521
- info("Chain", `${chainName} (${accountInfo.chainId})`);
2522
- info("Tx Hash", receipt.transactionHash);
2523
- info("Gas Cost", `${formatEther2(BigInt(receipt.actualGasCost))} ETH`);
2524
- info("Sponsored", sponsored ? "Yes (gasless)" : "No (user paid)");
2525
- if (chainConfig.blockExplorer) {
2526
- info("Explorer", `${chainConfig.blockExplorer}/tx/${receipt.transactionHash}`);
2666
+ outputError(ERR_REVERTED, "UserOp included but execution reverted.", {
2667
+ alias: accountInfo.alias,
2668
+ transactionHash: receipt.transactionHash,
2669
+ block: receipt.blockNumber
2670
+ });
2527
2671
  }
2528
2672
  } catch (err) {
2529
- spinner.fail("Activation failed.");
2530
- error(sanitizeErrorMessage(err.message));
2531
- process.exitCode = 1;
2673
+ spinner.stop();
2674
+ outputError(ERR_SEND_FAILED, err.message);
2532
2675
  }
2533
2676
  });
2534
2677
  account.command("list").description("List all accounts (or query one by alias/address)").argument("[account]", "Filter by alias or address").option("-c, --chain <chainId>", "Filter by chain ID").action(async (target, opts) => {
@@ -2536,53 +2679,40 @@ function registerAccountCommand(program2, ctx) {
2536
2679
  if (target) {
2537
2680
  const matched = ctx.account.resolveAccount(target);
2538
2681
  if (!matched) {
2539
- error(`Account "${target}" not found.`);
2540
- process.exitCode = 1;
2682
+ outputError(ERR_ACCOUNT_NOT_READY, `Account "${target}" not found.`);
2541
2683
  return;
2542
2684
  }
2543
2685
  accounts = [matched];
2544
2686
  }
2545
- if (accounts.length === 0) {
2546
- warn("No accounts found. Run `elytro account create --chain <chainId>` first.");
2547
- return;
2548
- }
2549
2687
  const current = ctx.account.currentAccount;
2550
- heading("Accounts");
2551
- table(
2552
- accounts.map((a) => {
2688
+ outputResult({
2689
+ accounts: accounts.map((a) => {
2553
2690
  const chainConfig = ctx.chain.chains.find((c) => c.id === a.chainId);
2554
2691
  return {
2555
- active: a.address === current?.address ? "\u2192" : " ",
2692
+ active: a.address === current?.address,
2556
2693
  alias: a.alias,
2557
2694
  address: a.address,
2558
2695
  chain: chainConfig?.name ?? String(a.chainId),
2559
- deployed: a.isDeployed ? "Yes" : "No",
2560
- recovery: a.isRecoveryEnabled ? "Yes" : "No"
2696
+ chainId: a.chainId,
2697
+ deployed: a.isDeployed,
2698
+ recovery: a.isRecoveryEnabled
2561
2699
  };
2562
2700
  }),
2563
- [
2564
- { key: "active", label: "", width: 3 },
2565
- { key: "alias", label: "Alias", width: 16 },
2566
- { key: "address", label: "Address", width: 44 },
2567
- { key: "chain", label: "Chain", width: 18 },
2568
- { key: "deployed", label: "Deployed", width: 10 },
2569
- { key: "recovery", label: "Recovery", width: 10 }
2570
- ]
2571
- );
2701
+ total: accounts.length
2702
+ });
2572
2703
  });
2573
2704
  account.command("info").description("Show details for an account").argument("[account]", "Alias or address (default: current)").action(async (target) => {
2574
2705
  const identifier = target ?? ctx.account.currentAccount?.alias ?? ctx.account.currentAccount?.address;
2575
2706
  if (!identifier) {
2576
- warn("No account selected. Run `elytro account create --chain <chainId>` first.");
2707
+ outputError(ERR_ACCOUNT_NOT_READY, "No account selected. Run `elytro account create` first.");
2577
2708
  return;
2578
2709
  }
2579
2710
  const spinner = ora2("Fetching on-chain data...").start();
2580
2711
  try {
2581
2712
  const accountInfo = ctx.account.resolveAccount(identifier);
2582
2713
  if (!accountInfo) {
2583
- spinner.fail("Account not found.");
2584
- error(`Account "${identifier}" not found.`);
2585
- process.exitCode = 1;
2714
+ spinner.stop();
2715
+ outputError(ERR_ACCOUNT_NOT_READY, `Account "${identifier}" not found.`);
2586
2716
  return;
2587
2717
  }
2588
2718
  const chainConfig = ctx.chain.chains.find((c) => c.id === accountInfo.chainId);
@@ -2591,26 +2721,27 @@ function registerAccountCommand(program2, ctx) {
2591
2721
  }
2592
2722
  const detail = await ctx.account.getAccountDetail(identifier);
2593
2723
  spinner.stop();
2594
- heading("Account Details");
2595
- info("Alias", detail.alias);
2596
- info("Address", detail.address);
2597
- info("Chain", chainConfig?.name ?? String(detail.chainId));
2598
- info("Deployed", detail.isDeployed ? "Yes" : "No");
2599
- info("Balance", `${detail.balance} ${chainConfig?.nativeCurrency.symbol ?? "ETH"}`);
2600
- info("Recovery", detail.isRecoveryEnabled ? "Enabled" : "Not set");
2601
- if (chainConfig?.blockExplorer) {
2602
- info("Explorer", `${chainConfig.blockExplorer}/address/${detail.address}`);
2603
- }
2724
+ outputResult({
2725
+ alias: detail.alias,
2726
+ address: detail.address,
2727
+ chain: chainConfig?.name ?? String(detail.chainId),
2728
+ chainId: detail.chainId,
2729
+ deployed: detail.isDeployed,
2730
+ balance: `${detail.balance} ${chainConfig?.nativeCurrency.symbol ?? "ETH"}`,
2731
+ recovery: detail.isRecoveryEnabled,
2732
+ ...detail.securityStatus ? { securityStatus: detail.securityStatus } : {},
2733
+ ...detail.securityIntent ? { securityIntent: detail.securityIntent } : {},
2734
+ ...chainConfig?.blockExplorer ? { explorer: `${chainConfig.blockExplorer}/address/${detail.address}` } : {}
2735
+ });
2604
2736
  } catch (err) {
2605
- spinner.fail("Failed to fetch account info.");
2606
- error(sanitizeErrorMessage(err.message));
2607
- process.exitCode = 1;
2737
+ spinner.stop();
2738
+ outputError(ERR_INTERNAL, err.message);
2608
2739
  }
2609
2740
  });
2610
2741
  account.command("switch").description("Switch the active account").argument("[account]", "Alias or address").action(async (target) => {
2611
2742
  const accounts = ctx.account.allAccounts;
2612
2743
  if (accounts.length === 0) {
2613
- warn("No accounts found.");
2744
+ outputError(ERR_ACCOUNT_NOT_READY, "No accounts found. Run `elytro account create` first.");
2614
2745
  return;
2615
2746
  }
2616
2747
  let identifier = target;
@@ -2631,26 +2762,42 @@ function registerAccountCommand(program2, ctx) {
2631
2762
  ctx.walletClient.initForChain(newChain);
2632
2763
  await ctx.sdk.initForChain(newChain);
2633
2764
  }
2634
- success(`Switched to "${switched.alias}"`);
2635
- const spinner = ora2("Fetching on-chain data...").start();
2765
+ let balance = null;
2636
2766
  try {
2637
2767
  const detail = await ctx.account.getAccountDetail(switched.alias);
2638
- spinner.stop();
2639
- console.log("");
2640
- info("Address", detail.address);
2641
- info("Chain", newChain?.name ?? String(detail.chainId));
2642
- info("Deployed", detail.isDeployed ? "Yes" : "No");
2643
- info("Balance", `${detail.balance} ${newChain?.nativeCurrency.symbol ?? "ETH"}`);
2644
- if (newChain?.blockExplorer) {
2645
- info("Explorer", `${newChain.blockExplorer}/address/${detail.address}`);
2646
- }
2768
+ balance = `${detail.balance} ${newChain?.nativeCurrency.symbol ?? "ETH"}`;
2647
2769
  } catch {
2648
- spinner.stop();
2649
- warn("Could not fetch on-chain data. Run `elytro account info` to retry.");
2650
2770
  }
2771
+ outputResult({
2772
+ alias: switched.alias,
2773
+ address: switched.address,
2774
+ chain: newChain?.name ?? String(switched.chainId),
2775
+ chainId: switched.chainId,
2776
+ deployed: switched.isDeployed,
2777
+ ...balance ? { balance } : {},
2778
+ ...newChain?.blockExplorer ? { explorer: `${newChain.blockExplorer}/address/${switched.address}` } : {}
2779
+ });
2651
2780
  } catch (err) {
2652
- error(sanitizeErrorMessage(err.message));
2653
- process.exitCode = 1;
2781
+ outputError(ERR_INTERNAL, err.message);
2782
+ }
2783
+ });
2784
+ }
2785
+ function createHookServiceForAccount(ctx, chainConfig) {
2786
+ return new SecurityHookService({
2787
+ store: ctx.store,
2788
+ graphqlEndpoint: ctx.chain.graphqlEndpoint,
2789
+ signMessageForAuth: createSignMessageForAuth({
2790
+ signDigest: (digest) => ctx.keyring.signDigest(digest),
2791
+ packRawHash: (hash) => ctx.sdk.packRawHash(hash),
2792
+ packSignature: (rawSig, valData) => ctx.sdk.packUserOpSignature(rawSig, valData)
2793
+ }),
2794
+ readContract: async (params) => {
2795
+ return ctx.walletClient.readContract(params);
2796
+ },
2797
+ getBlockTimestamp: async () => {
2798
+ const blockNumber = await ctx.walletClient.raw.getBlockNumber();
2799
+ const block = await ctx.walletClient.raw.getBlock({ blockNumber });
2800
+ return block.timestamp;
2654
2801
  }
2655
2802
  });
2656
2803
  }
@@ -2658,15 +2805,15 @@ function registerAccountCommand(program2, ctx) {
2658
2805
  // src/commands/tx.ts
2659
2806
  init_sponsor();
2660
2807
  import ora3 from "ora";
2661
- import { isAddress, isHex, formatEther as formatEther3, parseEther as parseEther2, toHex as toHex5 } from "viem";
2662
- var ERR_INVALID_PARAMS = -32602;
2808
+ import { isAddress, isHex, formatEther as formatEther3, parseEther as parseEther2, toHex as toHex6 } from "viem";
2809
+ var ERR_INVALID_PARAMS2 = -32602;
2663
2810
  var ERR_INSUFFICIENT_BALANCE = -32001;
2664
- var ERR_ACCOUNT_NOT_READY = -32002;
2665
- var ERR_SPONSOR_FAILED = -32003;
2811
+ var ERR_ACCOUNT_NOT_READY2 = -32002;
2812
+ var ERR_SPONSOR_FAILED2 = -32003;
2666
2813
  var ERR_BUILD_FAILED = -32004;
2667
- var ERR_SEND_FAILED = -32005;
2814
+ var ERR_SEND_FAILED2 = -32005;
2668
2815
  var ERR_EXECUTION_REVERTED = -32006;
2669
- var ERR_INTERNAL = -32e3;
2816
+ var ERR_INTERNAL2 = -32e3;
2670
2817
  var TxError = class extends Error {
2671
2818
  code;
2672
2819
  data;
@@ -2679,14 +2826,10 @@ var TxError = class extends Error {
2679
2826
  };
2680
2827
  function handleTxError(err) {
2681
2828
  if (err instanceof TxError) {
2682
- txError({ code: err.code, message: sanitizeErrorMessage(err.message), data: err.data });
2829
+ outputError(err.code, err.message, err.data);
2683
2830
  } else {
2684
- txError({
2685
- code: ERR_INTERNAL,
2686
- message: sanitizeErrorMessage(err.message ?? String(err))
2687
- });
2831
+ outputError(ERR_INTERNAL2, err.message ?? String(err));
2688
2832
  }
2689
- process.exitCode = 1;
2690
2833
  }
2691
2834
  function parseTxSpec(spec, index) {
2692
2835
  const prefix = `--tx #${index + 1}`;
@@ -2694,7 +2837,7 @@ function parseTxSpec(spec, index) {
2694
2837
  for (const part of spec.split(",")) {
2695
2838
  const colonIdx = part.indexOf(":");
2696
2839
  if (colonIdx === -1) {
2697
- throw new TxError(ERR_INVALID_PARAMS, `${prefix}: invalid segment "${part}". Expected key:value format.`, {
2840
+ throw new TxError(ERR_INVALID_PARAMS2, `${prefix}: invalid segment "${part}". Expected key:value format.`, {
2698
2841
  spec,
2699
2842
  index
2700
2843
  });
@@ -2702,17 +2845,17 @@ function parseTxSpec(spec, index) {
2702
2845
  const key = part.slice(0, colonIdx).trim().toLowerCase();
2703
2846
  const val = part.slice(colonIdx + 1).trim();
2704
2847
  if (!key || !val) {
2705
- throw new TxError(ERR_INVALID_PARAMS, `${prefix}: empty key or value in "${part}".`, { spec, index });
2848
+ throw new TxError(ERR_INVALID_PARAMS2, `${prefix}: empty key or value in "${part}".`, { spec, index });
2706
2849
  }
2707
2850
  if (fields[key]) {
2708
- throw new TxError(ERR_INVALID_PARAMS, `${prefix}: duplicate key "${key}".`, { spec, index, key });
2851
+ throw new TxError(ERR_INVALID_PARAMS2, `${prefix}: duplicate key "${key}".`, { spec, index, key });
2709
2852
  }
2710
2853
  fields[key] = val;
2711
2854
  }
2712
2855
  const knownKeys = /* @__PURE__ */ new Set(["to", "value", "data"]);
2713
2856
  for (const key of Object.keys(fields)) {
2714
2857
  if (!knownKeys.has(key)) {
2715
- throw new TxError(ERR_INVALID_PARAMS, `${prefix}: unknown key "${key}". Allowed: to, value, data.`, {
2858
+ throw new TxError(ERR_INVALID_PARAMS2, `${prefix}: unknown key "${key}". Allowed: to, value, data.`, {
2716
2859
  spec,
2717
2860
  index,
2718
2861
  key
@@ -2720,13 +2863,13 @@ function parseTxSpec(spec, index) {
2720
2863
  }
2721
2864
  }
2722
2865
  if (!fields.to) {
2723
- throw new TxError(ERR_INVALID_PARAMS, `${prefix}: "to" is required.`, { spec, index });
2866
+ throw new TxError(ERR_INVALID_PARAMS2, `${prefix}: "to" is required.`, { spec, index });
2724
2867
  }
2725
2868
  if (!isAddress(fields.to)) {
2726
- throw new TxError(ERR_INVALID_PARAMS, `${prefix}: invalid address "${fields.to}".`, { spec, index, to: fields.to });
2869
+ throw new TxError(ERR_INVALID_PARAMS2, `${prefix}: invalid address "${fields.to}".`, { spec, index, to: fields.to });
2727
2870
  }
2728
2871
  if (!fields.value && !fields.data) {
2729
- throw new TxError(ERR_INVALID_PARAMS, `${prefix}: at least one of "value" or "data" is required.`, { spec, index });
2872
+ throw new TxError(ERR_INVALID_PARAMS2, `${prefix}: at least one of "value" or "data" is required.`, { spec, index });
2730
2873
  }
2731
2874
  if (fields.value) {
2732
2875
  try {
@@ -2734,7 +2877,7 @@ function parseTxSpec(spec, index) {
2734
2877
  if (wei < 0n) throw new Error("negative");
2735
2878
  } catch {
2736
2879
  throw new TxError(
2737
- ERR_INVALID_PARAMS,
2880
+ ERR_INVALID_PARAMS2,
2738
2881
  `${prefix}: invalid ETH amount "${fields.value}". Use human-readable format (e.g. "0.1").`,
2739
2882
  { spec, index, value: fields.value }
2740
2883
  );
@@ -2742,14 +2885,14 @@ function parseTxSpec(spec, index) {
2742
2885
  }
2743
2886
  if (fields.data) {
2744
2887
  if (!isHex(fields.data)) {
2745
- throw new TxError(ERR_INVALID_PARAMS, `${prefix}: invalid hex in "data". Must start with 0x.`, {
2888
+ throw new TxError(ERR_INVALID_PARAMS2, `${prefix}: invalid hex in "data". Must start with 0x.`, {
2746
2889
  spec,
2747
2890
  index,
2748
2891
  data: fields.data
2749
2892
  });
2750
2893
  }
2751
2894
  if (fields.data.length > 2 && fields.data.length % 2 !== 0) {
2752
- throw new TxError(ERR_INVALID_PARAMS, `${prefix}: "data" hex must have even length (complete bytes).`, {
2895
+ throw new TxError(ERR_INVALID_PARAMS2, `${prefix}: "data" hex must have even length (complete bytes).`, {
2753
2896
  spec,
2754
2897
  index,
2755
2898
  data: fields.data
@@ -2771,7 +2914,7 @@ function detectTxType(specs) {
2771
2914
  function specsToTxs(specs) {
2772
2915
  return specs.map((s) => ({
2773
2916
  to: s.to,
2774
- value: s.value ? toHex5(parseEther2(s.value)) : "0x0",
2917
+ value: s.value ? toHex6(parseEther2(s.value)) : "0x0",
2775
2918
  data: s.data ?? "0x"
2776
2919
  }));
2777
2920
  }
@@ -2792,19 +2935,12 @@ function txTypeLabel(txType) {
2792
2935
  return "Batch Transaction";
2793
2936
  }
2794
2937
  }
2795
- function truncateHex(hex, maxLen = 42) {
2796
- if (hex.length <= maxLen) return hex;
2797
- return `${hex.slice(0, 20)}...${hex.slice(-8)} (${(hex.length - 2) / 2} bytes)`;
2798
- }
2799
- function displayTxSpec(spec, index) {
2800
- const parts = [`#${index + 1}`];
2801
- parts.push(`\u2192 ${spec.to}`);
2802
- if (spec.value) parts.push(`${spec.value} ETH`);
2803
- if (spec.data && spec.data !== "0x") {
2804
- const selector = spec.data.length >= 10 ? spec.data.slice(0, 10) : spec.data;
2805
- parts.push(`call ${selector}`);
2806
- }
2807
- info("Tx", parts.join(" "));
2938
+ function specToJson(spec) {
2939
+ return {
2940
+ to: spec.to,
2941
+ ...spec.value ? { value: spec.value } : {},
2942
+ ...spec.data && spec.data !== "0x" ? { data: spec.data, selector: spec.data.length >= 10 ? spec.data.slice(0, 10) : spec.data } : {}
2943
+ };
2808
2944
  }
2809
2945
  function registerTxCommand(program2, ctx) {
2810
2946
  const tx = program2.command("tx").description("Build, simulate, and send transactions");
@@ -2817,21 +2953,23 @@ function registerTxCommand(program2, ctx) {
2817
2953
  specs,
2818
2954
  opts?.sponsor
2819
2955
  );
2820
- heading("UserOperation (unsigned)");
2821
- console.log(JSON.stringify(serializeUserOp(userOp), null, 2));
2822
- console.log("");
2823
- info("Account", accountInfo.alias);
2824
- info("Chain", `${chainConfig.name} (${chainConfig.id})`);
2825
- info("Type", txTypeLabel(txType));
2826
- if (txType === "batch") info("Tx Count", specs.length.toString());
2827
- info("Sponsored", sponsored ? "Yes" : "No");
2956
+ outputResult({
2957
+ userOp: serializeUserOp(userOp),
2958
+ account: accountInfo.alias,
2959
+ address: accountInfo.address,
2960
+ chain: chainConfig.name,
2961
+ chainId: chainConfig.id,
2962
+ txType: txTypeLabel(txType),
2963
+ ...txType === "batch" ? { txCount: specs.length } : {},
2964
+ sponsored
2965
+ });
2828
2966
  } catch (err) {
2829
2967
  handleTxError(err);
2830
2968
  }
2831
2969
  });
2832
2970
  tx.command("send").description("Send a transaction on-chain").argument("[account]", "Source account alias or address (default: current)").option("--tx <spec...>", 'Transaction spec: "to:0xAddr,value:0.1,data:0x..." (repeatable, ordered)').option("--no-sponsor", "Skip sponsorship check").option("--no-hook", "Skip SecurityHook signing (bypass 2FA)").option("--userop <json>", "Send a pre-built UserOp JSON (skips build step)").action(async (target, opts) => {
2833
2971
  if (!ctx.keyring.isUnlocked) {
2834
- handleTxError(new TxError(ERR_ACCOUNT_NOT_READY, "Wallet not initialized. Run `elytro init` first."));
2972
+ handleTxError(new TxError(ERR_ACCOUNT_NOT_READY2, "Wallet not initialized. Run `elytro init` first."));
2835
2973
  return;
2836
2974
  }
2837
2975
  try {
@@ -2846,7 +2984,7 @@ function registerTxCommand(program2, ctx) {
2846
2984
  sponsored = !!userOp.paymaster;
2847
2985
  const identifier = target ?? ctx.account.currentAccount?.alias ?? ctx.account.currentAccount?.address;
2848
2986
  if (!identifier) {
2849
- throw new TxError(ERR_ACCOUNT_NOT_READY, "No account selected.", {
2987
+ throw new TxError(ERR_ACCOUNT_NOT_READY2, "No account selected.", {
2850
2988
  hint: "Specify an alias/address or create an account first."
2851
2989
  });
2852
2990
  }
@@ -2863,37 +3001,20 @@ function registerTxCommand(program2, ctx) {
2863
3001
  sponsored = result.sponsored;
2864
3002
  txType = result.txType;
2865
3003
  }
2866
- console.log("");
2867
- heading("Transaction Summary");
2868
- info("Type", txTypeLabel(txType));
2869
- info("From", `${accountInfo.alias} (${accountInfo.address})`);
2870
- if (txType === "batch") {
2871
- info("Tx Count", specs.length.toString());
2872
- for (let i = 0; i < specs.length; i++) {
2873
- displayTxSpec(specs[i], i);
2874
- }
2875
- } else if (txType === "contract-call") {
2876
- const s = specs[0];
2877
- info("To", s.to);
2878
- info("Calldata", truncateHex(s.data ?? "0x"));
2879
- if (s.data && s.data.length >= 10) {
2880
- info("Selector", s.data.slice(0, 10));
2881
- }
2882
- if (s.value && s.value !== "0") {
2883
- info("Value", `${s.value} ETH (payable)`);
2884
- }
2885
- } else {
2886
- const s = specs[0];
2887
- info("To", s.to);
2888
- info("Value", `${s.value ?? "0"} ETH`);
2889
- }
2890
- info("Sponsored", sponsored ? "Yes (gasless)" : "No (user pays gas)");
2891
3004
  const estimatedGas = userOp.callGasLimit + userOp.verificationGasLimit + userOp.preVerificationGas;
2892
- info("Est. Gas", estimatedGas.toString());
2893
- console.log("");
3005
+ console.error(JSON.stringify({
3006
+ summary: {
3007
+ txType: txTypeLabel(txType),
3008
+ from: accountInfo.alias,
3009
+ address: accountInfo.address,
3010
+ transactions: specs.map((s, i) => specToJson(s)),
3011
+ sponsored,
3012
+ estimatedGas: estimatedGas.toString()
3013
+ }
3014
+ }, null, 2));
2894
3015
  const confirmed = await askConfirm("Sign and send this transaction?");
2895
3016
  if (!confirmed) {
2896
- warn("Transaction cancelled.");
3017
+ outputResult({ status: "cancelled" });
2897
3018
  return;
2898
3019
  }
2899
3020
  const spinner = ora3("Signing UserOperation...").start();
@@ -2936,14 +3057,12 @@ function registerTxCommand(program2, ctx) {
2936
3057
  spinner.stop();
2937
3058
  const errCode = hookResult.error.code;
2938
3059
  if (errCode === "OTP_REQUIRED" || errCode === "SPENDING_LIMIT_EXCEEDED") {
2939
- warn(hookResult.error.message ?? `Verification required (${errCode}).`);
2940
- if (hookResult.error.maskedEmail) {
2941
- info("OTP sent to", hookResult.error.maskedEmail);
2942
- }
2943
- if (errCode === "SPENDING_LIMIT_EXCEEDED" && hookResult.error.projectedSpendUsdCents !== void 0) {
2944
- info("Projected spend", `$${(hookResult.error.projectedSpendUsdCents / 100).toFixed(2)}`);
2945
- info("Daily limit", `$${((hookResult.error.dailyLimitUsdCents ?? 0) / 100).toFixed(2)}`);
2946
- }
3060
+ console.error(JSON.stringify({
3061
+ challenge: errCode,
3062
+ message: hookResult.error.message ?? `Verification required (${errCode}).`,
3063
+ ...hookResult.error.maskedEmail ? { maskedEmail: hookResult.error.maskedEmail } : {},
3064
+ ...errCode === "SPENDING_LIMIT_EXCEEDED" && hookResult.error.projectedSpendUsdCents !== void 0 ? { projectedSpendUsd: (hookResult.error.projectedSpendUsdCents / 100).toFixed(2), dailyLimitUsd: ((hookResult.error.dailyLimitUsdCents ?? 0) / 100).toFixed(2) } : {}
3065
+ }, null, 2));
2947
3066
  const otpCode = await askInput("Enter the 6-digit OTP code:");
2948
3067
  spinner.start("Verifying OTP...");
2949
3068
  await hookService.verifySecurityOtp(
@@ -2961,13 +3080,13 @@ function registerTxCommand(program2, ctx) {
2961
3080
  );
2962
3081
  if (hookResult.error) {
2963
3082
  throw new TxError(
2964
- ERR_SEND_FAILED,
3083
+ ERR_SEND_FAILED2,
2965
3084
  `Hook authorization failed after OTP: ${hookResult.error.message}`
2966
3085
  );
2967
3086
  }
2968
3087
  } else {
2969
3088
  throw new TxError(
2970
- ERR_SEND_FAILED,
3089
+ ERR_SEND_FAILED2,
2971
3090
  `Hook authorization failed: ${hookResult.error.message ?? errCode}`
2972
3091
  );
2973
3092
  }
@@ -2988,48 +3107,41 @@ function registerTxCommand(program2, ctx) {
2988
3107
  spinner.text = "Sending to bundler...";
2989
3108
  opHash = await ctx.sdk.sendUserOp(userOp);
2990
3109
  } catch (err) {
2991
- spinner.fail("Send failed.");
2992
- throw new TxError(ERR_SEND_FAILED, err.message, {
3110
+ spinner.stop();
3111
+ throw new TxError(ERR_SEND_FAILED2, err.message, {
2993
3112
  sender: accountInfo.address,
2994
3113
  chain: chainConfig.name
2995
3114
  });
2996
3115
  }
2997
3116
  spinner.text = "Waiting for on-chain confirmation...";
2998
3117
  const receipt = await ctx.sdk.waitForReceipt(opHash);
3118
+ spinner.stop();
2999
3119
  if (receipt.success) {
3000
- spinner.succeed("Transaction confirmed!");
3120
+ outputResult({
3121
+ status: "confirmed",
3122
+ account: accountInfo.alias,
3123
+ address: accountInfo.address,
3124
+ transactionHash: receipt.transactionHash,
3125
+ block: receipt.blockNumber,
3126
+ gasCost: `${formatEther3(BigInt(receipt.actualGasCost))} ETH`,
3127
+ sponsored,
3128
+ ...chainConfig.blockExplorer ? { explorer: `${chainConfig.blockExplorer}/tx/${receipt.transactionHash}` } : {}
3129
+ });
3001
3130
  } else {
3002
- spinner.warn("Execution reverted.");
3003
- txError({
3004
- code: ERR_EXECUTION_REVERTED,
3005
- message: "UserOp included but execution reverted on-chain.",
3006
- data: {
3007
- txHash: receipt.transactionHash,
3008
- block: receipt.blockNumber,
3009
- gasCost: `${formatEther3(BigInt(receipt.actualGasCost))} ETH`,
3010
- sender: accountInfo.address
3011
- }
3131
+ outputError(ERR_EXECUTION_REVERTED, "UserOp included but execution reverted on-chain.", {
3132
+ transactionHash: receipt.transactionHash,
3133
+ block: receipt.blockNumber,
3134
+ gasCost: `${formatEther3(BigInt(receipt.actualGasCost))} ETH`,
3135
+ sender: accountInfo.address
3012
3136
  });
3013
3137
  }
3014
- console.log("");
3015
- info("Account", accountInfo.alias);
3016
- info("Tx Hash", receipt.transactionHash);
3017
- info("Block", receipt.blockNumber);
3018
- info("Gas Cost", `${formatEther3(BigInt(receipt.actualGasCost))} ETH`);
3019
- info("Sponsored", sponsored ? "Yes (gasless)" : "No (user paid)");
3020
- if (chainConfig.blockExplorer) {
3021
- info("Explorer", `${chainConfig.blockExplorer}/tx/${receipt.transactionHash}`);
3022
- }
3023
- if (!receipt.success) {
3024
- process.exitCode = 1;
3025
- }
3026
3138
  } catch (err) {
3027
3139
  handleTxError(err);
3028
3140
  }
3029
3141
  });
3030
3142
  tx.command("simulate").description("Preview a transaction (gas estimate, sponsor check)").argument("[account]", "Source account alias or address (default: current)").option("--tx <spec...>", 'Transaction spec: "to:0xAddr,value:0.1,data:0x..." (repeatable, ordered)').option("--no-sponsor", "Skip sponsorship check").action(async (target, opts) => {
3031
3143
  if (!ctx.keyring.isUnlocked) {
3032
- handleTxError(new TxError(ERR_ACCOUNT_NOT_READY, "Wallet not initialized. Run `elytro init` first."));
3144
+ handleTxError(new TxError(ERR_ACCOUNT_NOT_READY2, "Wallet not initialized. Run `elytro init` first."));
3033
3145
  return;
3034
3146
  }
3035
3147
  try {
@@ -3042,77 +3154,44 @@ function registerTxCommand(program2, ctx) {
3042
3154
  );
3043
3155
  const { wei: ethBalance, ether: ethFormatted } = await ctx.walletClient.getBalance(accountInfo.address);
3044
3156
  const nativeCurrency = chainConfig.nativeCurrency.symbol;
3045
- console.log("");
3046
- heading("Transaction Simulation");
3047
- info("Type", txTypeLabel(txType));
3048
- info("From", `${accountInfo.alias} (${accountInfo.address})`);
3049
- info("Chain", `${chainConfig.name} (${chainConfig.id})`);
3050
- if (txType === "batch") {
3051
- console.log("");
3052
- info("Tx Count", specs.length.toString());
3053
- for (let i = 0; i < specs.length; i++) {
3054
- displayTxSpec(specs[i], i);
3055
- }
3056
- const total = totalEthValue(specs);
3057
- if (total > 0n) {
3058
- info("Total ETH", formatEther3(total));
3059
- if (ethBalance < total) {
3060
- warn(`Insufficient balance: need ${formatEther3(total)}, have ${ethFormatted} ${nativeCurrency}`);
3061
- }
3062
- }
3063
- } else if (txType === "contract-call") {
3064
- const s = specs[0];
3065
- console.log("");
3066
- info("To", s.to);
3067
- info("Calldata", truncateHex(s.data ?? "0x"));
3068
- info("Calldata Size", `${Math.max(0, ((s.data?.length ?? 2) - 2) / 2)} bytes`);
3069
- if (s.data && s.data.length >= 10) {
3070
- info("Selector", s.data.slice(0, 10));
3071
- }
3072
- if (s.value && s.value !== "0") {
3073
- info("Value", `${s.value} ${nativeCurrency} (payable)`);
3074
- const sendValue = parseEther2(s.value);
3075
- if (ethBalance < sendValue) {
3076
- warn(`Insufficient balance for value: need ${s.value}, have ${ethFormatted} ${nativeCurrency}`);
3077
- }
3078
- }
3079
- const isContract = await ctx.walletClient.isContractDeployed(s.to);
3080
- info("Target", isContract ? "Contract" : "EOA (warning: calling non-contract)");
3081
- if (!isContract) {
3082
- warn("Target address has no deployed code. The call may be a no-op or revert.");
3083
- }
3084
- } else {
3085
- const s = specs[0];
3086
- console.log("");
3087
- info("To", s.to);
3088
- info("Value", `${s.value ?? "0"} ${nativeCurrency}`);
3089
- if (s.value) {
3090
- const sendValue = parseEther2(s.value);
3091
- if (ethBalance < sendValue) {
3092
- warn(`Insufficient balance: need ${s.value}, have ${ethFormatted} ${nativeCurrency}`);
3093
- }
3094
- }
3095
- }
3096
- console.log("");
3097
- info("callGasLimit", userOp.callGasLimit.toString());
3098
- info("verificationGasLimit", userOp.verificationGasLimit.toString());
3099
- info("preVerificationGas", userOp.preVerificationGas.toString());
3100
- info("maxFeePerGas", `${userOp.maxFeePerGas.toString()} wei`);
3101
- info("maxPriorityFeePerGas", `${userOp.maxPriorityFeePerGas.toString()} wei`);
3102
3157
  const totalGas = userOp.callGasLimit + userOp.verificationGasLimit + userOp.preVerificationGas;
3103
3158
  const maxCostWei = totalGas * userOp.maxFeePerGas;
3104
- info("Max Gas Cost", `${formatEther3(maxCostWei)} ${nativeCurrency}`);
3105
- console.log("");
3106
- info("Sponsored", sponsored ? "Yes (gasless)" : "No (user pays gas)");
3107
- if (sponsored && userOp.paymaster) {
3108
- info("Paymaster", userOp.paymaster);
3159
+ const ethValueSum = totalEthValue(specs);
3160
+ const warnings = [];
3161
+ if (ethValueSum > 0n && ethBalance < ethValueSum) {
3162
+ warnings.push(`Insufficient balance for value: need ${formatEther3(ethValueSum)}, have ${ethFormatted} ${nativeCurrency}`);
3109
3163
  }
3110
- info(`${nativeCurrency} Balance`, `${ethFormatted} ${nativeCurrency}`);
3111
3164
  if (!sponsored && ethBalance < maxCostWei) {
3112
- warn(
3113
- `Insufficient ${nativeCurrency} for gas: need ~${formatEther3(maxCostWei)}, have ${ethFormatted}`
3114
- );
3165
+ warnings.push(`Insufficient ${nativeCurrency} for gas: need ~${formatEther3(maxCostWei)}, have ${ethFormatted}`);
3166
+ }
3167
+ let targetIsContract;
3168
+ if (txType === "contract-call") {
3169
+ targetIsContract = await ctx.walletClient.isContractDeployed(specs[0].to);
3170
+ if (!targetIsContract) {
3171
+ warnings.push("Target address has no deployed code. The call may be a no-op or revert.");
3172
+ }
3115
3173
  }
3174
+ outputResult({
3175
+ txType: txTypeLabel(txType),
3176
+ account: accountInfo.alias,
3177
+ address: accountInfo.address,
3178
+ chain: chainConfig.name,
3179
+ chainId: chainConfig.id,
3180
+ transactions: specs.map((s) => specToJson(s)),
3181
+ ...txType === "contract-call" && targetIsContract !== void 0 ? { targetIsContract } : {},
3182
+ gas: {
3183
+ callGasLimit: userOp.callGasLimit.toString(),
3184
+ verificationGasLimit: userOp.verificationGasLimit.toString(),
3185
+ preVerificationGas: userOp.preVerificationGas.toString(),
3186
+ maxFeePerGas: userOp.maxFeePerGas.toString(),
3187
+ maxPriorityFeePerGas: userOp.maxPriorityFeePerGas.toString(),
3188
+ maxCost: `${formatEther3(maxCostWei)} ${nativeCurrency}`
3189
+ },
3190
+ sponsored,
3191
+ ...sponsored && userOp.paymaster ? { paymaster: userOp.paymaster } : {},
3192
+ balance: `${ethFormatted} ${nativeCurrency}`,
3193
+ ...warnings.length > 0 ? { warnings } : {}
3194
+ });
3116
3195
  } catch (err) {
3117
3196
  handleTxError(err);
3118
3197
  }
@@ -3120,21 +3199,21 @@ function registerTxCommand(program2, ctx) {
3120
3199
  }
3121
3200
  function parseAllTxSpecs(rawSpecs) {
3122
3201
  if (!rawSpecs || rawSpecs.length === 0) {
3123
- throw new TxError(ERR_INVALID_PARAMS, 'At least one --tx is required. Format: --tx "to:0xAddr,value:0.1"');
3202
+ throw new TxError(ERR_INVALID_PARAMS2, 'At least one --tx is required. Format: --tx "to:0xAddr,value:0.1"');
3124
3203
  }
3125
3204
  return rawSpecs.map((spec, i) => parseTxSpec(spec, i));
3126
3205
  }
3127
3206
  async function buildUserOp(ctx, target, specs, sponsor) {
3128
3207
  const identifier = target ?? ctx.account.currentAccount?.alias ?? ctx.account.currentAccount?.address;
3129
3208
  if (!identifier) {
3130
- throw new TxError(ERR_ACCOUNT_NOT_READY, "No account selected.", {
3209
+ throw new TxError(ERR_ACCOUNT_NOT_READY2, "No account selected.", {
3131
3210
  hint: "Specify an alias/address or create an account first."
3132
3211
  });
3133
3212
  }
3134
3213
  const accountInfo = resolveAccountStrict(ctx, identifier);
3135
3214
  const chainConfig = resolveChainStrict(ctx, accountInfo.chainId);
3136
3215
  if (!accountInfo.isDeployed) {
3137
- throw new TxError(ERR_ACCOUNT_NOT_READY, `Account "${accountInfo.alias}" is not deployed.`, {
3216
+ throw new TxError(ERR_ACCOUNT_NOT_READY2, `Account "${accountInfo.alias}" is not deployed.`, {
3138
3217
  account: accountInfo.alias,
3139
3218
  address: accountInfo.address,
3140
3219
  hint: "Run `elytro account activate` first."
@@ -3163,7 +3242,7 @@ async function buildUserOp(ctx, target, specs, sponsor) {
3163
3242
  try {
3164
3243
  userOp = await ctx.sdk.createSendUserOp(accountInfo.address, txs);
3165
3244
  } catch (err) {
3166
- spinner.fail("Build failed.");
3245
+ spinner.stop();
3167
3246
  throw new TxError(ERR_BUILD_FAILED, `Failed to build UserOp: ${err.message}`, {
3168
3247
  account: accountInfo.address,
3169
3248
  chain: chainConfig.name
@@ -3180,7 +3259,7 @@ async function buildUserOp(ctx, target, specs, sponsor) {
3180
3259
  userOp.verificationGasLimit = gasEstimate.verificationGasLimit;
3181
3260
  userOp.preVerificationGas = gasEstimate.preVerificationGas;
3182
3261
  } catch (err) {
3183
- spinner.fail("Gas estimation failed.");
3262
+ spinner.stop();
3184
3263
  throw new TxError(ERR_BUILD_FAILED, `Gas estimation failed: ${err.message}`, {
3185
3264
  account: accountInfo.address,
3186
3265
  chain: chainConfig.name
@@ -3202,8 +3281,8 @@ async function buildUserOp(ctx, target, specs, sponsor) {
3202
3281
  spinner.text = "Sponsorship unavailable, checking balance...";
3203
3282
  const { wei: balance } = await ctx.walletClient.getBalance(accountInfo.address);
3204
3283
  if (balance === 0n) {
3205
- spinner.fail("Build failed.");
3206
- throw new TxError(ERR_SPONSOR_FAILED, "Sponsorship failed and account has no ETH to pay gas.", {
3284
+ spinner.stop();
3285
+ throw new TxError(ERR_SPONSOR_FAILED2, "Sponsorship failed and account has no ETH to pay gas.", {
3207
3286
  reason: sponsorError ?? "unknown",
3208
3287
  account: accountInfo.address,
3209
3288
  chain: chainConfig.name,
@@ -3212,38 +3291,38 @@ async function buildUserOp(ctx, target, specs, sponsor) {
3212
3291
  }
3213
3292
  }
3214
3293
  }
3215
- spinner.succeed("UserOp built.");
3294
+ spinner.stop();
3216
3295
  return { userOp, accountInfo, chainConfig, sponsored, txType };
3217
3296
  }
3218
3297
  function resolveAccountStrict(ctx, identifier) {
3219
3298
  const account = ctx.account.resolveAccount(identifier);
3220
3299
  if (!account) {
3221
- throw new TxError(ERR_ACCOUNT_NOT_READY, `Account "${identifier}" not found.`, { identifier });
3300
+ throw new TxError(ERR_ACCOUNT_NOT_READY2, `Account "${identifier}" not found.`, { identifier });
3222
3301
  }
3223
3302
  return account;
3224
3303
  }
3225
3304
  function resolveChainStrict(ctx, chainId) {
3226
3305
  const chain = ctx.chain.chains.find((c) => c.id === chainId);
3227
3306
  if (!chain) {
3228
- throw new TxError(ERR_ACCOUNT_NOT_READY, `Chain ${chainId} not configured.`, { chainId });
3307
+ throw new TxError(ERR_ACCOUNT_NOT_READY2, `Chain ${chainId} not configured.`, { chainId });
3229
3308
  }
3230
3309
  return chain;
3231
3310
  }
3232
3311
  function serializeUserOp(op) {
3233
3312
  return {
3234
3313
  sender: op.sender,
3235
- nonce: toHex5(op.nonce),
3314
+ nonce: toHex6(op.nonce),
3236
3315
  factory: op.factory,
3237
3316
  factoryData: op.factoryData,
3238
3317
  callData: op.callData,
3239
- callGasLimit: toHex5(op.callGasLimit),
3240
- verificationGasLimit: toHex5(op.verificationGasLimit),
3241
- preVerificationGas: toHex5(op.preVerificationGas),
3242
- maxFeePerGas: toHex5(op.maxFeePerGas),
3243
- maxPriorityFeePerGas: toHex5(op.maxPriorityFeePerGas),
3318
+ callGasLimit: toHex6(op.callGasLimit),
3319
+ verificationGasLimit: toHex6(op.verificationGasLimit),
3320
+ preVerificationGas: toHex6(op.preVerificationGas),
3321
+ maxFeePerGas: toHex6(op.maxFeePerGas),
3322
+ maxPriorityFeePerGas: toHex6(op.maxPriorityFeePerGas),
3244
3323
  paymaster: op.paymaster,
3245
- paymasterVerificationGasLimit: op.paymasterVerificationGasLimit ? toHex5(op.paymasterVerificationGasLimit) : null,
3246
- paymasterPostOpGasLimit: op.paymasterPostOpGasLimit ? toHex5(op.paymasterPostOpGasLimit) : null,
3324
+ paymasterVerificationGasLimit: op.paymasterVerificationGasLimit ? toHex6(op.paymasterVerificationGasLimit) : null,
3325
+ paymasterPostOpGasLimit: op.paymasterPostOpGasLimit ? toHex6(op.paymasterPostOpGasLimit) : null,
3247
3326
  paymasterData: op.paymasterData,
3248
3327
  signature: op.signature
3249
3328
  };
@@ -3253,10 +3332,10 @@ function deserializeUserOp(json) {
3253
3332
  try {
3254
3333
  raw = JSON.parse(json);
3255
3334
  } catch {
3256
- throw new TxError(ERR_INVALID_PARAMS, "Invalid UserOp JSON. Pass a JSON-encoded UserOp object.", { json });
3335
+ throw new TxError(ERR_INVALID_PARAMS2, "Invalid UserOp JSON. Pass a JSON-encoded UserOp object.", { json });
3257
3336
  }
3258
3337
  if (!raw.sender || !raw.callData) {
3259
- throw new TxError(ERR_INVALID_PARAMS, "Invalid UserOp: missing required fields (sender, callData).");
3338
+ throw new TxError(ERR_INVALID_PARAMS2, "Invalid UserOp: missing required fields (sender, callData).");
3260
3339
  }
3261
3340
  return {
3262
3341
  sender: raw.sender,
@@ -3282,7 +3361,7 @@ import ora4 from "ora";
3282
3361
  import { isAddress as isAddress2, formatEther as formatEther4, formatUnits } from "viem";
3283
3362
 
3284
3363
  // src/utils/erc20.ts
3285
- import { encodeFunctionData, parseUnits } from "viem";
3364
+ import { encodeFunctionData as encodeFunctionData2, parseUnits } from "viem";
3286
3365
  var ERC20_ABI = [
3287
3366
  {
3288
3367
  name: "transfer",
@@ -3359,7 +3438,7 @@ function registerQueryCommand(program2, ctx) {
3359
3438
  getTokenBalance(ctx.walletClient, opts.token, accountInfo.address)
3360
3439
  ]);
3361
3440
  spinner.stop();
3362
- outputSuccess({
3441
+ outputResult({
3363
3442
  account: accountInfo.alias,
3364
3443
  address: accountInfo.address,
3365
3444
  chain: chainConfig.name,
@@ -3371,7 +3450,7 @@ function registerQueryCommand(program2, ctx) {
3371
3450
  } else {
3372
3451
  const { ether } = await ctx.walletClient.getBalance(accountInfo.address);
3373
3452
  spinner.stop();
3374
- outputSuccess({
3453
+ outputResult({
3375
3454
  account: accountInfo.alias,
3376
3455
  address: accountInfo.address,
3377
3456
  chain: chainConfig.name,
@@ -3391,20 +3470,25 @@ function registerQueryCommand(program2, ctx) {
3391
3470
  const rawBalances = await ctx.walletClient.getTokenBalances(accountInfo.address);
3392
3471
  if (rawBalances.length === 0) {
3393
3472
  spinner.stop();
3394
- heading(`Token Holdings (${accountInfo.alias})`);
3395
- info("Result", "No ERC-20 tokens found.");
3473
+ outputResult({
3474
+ account: accountInfo.alias,
3475
+ address: accountInfo.address,
3476
+ chain: chainConfig.name,
3477
+ tokens: [],
3478
+ total: 0
3479
+ });
3396
3480
  return;
3397
3481
  }
3398
3482
  spinner.text = `Fetching metadata for ${rawBalances.length} tokens...`;
3399
3483
  const tokens = await Promise.all(
3400
3484
  rawBalances.map(async ({ tokenAddress, balance }) => {
3401
3485
  try {
3402
- const info2 = await getTokenInfo(ctx.walletClient, tokenAddress);
3486
+ const info = await getTokenInfo(ctx.walletClient, tokenAddress);
3403
3487
  return {
3404
3488
  address: tokenAddress,
3405
- symbol: info2.symbol,
3406
- decimals: info2.decimals,
3407
- balance: formatUnits(balance, info2.decimals),
3489
+ symbol: info.symbol,
3490
+ decimals: info.decimals,
3491
+ balance: formatUnits(balance, info.decimals),
3408
3492
  rawBalance: balance
3409
3493
  };
3410
3494
  } catch {
@@ -3419,25 +3503,18 @@ function registerQueryCommand(program2, ctx) {
3419
3503
  })
3420
3504
  );
3421
3505
  spinner.stop();
3422
- heading(`Token Holdings (${accountInfo.alias})`);
3423
- info("Account", accountInfo.address);
3424
- info("Chain", `${chainConfig.name} (${chainConfig.id})`);
3425
- info("Tokens", tokens.length.toString());
3426
- console.log("");
3427
- table(
3428
- tokens.map((t) => ({
3506
+ outputResult({
3507
+ account: accountInfo.alias,
3508
+ address: accountInfo.address,
3509
+ chain: chainConfig.name,
3510
+ tokens: tokens.map((t) => ({
3429
3511
  address: t.address,
3430
3512
  symbol: t.symbol,
3431
- decimals: String(t.decimals),
3513
+ decimals: t.decimals,
3432
3514
  balance: t.balance
3433
3515
  })),
3434
- [
3435
- { key: "address", label: "Token Address", width: 44 },
3436
- { key: "symbol", label: "Symbol", width: 10 },
3437
- { key: "decimals", label: "Decimals", width: 10 },
3438
- { key: "balance", label: "Balance", width: 24 }
3439
- ]
3440
- );
3516
+ total: tokens.length
3517
+ });
3441
3518
  } catch (err) {
3442
3519
  outputError(-32e3, sanitizeErrorMessage(err.message));
3443
3520
  }
@@ -3463,7 +3540,7 @@ function registerQueryCommand(program2, ctx) {
3463
3540
  return;
3464
3541
  }
3465
3542
  spinner.stop();
3466
- outputSuccess({
3543
+ outputResult({
3467
3544
  hash: receipt.transactionHash,
3468
3545
  status: receipt.status,
3469
3546
  block: receipt.blockNumber.toString(),
@@ -3486,7 +3563,7 @@ function registerQueryCommand(program2, ctx) {
3486
3563
  ctx.walletClient.getGasPrice()
3487
3564
  ]);
3488
3565
  spinner.stop();
3489
- outputSuccess({
3566
+ outputResult({
3490
3567
  chainId: chainConfig.id,
3491
3568
  name: chainConfig.name,
3492
3569
  nativeCurrency: chainConfig.nativeCurrency.symbol,
@@ -3516,7 +3593,7 @@ function registerQueryCommand(program2, ctx) {
3516
3593
  const isContract = !!code && code !== "0x";
3517
3594
  const codeSize = isContract ? (code.length - 2) / 2 : 0;
3518
3595
  spinner.stop();
3519
- outputSuccess({
3596
+ outputResult({
3520
3597
  address: addr,
3521
3598
  chain: chainConfig.name,
3522
3599
  type: isContract ? "contract" : "EOA",
@@ -3557,53 +3634,15 @@ function resolveCurrentChain(ctx) {
3557
3634
  function isHex66(s) {
3558
3635
  return /^0x[0-9a-fA-F]{64}$/.test(s);
3559
3636
  }
3560
- function outputSuccess(result) {
3561
- console.log(JSON.stringify({ success: true, result }, null, 2));
3562
- }
3563
- function outputError(code, message, data) {
3564
- txError({ code, message, data });
3565
- process.exitCode = 1;
3566
- }
3567
3637
 
3568
3638
  // src/commands/security.ts
3569
3639
  import ora5 from "ora";
3570
-
3571
- // src/utils/contracts/securityHook.ts
3572
- import { encodeFunctionData as encodeFunctionData2, parseAbi as parseAbi2, pad, toHex as toHex6 } from "viem";
3573
- function encodeInstallHook(walletAddress, hookAddress, safetyDelay = DEFAULT_SAFETY_DELAY, capabilityFlags = DEFAULT_CAPABILITY) {
3574
- const safetyDelayHex = pad(toHex6(safetyDelay), { size: 4 }).slice(2);
3575
- const hookAndData = hookAddress + safetyDelayHex;
3576
- const callData = encodeFunctionData2({
3577
- abi: parseAbi2(["function installHook(bytes calldata hookAndData, uint8 capabilityFlags)"]),
3578
- functionName: "installHook",
3579
- args: [hookAndData, capabilityFlags]
3580
- });
3581
- return { to: walletAddress, value: "0", data: callData };
3582
- }
3583
- function encodeUninstallHook(walletAddress, hookAddress) {
3584
- const callData = encodeFunctionData2({
3585
- abi: parseAbi2(["function uninstallHook(address)"]),
3586
- functionName: "uninstallHook",
3587
- args: [hookAddress]
3588
- });
3589
- return { to: walletAddress, value: "0", data: callData };
3590
- }
3591
- function encodeForcePreUninstall(hookAddress) {
3592
- const callData = encodeFunctionData2({
3593
- abi: parseAbi2(["function forcePreUninstall()"]),
3594
- functionName: "forcePreUninstall",
3595
- args: []
3596
- });
3597
- return { to: hookAddress, value: "0", data: callData };
3598
- }
3599
-
3600
- // src/commands/security.ts
3601
- var ERR_ACCOUNT_NOT_READY2 = -32002;
3640
+ var ERR_ACCOUNT_NOT_READY3 = -32002;
3602
3641
  var ERR_HOOK_AUTH_FAILED = -32007;
3603
3642
  var ERR_EMAIL_NOT_BOUND = -32010;
3604
3643
  var ERR_SAFETY_DELAY = -32011;
3605
3644
  var ERR_OTP_VERIFY_FAILED = -32012;
3606
- var ERR_INTERNAL2 = -32e3;
3645
+ var ERR_INTERNAL3 = -32e3;
3607
3646
  var SecurityError = class extends Error {
3608
3647
  code;
3609
3648
  data;
@@ -3616,33 +3655,32 @@ var SecurityError = class extends Error {
3616
3655
  };
3617
3656
  function handleSecurityError(err) {
3618
3657
  if (err instanceof SecurityError) {
3619
- txError({ code: err.code, message: sanitizeErrorMessage(err.message), data: err.data });
3658
+ outputError(err.code, err.message, err.data);
3620
3659
  } else {
3621
- txError({
3622
- code: ERR_INTERNAL2,
3623
- message: sanitizeErrorMessage(err.message ?? String(err))
3624
- });
3660
+ outputError(ERR_INTERNAL3, err.message ?? String(err));
3625
3661
  }
3626
- process.exitCode = 1;
3627
3662
  }
3628
3663
  function initSecurityContext(ctx) {
3629
3664
  if (!ctx.keyring.isUnlocked) {
3630
- throw new SecurityError(ERR_ACCOUNT_NOT_READY2, "Wallet not initialized. Run `elytro init` first.");
3665
+ throw new SecurityError(
3666
+ ERR_ACCOUNT_NOT_READY3,
3667
+ "Keyring is locked. Run `elytro init` to initialize, or check your secret provider (Keychain / ELYTRO_VAULT_SECRET)."
3668
+ );
3631
3669
  }
3632
3670
  const current = ctx.account.currentAccount;
3633
3671
  if (!current) {
3634
- throw new SecurityError(ERR_ACCOUNT_NOT_READY2, "No account selected. Run `elytro account create` first.");
3672
+ throw new SecurityError(ERR_ACCOUNT_NOT_READY3, "No account selected. Run `elytro account create` first.");
3635
3673
  }
3636
3674
  const account = ctx.account.resolveAccount(current.alias ?? current.address);
3637
3675
  if (!account) {
3638
- throw new SecurityError(ERR_ACCOUNT_NOT_READY2, "Account not found.");
3676
+ throw new SecurityError(ERR_ACCOUNT_NOT_READY3, "Account not found.");
3639
3677
  }
3640
3678
  if (!account.isDeployed) {
3641
- throw new SecurityError(ERR_ACCOUNT_NOT_READY2, "Account not deployed. Run `elytro account activate` first.");
3679
+ throw new SecurityError(ERR_ACCOUNT_NOT_READY3, "Account not deployed. Run `elytro account activate` first.");
3642
3680
  }
3643
3681
  const chainConfig = ctx.chain.chains.find((c) => c.id === account.chainId);
3644
3682
  if (!chainConfig) {
3645
- throw new SecurityError(ERR_ACCOUNT_NOT_READY2, `No chain config for chainId ${account.chainId}.`);
3683
+ throw new SecurityError(ERR_ACCOUNT_NOT_READY3, `No chain config for chainId ${account.chainId}.`);
3646
3684
  }
3647
3685
  ctx.walletClient.initForChain(chainConfig);
3648
3686
  const hookService = new SecurityHookService({
@@ -3701,14 +3739,8 @@ async function signAndSend(ctx, chainConfig, userOp, spinner) {
3701
3739
  spinner.text = "Waiting for receipt...";
3702
3740
  const receipt = await ctx.sdk.waitForReceipt(opHash);
3703
3741
  spinner.stop();
3704
- if (receipt.success) {
3705
- success("Transaction confirmed!");
3706
- info("Tx Hash", receipt.transactionHash);
3707
- if (chainConfig.blockExplorer) {
3708
- info("Explorer", `${chainConfig.blockExplorer}/tx/${receipt.transactionHash}`);
3709
- }
3710
- } else {
3711
- throw new SecurityError(ERR_INTERNAL2, "Transaction reverted on-chain.", {
3742
+ if (!receipt.success) {
3743
+ throw new SecurityError(ERR_INTERNAL3, "Transaction reverted on-chain.", {
3712
3744
  txHash: receipt.transactionHash
3713
3745
  });
3714
3746
  }
@@ -3737,14 +3769,8 @@ async function signWithHookAndSend(ctx, chainConfig, account, hookService, userO
3737
3769
  spinner.text = "Waiting for receipt...";
3738
3770
  const receipt = await ctx.sdk.waitForReceipt(opHash);
3739
3771
  spinner.stop();
3740
- if (receipt.success) {
3741
- success("Transaction confirmed!");
3742
- info("Tx Hash", receipt.transactionHash);
3743
- if (chainConfig.blockExplorer) {
3744
- info("Explorer", `${chainConfig.blockExplorer}/tx/${receipt.transactionHash}`);
3745
- }
3746
- } else {
3747
- throw new SecurityError(ERR_INTERNAL2, "Transaction reverted on-chain.", {
3772
+ if (!receipt.success) {
3773
+ throw new SecurityError(ERR_INTERNAL3, "Transaction reverted on-chain.", {
3748
3774
  txHash: receipt.transactionHash
3749
3775
  });
3750
3776
  }
@@ -3755,14 +3781,12 @@ async function handleOtpChallenge(hookService, account, ctx, userOp, hookResult)
3755
3781
  if (errCode !== "OTP_REQUIRED" && errCode !== "SPENDING_LIMIT_EXCEEDED") {
3756
3782
  throw new SecurityError(ERR_HOOK_AUTH_FAILED, `Hook authorization failed: ${err.message ?? errCode}`);
3757
3783
  }
3758
- warn(err.message ?? `Verification required (${errCode}).`);
3759
- if (err.maskedEmail) {
3760
- info("OTP sent to", err.maskedEmail);
3761
- }
3762
- if (errCode === "SPENDING_LIMIT_EXCEEDED" && err.projectedSpendUsdCents !== void 0) {
3763
- info("Projected spend", `$${(err.projectedSpendUsdCents / 100).toFixed(2)}`);
3764
- info("Daily limit", `$${((err.dailyLimitUsdCents ?? 0) / 100).toFixed(2)}`);
3765
- }
3784
+ console.error(JSON.stringify({
3785
+ challenge: errCode,
3786
+ message: err.message ?? `Verification required (${errCode}).`,
3787
+ ...err.maskedEmail ? { maskedEmail: err.maskedEmail } : {},
3788
+ ...errCode === "SPENDING_LIMIT_EXCEEDED" && err.projectedSpendUsdCents !== void 0 ? { projectedSpendUsd: (err.projectedSpendUsdCents / 100).toFixed(2), dailyLimitUsd: ((err.dailyLimitUsdCents ?? 0) / 100).toFixed(2) } : {}
3789
+ }, null, 2));
3766
3790
  const otpCode = await askInput("Enter the 6-digit OTP code:");
3767
3791
  const verifySpinner = ora5("Verifying OTP...").start();
3768
3792
  try {
@@ -3802,37 +3826,34 @@ function registerSecurityCommand(program2, ctx) {
3802
3826
  } catch {
3803
3827
  }
3804
3828
  spinner.stop();
3805
- heading("Security Status");
3806
- info("Account", `${account.alias} (${address(account.address)})`);
3807
- info("Chain", `${chainConfig.name} (${account.chainId})`);
3808
- info("Hook Installed", hookStatus.installed ? "Yes" : "No");
3809
- if (hookStatus.installed) {
3810
- info("Hook Address", address(hookStatus.hookAddress));
3811
- info(
3812
- "Capabilities",
3813
- [
3814
- hookStatus.capabilities.preUserOpValidation && "UserOp",
3815
- hookStatus.capabilities.preIsValidSignature && "Signature"
3816
- ].filter(Boolean).join(" + ") || "None"
3817
- );
3818
- if (hookStatus.forceUninstall.initiated) {
3819
- info(
3820
- "Force Uninstall",
3821
- hookStatus.forceUninstall.canExecute ? "Ready to execute" : `Pending until ${hookStatus.forceUninstall.availableAfter}`
3822
- );
3823
- }
3824
- }
3825
- if (profile) {
3826
- console.log("");
3827
- info("Email", profile.maskedEmail ?? profile.email ?? "Not bound");
3828
- info("Email Verified", profile.emailVerified ? "Yes" : "No");
3829
- if (profile.dailyLimitUsdCents !== void 0) {
3830
- info("Daily Limit", `$${(profile.dailyLimitUsdCents / 100).toFixed(2)}`);
3831
- }
3832
- } else if (hookStatus.installed) {
3833
- console.log("");
3834
- warn("Security profile not loaded (not yet authenticated or email not bound).");
3835
- }
3829
+ outputResult({
3830
+ account: account.alias,
3831
+ address: account.address,
3832
+ chain: chainConfig.name,
3833
+ chainId: account.chainId,
3834
+ hookInstalled: hookStatus.installed,
3835
+ ...hookStatus.installed ? {
3836
+ hookAddress: hookStatus.hookAddress,
3837
+ capabilities: {
3838
+ preUserOpValidation: hookStatus.capabilities.preUserOpValidation,
3839
+ preIsValidSignature: hookStatus.capabilities.preIsValidSignature
3840
+ },
3841
+ ...hookStatus.forceUninstall.initiated ? {
3842
+ forceUninstall: {
3843
+ initiated: true,
3844
+ canExecute: hookStatus.forceUninstall.canExecute,
3845
+ availableAfter: hookStatus.forceUninstall.availableAfter
3846
+ }
3847
+ } : {}
3848
+ } : {},
3849
+ ...profile ? {
3850
+ profile: {
3851
+ email: profile.maskedEmail ?? profile.email ?? null,
3852
+ emailVerified: profile.emailVerified,
3853
+ ...profile.dailyLimitUsdCents !== void 0 ? { dailyLimitUsd: (profile.dailyLimitUsdCents / 100).toFixed(2) } : {}
3854
+ }
3855
+ } : {}
3856
+ });
3836
3857
  } catch (err) {
3837
3858
  handleSecurityError(err);
3838
3859
  }
@@ -3850,25 +3871,22 @@ function registerSecurityCommand(program2, ctx) {
3850
3871
  const currentStatus = await hookService.getHookStatus(account.address, account.chainId);
3851
3872
  spinner.stop();
3852
3873
  if (currentStatus.installed) {
3853
- warn("SecurityHook is already installed on this account.");
3874
+ outputResult({ status: "already_installed", account: account.alias, address: account.address });
3854
3875
  return;
3855
3876
  }
3856
3877
  const hookAddress = SECURITY_HOOK_ADDRESS_MAP[account.chainId];
3857
3878
  if (!hookAddress) {
3858
- throw new SecurityError(ERR_INTERNAL2, `SecurityHook not deployed on chain ${account.chainId}.`);
3879
+ throw new SecurityError(ERR_INTERNAL3, `SecurityHook not deployed on chain ${account.chainId}.`);
3859
3880
  }
3860
3881
  const capabilityFlags = Number(opts.capability);
3861
3882
  if (![1, 2, 3].includes(capabilityFlags)) {
3862
- throw new SecurityError(ERR_INTERNAL2, "Invalid capability flags. Use 1, 2, or 3.");
3883
+ throw new SecurityError(ERR_INTERNAL3, "Invalid capability flags. Use 1, 2, or 3.");
3863
3884
  }
3864
- heading("Install SecurityHook");
3865
- info("Account", `${account.alias} (${address(account.address)})`);
3866
- info("Hook Address", address(hookAddress));
3867
- info("Capability", CAPABILITY_LABELS[capabilityFlags]);
3868
- info("Safety Delay", `${DEFAULT_SAFETY_DELAY}s`);
3869
- const confirmed = await askConfirm("Proceed with hook installation?");
3885
+ const confirmed = await askConfirm(
3886
+ `Install SecurityHook on ${account.alias} (${address(account.address)})? Capability: ${CAPABILITY_LABELS[capabilityFlags]}, Safety Delay: ${DEFAULT_SAFETY_DELAY}s`
3887
+ );
3870
3888
  if (!confirmed) {
3871
- warn("Cancelled.");
3889
+ outputResult({ status: "cancelled" });
3872
3890
  return;
3873
3891
  }
3874
3892
  const installTx = encodeInstallHook(account.address, hookAddress, DEFAULT_SAFETY_DELAY, capabilityFlags);
@@ -3876,7 +3894,14 @@ function registerSecurityCommand(program2, ctx) {
3876
3894
  try {
3877
3895
  const userOp = await buildUserOp2(ctx, chainConfig, account, [installTx], buildSpinner);
3878
3896
  await signAndSend(ctx, chainConfig, userOp, buildSpinner);
3879
- success("SecurityHook installed successfully!");
3897
+ outputResult({
3898
+ status: "installed",
3899
+ account: account.alias,
3900
+ address: account.address,
3901
+ hookAddress,
3902
+ capability: CAPABILITY_LABELS[capabilityFlags],
3903
+ safetyDelay: DEFAULT_SAFETY_DELAY
3904
+ });
3880
3905
  } catch (innerErr) {
3881
3906
  buildSpinner.stop();
3882
3907
  throw innerErr;
@@ -3893,7 +3918,7 @@ function registerSecurityCommand(program2, ctx) {
3893
3918
  const currentStatus = await hookService.getHookStatus(account.address, account.chainId);
3894
3919
  spinner.stop();
3895
3920
  if (!currentStatus.installed) {
3896
- warn("SecurityHook is not installed on this account.");
3921
+ outputResult({ status: "not_installed", account: account.alias, address: account.address });
3897
3922
  return;
3898
3923
  }
3899
3924
  const hookAddress = currentStatus.hookAddress;
@@ -3922,8 +3947,7 @@ function registerSecurityCommand(program2, ctx) {
3922
3947
  throw new SecurityError(ERR_HOOK_AUTH_FAILED, sanitizeErrorMessage(err.message));
3923
3948
  }
3924
3949
  spinner.stop();
3925
- info("OTP sent to", bindingResult.maskedEmail);
3926
- info("Expires at", bindingResult.otpExpiresAt);
3950
+ console.error(JSON.stringify({ otpSentTo: bindingResult.maskedEmail, expiresAt: bindingResult.otpExpiresAt }, null, 2));
3927
3951
  const otpCode = await askInput("Enter the 6-digit OTP code:");
3928
3952
  const confirmSpinner = ora5("Confirming email binding...").start();
3929
3953
  try {
@@ -3934,9 +3958,11 @@ function registerSecurityCommand(program2, ctx) {
3934
3958
  otpCode.trim()
3935
3959
  );
3936
3960
  confirmSpinner.stop();
3937
- success("Email bound successfully!");
3938
- info("Email", profile.maskedEmail ?? profile.email ?? emailAddr);
3939
- info("Verified", profile.emailVerified ? "Yes" : "No");
3961
+ outputResult({
3962
+ status: "email_bound",
3963
+ email: profile.maskedEmail ?? profile.email ?? emailAddr,
3964
+ emailVerified: profile.emailVerified
3965
+ });
3940
3966
  } catch (err) {
3941
3967
  confirmSpinner.stop();
3942
3968
  throw new SecurityError(
@@ -3961,7 +3987,7 @@ function registerSecurityCommand(program2, ctx) {
3961
3987
  throw new SecurityError(ERR_HOOK_AUTH_FAILED, sanitizeErrorMessage(err.message));
3962
3988
  }
3963
3989
  spinner.stop();
3964
- info("OTP sent to", bindingResult.maskedEmail);
3990
+ console.error(JSON.stringify({ otpSentTo: bindingResult.maskedEmail }, null, 2));
3965
3991
  const otpCode = await askInput("Enter the 6-digit OTP code:");
3966
3992
  const confirmSpinner = ora5("Confirming email change...").start();
3967
3993
  try {
@@ -3972,8 +3998,10 @@ function registerSecurityCommand(program2, ctx) {
3972
3998
  otpCode.trim()
3973
3999
  );
3974
4000
  confirmSpinner.stop();
3975
- success("Email changed successfully!");
3976
- info("Email", profile.maskedEmail ?? profile.email ?? emailAddr);
4001
+ outputResult({
4002
+ status: "email_changed",
4003
+ email: profile.maskedEmail ?? profile.email ?? emailAddr
4004
+ });
3977
4005
  } catch (err) {
3978
4006
  confirmSpinner.stop();
3979
4007
  throw new SecurityError(
@@ -4012,11 +4040,9 @@ async function handleForceExecute(ctx, chainConfig, account, currentStatus) {
4012
4040
  `Safety delay not elapsed. Available after ${currentStatus.forceUninstall.availableAfter}.`
4013
4041
  );
4014
4042
  }
4015
- heading("Execute Force Uninstall");
4016
- info("Account", `${account.alias} (${address(account.address)})`);
4017
- const confirmed = await askConfirm("Execute force uninstall? This will remove the SecurityHook.");
4043
+ const confirmed = await askConfirm(`Execute force uninstall on ${account.alias} (${address(account.address)})? This will remove the SecurityHook.`);
4018
4044
  if (!confirmed) {
4019
- warn("Cancelled.");
4045
+ outputResult({ status: "cancelled" });
4020
4046
  return;
4021
4047
  }
4022
4048
  const uninstallTx = encodeUninstallHook(account.address, currentStatus.hookAddress);
@@ -4024,6 +4050,7 @@ async function handleForceExecute(ctx, chainConfig, account, currentStatus) {
4024
4050
  try {
4025
4051
  const userOp = await buildUserOp2(ctx, chainConfig, account, [uninstallTx], spinner);
4026
4052
  await signAndSend(ctx, chainConfig, userOp, spinner);
4053
+ outputResult({ status: "force_uninstalled", account: account.alias, address: account.address });
4027
4054
  } catch (err) {
4028
4055
  spinner.stop();
4029
4056
  throw err;
@@ -4031,22 +4058,19 @@ async function handleForceExecute(ctx, chainConfig, account, currentStatus) {
4031
4058
  }
4032
4059
  async function handleForceStart(ctx, chainConfig, account, currentStatus, hookAddress) {
4033
4060
  if (currentStatus.forceUninstall.initiated) {
4034
- if (currentStatus.forceUninstall.canExecute) {
4035
- info("Force Uninstall", "Ready to execute. Run `security 2fa uninstall --force --execute`.");
4036
- } else {
4037
- info(
4038
- "Force Uninstall",
4039
- `Already initiated. Available after ${currentStatus.forceUninstall.availableAfter}.`
4040
- );
4041
- }
4061
+ outputResult({
4062
+ status: "already_initiated",
4063
+ canExecute: currentStatus.forceUninstall.canExecute,
4064
+ availableAfter: currentStatus.forceUninstall.availableAfter,
4065
+ hint: currentStatus.forceUninstall.canExecute ? "Run `security 2fa uninstall --force --execute`." : `Wait until ${currentStatus.forceUninstall.availableAfter}.`
4066
+ });
4042
4067
  return;
4043
4068
  }
4044
- heading("Start Force Uninstall");
4045
- info("Account", `${account.alias} (${address(account.address)})`);
4046
- warn(`After starting, you must wait the safety delay (${DEFAULT_SAFETY_DELAY}s) before executing.`);
4047
- const confirmed = await askConfirm("Start force-uninstall countdown?");
4069
+ const confirmed = await askConfirm(
4070
+ `Start force-uninstall countdown on ${account.alias} (${address(account.address)})? You must wait ${DEFAULT_SAFETY_DELAY}s before executing.`
4071
+ );
4048
4072
  if (!confirmed) {
4049
- warn("Cancelled.");
4073
+ outputResult({ status: "cancelled" });
4050
4074
  return;
4051
4075
  }
4052
4076
  const preUninstallTx = encodeForcePreUninstall(hookAddress);
@@ -4054,17 +4078,21 @@ async function handleForceStart(ctx, chainConfig, account, currentStatus, hookAd
4054
4078
  try {
4055
4079
  const userOp = await buildUserOp2(ctx, chainConfig, account, [preUninstallTx], spinner);
4056
4080
  await signAndSend(ctx, chainConfig, userOp, spinner);
4081
+ outputResult({
4082
+ status: "force_uninstall_started",
4083
+ account: account.alias,
4084
+ address: account.address,
4085
+ safetyDelay: DEFAULT_SAFETY_DELAY
4086
+ });
4057
4087
  } catch (err) {
4058
4088
  spinner.stop();
4059
4089
  throw err;
4060
4090
  }
4061
4091
  }
4062
4092
  async function handleNormalUninstall(ctx, chainConfig, account, hookService, hookAddress) {
4063
- heading("Uninstall SecurityHook");
4064
- info("Account", `${account.alias} (${address(account.address)})`);
4065
- const confirmed = await askConfirm("Proceed with hook uninstall? (requires 2FA approval)");
4093
+ const confirmed = await askConfirm(`Uninstall SecurityHook from ${account.alias} (${address(account.address)})? (requires 2FA approval)`);
4066
4094
  if (!confirmed) {
4067
- warn("Cancelled.");
4095
+ outputResult({ status: "cancelled" });
4068
4096
  return;
4069
4097
  }
4070
4098
  const uninstallTx = encodeUninstallHook(account.address, hookAddress);
@@ -4072,6 +4100,7 @@ async function handleNormalUninstall(ctx, chainConfig, account, hookService, hoo
4072
4100
  try {
4073
4101
  const userOp = await buildUserOp2(ctx, chainConfig, account, [uninstallTx], spinner);
4074
4102
  await signWithHookAndSend(ctx, chainConfig, account, hookService, userOp, spinner);
4103
+ outputResult({ status: "uninstalled", account: account.alias, address: account.address });
4075
4104
  } catch (err) {
4076
4105
  spinner.stop();
4077
4106
  throw err;
@@ -4088,24 +4117,23 @@ async function showSpendingLimit(hookService, account) {
4088
4117
  }
4089
4118
  spinner.stop();
4090
4119
  if (!profile) {
4091
- warn("No security profile found. Bind an email first: `elytro security email bind <email>`.");
4120
+ outputResult({
4121
+ status: "no_profile",
4122
+ hint: "Bind an email first: `elytro security email bind <email>`."
4123
+ });
4092
4124
  return;
4093
4125
  }
4094
- heading("Spending Limit");
4095
- info(
4096
- "Daily Limit",
4097
- profile.dailyLimitUsdCents !== void 0 ? `$${(profile.dailyLimitUsdCents / 100).toFixed(2)}` : "Not set"
4098
- );
4099
- info("Email", profile.maskedEmail ?? "Not bound");
4126
+ outputResult({
4127
+ dailyLimitUsd: profile.dailyLimitUsdCents !== void 0 ? (profile.dailyLimitUsdCents / 100).toFixed(2) : null,
4128
+ email: profile.maskedEmail ?? null
4129
+ });
4100
4130
  }
4101
4131
  async function setSpendingLimit(hookService, account, amountStr) {
4102
4132
  const amountUsd = parseFloat(amountStr);
4103
4133
  if (isNaN(amountUsd) || amountUsd < 0) {
4104
- throw new SecurityError(ERR_INTERNAL2, "Invalid amount. Provide a positive number in USD.");
4134
+ throw new SecurityError(ERR_INTERNAL3, "Invalid amount. Provide a positive number in USD.");
4105
4135
  }
4106
4136
  const dailyLimitUsdCents = Math.round(amountUsd * 100);
4107
- heading("Set Daily Spending Limit");
4108
- info("New Limit", `$${amountUsd.toFixed(2)}`);
4109
4137
  const spinner = ora5("Requesting OTP for limit change...").start();
4110
4138
  let otpResult;
4111
4139
  try {
@@ -4119,13 +4147,16 @@ async function setSpendingLimit(hookService, account, amountStr) {
4119
4147
  throw new SecurityError(ERR_HOOK_AUTH_FAILED, sanitizeErrorMessage(msg));
4120
4148
  }
4121
4149
  spinner.stop();
4122
- info("OTP sent to", otpResult.maskedEmail);
4150
+ console.error(JSON.stringify({ otpSentTo: otpResult.maskedEmail }, null, 2));
4123
4151
  const otpCode = await askInput("Enter the 6-digit OTP code:");
4124
4152
  const setSpinner = ora5("Setting daily limit...").start();
4125
4153
  try {
4126
4154
  await hookService.setDailyLimit(account.address, account.chainId, dailyLimitUsdCents, otpCode.trim());
4127
4155
  setSpinner.stop();
4128
- success(`Daily limit set to $${amountUsd.toFixed(2)}.`);
4156
+ outputResult({
4157
+ status: "daily_limit_set",
4158
+ dailyLimitUsd: amountUsd.toFixed(2)
4159
+ });
4129
4160
  } catch (err) {
4130
4161
  setSpinner.stop();
4131
4162
  throw new SecurityError(
@@ -4148,58 +4179,51 @@ function maskKey(value) {
4148
4179
  function registerConfigCommand(program2, ctx) {
4149
4180
  const configCmd = program2.command("config").description("Manage CLI configuration (API keys, RPC endpoints)");
4150
4181
  configCmd.command("show").description("Show current endpoint configuration").action(() => {
4151
- heading("Configuration");
4152
4182
  const keys = ctx.chain.getUserKeys();
4153
- const hasAlchemy = !!keys.alchemyKey;
4154
- const hasPimlico = !!keys.pimlicoKey;
4155
- info("RPC provider", hasAlchemy ? "Alchemy (user-configured)" : "Public (publicnode.com)");
4156
- info("Bundler provider", hasPimlico ? "Pimlico (user-configured)" : "Public (pimlico.io/public)");
4157
- if (keys.alchemyKey) {
4158
- info("Alchemy key", maskKey(keys.alchemyKey));
4159
- }
4160
- if (keys.pimlicoKey) {
4161
- info("Pimlico key", maskKey(keys.pimlicoKey));
4162
- }
4163
- console.log("");
4164
4183
  const chain = ctx.chain.currentChain;
4165
- info("Current chain", `${chain.name} (${chain.id})`);
4166
- info("RPC endpoint", maskApiKeys(chain.endpoint));
4167
- info("Bundler", maskApiKeys(chain.bundler));
4168
- if (!hasAlchemy || !hasPimlico) {
4169
- console.log("");
4170
- warn("Public endpoints have rate limits. Set your own keys for production use:");
4171
- if (!hasAlchemy) console.log(" elytro config set alchemy-key <YOUR_KEY>");
4172
- if (!hasPimlico) console.log(" elytro config set pimlico-key <YOUR_KEY>");
4173
- }
4184
+ outputResult({
4185
+ rpcProvider: keys.alchemyKey ? "Alchemy (user-configured)" : "Public (publicnode.com)",
4186
+ bundlerProvider: keys.pimlicoKey ? "Pimlico (user-configured)" : "Public (pimlico.io/public)",
4187
+ ...keys.alchemyKey ? { alchemyKey: maskKey(keys.alchemyKey) } : {},
4188
+ ...keys.pimlicoKey ? { pimlicoKey: maskKey(keys.pimlicoKey) } : {},
4189
+ currentChain: chain.name,
4190
+ chainId: chain.id,
4191
+ rpcEndpoint: maskApiKeys(chain.endpoint),
4192
+ bundler: maskApiKeys(chain.bundler)
4193
+ });
4174
4194
  });
4175
4195
  configCmd.command("set <key> <value>").description(`Set an API key (${VALID_KEYS.join(" | ")})`).action(async (key, value) => {
4176
4196
  const mapped = KEY_MAP[key];
4177
4197
  if (!mapped) {
4178
- error(`Unknown key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
4179
- process.exitCode = 1;
4198
+ outputError(-32602, `Unknown key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
4180
4199
  return;
4181
4200
  }
4182
4201
  await ctx.chain.setUserKey(mapped, value);
4183
- success(`${key} saved. Endpoints updated.`);
4184
4202
  const chain = ctx.chain.currentChain;
4185
- info("RPC endpoint", maskApiKeys(chain.endpoint));
4186
- info("Bundler", maskApiKeys(chain.bundler));
4203
+ outputResult({
4204
+ key,
4205
+ status: "saved",
4206
+ rpcEndpoint: maskApiKeys(chain.endpoint),
4207
+ bundler: maskApiKeys(chain.bundler)
4208
+ });
4187
4209
  });
4188
4210
  configCmd.command("remove <key>").description(`Remove an API key and revert to public endpoint (${VALID_KEYS.join(" | ")})`).action(async (key) => {
4189
4211
  const mapped = KEY_MAP[key];
4190
4212
  if (!mapped) {
4191
- error(`Unknown key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
4192
- process.exitCode = 1;
4213
+ outputError(-32602, `Unknown key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
4193
4214
  return;
4194
4215
  }
4195
4216
  await ctx.chain.removeUserKey(mapped);
4196
- success(`${key} removed. Reverted to public endpoint.`);
4217
+ outputResult({
4218
+ key,
4219
+ status: "removed"
4220
+ });
4197
4221
  });
4198
4222
  }
4199
4223
 
4200
4224
  // src/index.ts
4201
4225
  var program = new Command();
4202
- program.name("elytro").description("Elytro \u2014 ERC-4337 Smart Account Wallet CLI").version("0.0.1");
4226
+ program.name("elytro").description("Elytro \u2014 ERC-4337 Smart Account Wallet CLI").version("0.0.1").addHelpText("after", "\nLearn how to use Elytro skills: https://github.com/Elytro-eth/skills\n");
4203
4227
  async function main() {
4204
4228
  let ctx = null;
4205
4229
  try {
@@ -4212,8 +4236,7 @@ async function main() {
4212
4236
  registerConfigCommand(program, ctx);
4213
4237
  await program.parseAsync(process.argv);
4214
4238
  } catch (err) {
4215
- error(sanitizeErrorMessage(err.message));
4216
- process.exitCode = 1;
4239
+ outputError(-32e3, sanitizeErrorMessage(err.message));
4217
4240
  } finally {
4218
4241
  ctx?.keyring.lock();
4219
4242
  }