@elytro/cli 0.2.0 → 0.5.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.
- package/README.md +89 -0
- package/dist/index.js +730 -576
- 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
|
|
1379
|
-
* @param alias
|
|
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(
|
|
1791
|
-
if (!
|
|
1792
|
-
const msg = String(
|
|
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
|
-
|
|
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
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
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
|
-
|
|
2377
|
+
providerName = initProvider.name;
|
|
2335
2378
|
} else {
|
|
2336
|
-
|
|
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.
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
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.
|
|
2430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
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.
|
|
2530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2551
|
-
|
|
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
|
|
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
|
-
|
|
2560
|
-
|
|
2696
|
+
chainId: a.chainId,
|
|
2697
|
+
deployed: a.isDeployed,
|
|
2698
|
+
recovery: a.isRecoveryEnabled
|
|
2561
2699
|
};
|
|
2562
2700
|
}),
|
|
2563
|
-
|
|
2564
|
-
|
|
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
|
-
|
|
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.
|
|
2584
|
-
|
|
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
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
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.
|
|
2606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2653
|
-
|
|
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
|
|
2662
|
-
var
|
|
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
|
|
2665
|
-
var
|
|
2811
|
+
var ERR_ACCOUNT_NOT_READY2 = -32002;
|
|
2812
|
+
var ERR_SPONSOR_FAILED2 = -32003;
|
|
2666
2813
|
var ERR_BUILD_FAILED = -32004;
|
|
2667
|
-
var
|
|
2814
|
+
var ERR_SEND_FAILED2 = -32005;
|
|
2668
2815
|
var ERR_EXECUTION_REVERTED = -32006;
|
|
2669
|
-
var
|
|
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
|
-
|
|
2829
|
+
outputError(err.code, err.message, err.data);
|
|
2683
2830
|
} else {
|
|
2684
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 ?
|
|
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
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2992
|
-
throw new TxError(
|
|
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
|
-
|
|
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
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
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(
|
|
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
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
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
|
-
|
|
3113
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
3206
|
-
throw new TxError(
|
|
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.
|
|
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(
|
|
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(
|
|
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:
|
|
3314
|
+
nonce: toHex6(op.nonce),
|
|
3236
3315
|
factory: op.factory,
|
|
3237
3316
|
factoryData: op.factoryData,
|
|
3238
3317
|
callData: op.callData,
|
|
3239
|
-
callGasLimit:
|
|
3240
|
-
verificationGasLimit:
|
|
3241
|
-
preVerificationGas:
|
|
3242
|
-
maxFeePerGas:
|
|
3243
|
-
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 ?
|
|
3246
|
-
paymasterPostOpGasLimit: op.paymasterPostOpGasLimit ?
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3395
|
-
|
|
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
|
|
3486
|
+
const info = await getTokenInfo(ctx.walletClient, tokenAddress);
|
|
3403
3487
|
return {
|
|
3404
3488
|
address: tokenAddress,
|
|
3405
|
-
symbol:
|
|
3406
|
-
decimals:
|
|
3407
|
-
balance: formatUnits(balance,
|
|
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
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
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:
|
|
3513
|
+
decimals: t.decimals,
|
|
3432
3514
|
balance: t.balance
|
|
3433
3515
|
})),
|
|
3434
|
-
|
|
3435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3658
|
+
outputError(err.code, err.message, err.data);
|
|
3620
3659
|
} else {
|
|
3621
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
3676
|
+
throw new SecurityError(ERR_ACCOUNT_NOT_READY3, "Account not found.");
|
|
3639
3677
|
}
|
|
3640
3678
|
if (!account.isDeployed) {
|
|
3641
|
-
throw new SecurityError(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
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
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
hookStatus.capabilities.preUserOpValidation
|
|
3815
|
-
hookStatus.capabilities.preIsValidSignature
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
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
|
-
|
|
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(
|
|
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(
|
|
3883
|
+
throw new SecurityError(ERR_INTERNAL3, "Invalid capability flags. Use 1, 2, or 3.");
|
|
3863
3884
|
}
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3976
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
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
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4120
|
+
outputResult({
|
|
4121
|
+
status: "no_profile",
|
|
4122
|
+
hint: "Bind an email first: `elytro security email bind <email>`."
|
|
4123
|
+
});
|
|
4092
4124
|
return;
|
|
4093
4125
|
}
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,181 @@ 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
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4217
|
+
outputResult({
|
|
4218
|
+
key,
|
|
4219
|
+
status: "removed"
|
|
4220
|
+
});
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
// src/commands/update.ts
|
|
4225
|
+
import { execSync } from "child_process";
|
|
4226
|
+
|
|
4227
|
+
// src/version.ts
|
|
4228
|
+
import { createRequire } from "module";
|
|
4229
|
+
function resolveVersion() {
|
|
4230
|
+
if (true) {
|
|
4231
|
+
return "0.5.0";
|
|
4232
|
+
}
|
|
4233
|
+
try {
|
|
4234
|
+
const require2 = createRequire(import.meta.url);
|
|
4235
|
+
const pkg = require2("../package.json");
|
|
4236
|
+
return pkg.version;
|
|
4237
|
+
} catch {
|
|
4238
|
+
return "0.0.0";
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
var VERSION = resolveVersion();
|
|
4242
|
+
|
|
4243
|
+
// src/commands/update.ts
|
|
4244
|
+
import ora6 from "ora";
|
|
4245
|
+
import chalk2 from "chalk";
|
|
4246
|
+
import { realpathSync } from "fs";
|
|
4247
|
+
import { fileURLToPath } from "url";
|
|
4248
|
+
var PACKAGE_NAME = "@elytro/cli";
|
|
4249
|
+
var NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
4250
|
+
async function fetchLatestVersion() {
|
|
4251
|
+
const res = await fetch(NPM_REGISTRY_URL);
|
|
4252
|
+
if (!res.ok) {
|
|
4253
|
+
throw new Error(`npm registry returned ${res.status}`);
|
|
4254
|
+
}
|
|
4255
|
+
const data = await res.json();
|
|
4256
|
+
return data.version;
|
|
4257
|
+
}
|
|
4258
|
+
function compareSemver(a, b) {
|
|
4259
|
+
const pa = a.split(".").map(Number);
|
|
4260
|
+
const pb = b.split(".").map(Number);
|
|
4261
|
+
for (let i = 0; i < 3; i++) {
|
|
4262
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return -1;
|
|
4263
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return 1;
|
|
4264
|
+
}
|
|
4265
|
+
return 0;
|
|
4266
|
+
}
|
|
4267
|
+
function detectPackageManager() {
|
|
4268
|
+
try {
|
|
4269
|
+
const scriptPath = realpathSync(fileURLToPath(import.meta.url));
|
|
4270
|
+
if (scriptPath.includes("/.bun/")) return "bun";
|
|
4271
|
+
if (scriptPath.includes("/pnpm/") || scriptPath.includes("/pnpm-global/"))
|
|
4272
|
+
return "pnpm";
|
|
4273
|
+
if (scriptPath.includes("/yarn/global/")) return "yarn";
|
|
4274
|
+
} catch {
|
|
4275
|
+
}
|
|
4276
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
4277
|
+
if (ua.startsWith("bun")) return "bun";
|
|
4278
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
4279
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
4280
|
+
return "npm";
|
|
4281
|
+
}
|
|
4282
|
+
function buildInstallCommand(pm, version) {
|
|
4283
|
+
const pkg = `${PACKAGE_NAME}@${version}`;
|
|
4284
|
+
switch (pm) {
|
|
4285
|
+
case "yarn":
|
|
4286
|
+
return `yarn global add ${pkg}`;
|
|
4287
|
+
case "pnpm":
|
|
4288
|
+
return `pnpm add -g ${pkg}`;
|
|
4289
|
+
case "bun":
|
|
4290
|
+
return `bun add -g ${pkg}`;
|
|
4291
|
+
default:
|
|
4292
|
+
return `npm install -g ${pkg}`;
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
function registerUpdateCommand(program2) {
|
|
4296
|
+
const updateCmd = program2.command("update").alias("upgrade").description("Check for updates and upgrade to the latest version");
|
|
4297
|
+
updateCmd.command("check").description("Check if a newer version is available (no install)").action(async () => {
|
|
4298
|
+
try {
|
|
4299
|
+
const latest = await fetchLatestVersion();
|
|
4300
|
+
const cmp = compareSemver(VERSION, latest);
|
|
4301
|
+
outputResult({
|
|
4302
|
+
currentVersion: VERSION,
|
|
4303
|
+
latestVersion: latest,
|
|
4304
|
+
updateAvailable: cmp < 0,
|
|
4305
|
+
...cmp < 0 ? {
|
|
4306
|
+
upgradeCommand: buildInstallCommand(
|
|
4307
|
+
detectPackageManager(),
|
|
4308
|
+
latest
|
|
4309
|
+
)
|
|
4310
|
+
} : {}
|
|
4311
|
+
});
|
|
4312
|
+
} catch (err) {
|
|
4313
|
+
outputError(
|
|
4314
|
+
-32e3,
|
|
4315
|
+
`Failed to check for updates: ${err.message}`
|
|
4316
|
+
);
|
|
4317
|
+
}
|
|
4318
|
+
});
|
|
4319
|
+
updateCmd.action(async () => {
|
|
4320
|
+
const spinner = ora6("Checking for updates\u2026").start();
|
|
4321
|
+
try {
|
|
4322
|
+
const latest = await fetchLatestVersion();
|
|
4323
|
+
const cmp = compareSemver(VERSION, latest);
|
|
4324
|
+
if (cmp >= 0) {
|
|
4325
|
+
spinner.succeed(chalk2.green(`Already up to date (v${VERSION})`));
|
|
4326
|
+
outputResult({
|
|
4327
|
+
currentVersion: VERSION,
|
|
4328
|
+
latestVersion: latest,
|
|
4329
|
+
updateAvailable: false
|
|
4330
|
+
});
|
|
4331
|
+
return;
|
|
4332
|
+
}
|
|
4333
|
+
spinner.text = `Updating ${chalk2.gray(`v${VERSION}`)} \u2192 ${chalk2.green(`v${latest}`)}\u2026`;
|
|
4334
|
+
const pm = detectPackageManager();
|
|
4335
|
+
const cmd = buildInstallCommand(pm, latest);
|
|
4336
|
+
execSync(cmd, { stdio: "pipe" });
|
|
4337
|
+
spinner.succeed(chalk2.green(`Updated to v${latest}`));
|
|
4338
|
+
outputResult({
|
|
4339
|
+
previousVersion: VERSION,
|
|
4340
|
+
currentVersion: latest,
|
|
4341
|
+
updateAvailable: false,
|
|
4342
|
+
packageManager: pm
|
|
4343
|
+
});
|
|
4344
|
+
} catch (err) {
|
|
4345
|
+
spinner.fail("Update failed");
|
|
4346
|
+
outputError(-32e3, `Update failed: ${err.message}`);
|
|
4347
|
+
}
|
|
4197
4348
|
});
|
|
4198
4349
|
}
|
|
4199
4350
|
|
|
4200
4351
|
// src/index.ts
|
|
4201
4352
|
var program = new Command();
|
|
4202
|
-
program.name("elytro").description("Elytro \u2014 ERC-4337 Smart Account Wallet CLI").version(
|
|
4353
|
+
program.name("elytro").description("Elytro \u2014 ERC-4337 Smart Account Wallet CLI").version(VERSION).addHelpText(
|
|
4354
|
+
"after",
|
|
4355
|
+
"\nLearn how to use Elytro skills: https://github.com/Elytro-eth/skills\n"
|
|
4356
|
+
);
|
|
4203
4357
|
async function main() {
|
|
4204
4358
|
let ctx = null;
|
|
4205
4359
|
try {
|
|
@@ -4210,10 +4364,10 @@ async function main() {
|
|
|
4210
4364
|
registerQueryCommand(program, ctx);
|
|
4211
4365
|
registerSecurityCommand(program, ctx);
|
|
4212
4366
|
registerConfigCommand(program, ctx);
|
|
4367
|
+
registerUpdateCommand(program);
|
|
4213
4368
|
await program.parseAsync(process.argv);
|
|
4214
4369
|
} catch (err) {
|
|
4215
|
-
|
|
4216
|
-
process.exitCode = 1;
|
|
4370
|
+
outputError(-32e3, sanitizeErrorMessage(err.message));
|
|
4217
4371
|
} finally {
|
|
4218
4372
|
ctx?.keyring.lock();
|
|
4219
4373
|
}
|