@elytro/cli 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +98 -37
  2. package/dist/index.js +508 -150
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,89 +1,150 @@
1
1
  # Elytro CLI
2
2
 
3
- A command-line interface for ERC-4337 smart account wallets. Built for power users and AI Agents managing smart accounts across multiple chains.
3
+ A command-line interface for ERC-4337 smart account wallets. Built for power users and AI agents managing smart accounts across multiple chains.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @elytro/cli
9
+ # or
10
+ bun add -g @elytro/cli
11
+ # or
12
+ pnpm add -g @elytro/cli
13
+ ```
4
14
 
5
15
  ## Quick Start
6
16
 
7
17
  ```bash
8
18
  # Initialize wallet (creates vault + EOA)
9
- bun dev init
19
+ elytro init
10
20
 
11
- # Create a smart account on Sepolia
12
- bun dev account create --chain 11155420 --email user@example.com --daily-limit 100
21
+ # Create a smart account on OP Sepolia
22
+ elytro account create --chain 11155420 --email user@example.com --daily-limit 100
13
23
 
14
24
  # Send a transaction
15
- bun dev tx send --tx "to:0xRecipient,value:0.1"
25
+ elytro tx send --tx "to:0xRecipient,value:0.1"
16
26
 
17
27
  # Check balance
18
- bun dev query balance
28
+ elytro query balance
19
29
  ```
20
30
 
21
31
  ## Key Features
22
32
 
23
33
  - **Multi-account management** — Create multiple smart accounts per chain with user-friendly aliases
24
- - **Zero-interaction security** — macOS: vault key stored in Keychain; non-macOS: injected via `ELYTRO_VAULT_SECRET`
34
+ - **Zero-interaction security** — macOS/Linux/Windows: vault key stored in system keychain via `@napi-rs/keyring`; fallback: inject via `ELYTRO_VAULT_SECRET`
25
35
  - **Flexible transaction building** — Single transfers, batch operations, contract calls via unified `--tx` syntax
26
36
  - **Transaction simulation** — Preview gas, paymaster sponsorship, and balance impact before sending
27
- - **Cross-chain support** — Manage accounts across Sepolia, OP Sepolia, Arbitrum, and custom networks
28
- - **Security intents** — Declare email/spending limits at account creation; deployed atomically on activation
37
+ - **Cross-chain support** — Manage accounts across Ethereum, Optimism, Arbitrum, Base, and testnets
38
+ - **SecurityHook (2FA)** — Install on-chain 2FA with email OTP and daily spending limits
39
+ - **Deferred OTP** — Commands that require OTP exit immediately after sending the code; complete later with `elytro otp submit <id> <code>`
40
+ - **Self-updating** — `elytro update` detects your package manager and upgrades in place
41
+
42
+ ## Supported Chains
43
+
44
+ | Chain | Chain ID |
45
+ | ---------------- | --------- |
46
+ | Ethereum | 1 |
47
+ | Optimism | 10 |
48
+ | Arbitrum One | 42161 |
49
+ | Base | 8453 |
50
+ | Sepolia | 11155111 |
51
+ | Optimism Sepolia | 11155420 |
52
+
53
+ Public RPC and bundler endpoints are used by default. Provide your own Alchemy/Pimlico keys for higher rate limits.
29
54
 
30
55
  ## Architecture
31
56
 
32
57
  | Component | Purpose |
33
58
  | ------------------ | ------------------------------------------------ |
34
- | **SecretProvider** | Vault key management (Keychain/env var) |
59
+ | **SecretProvider** | Vault key management (Keychain / env var) |
35
60
  | **KeyringService** | EOA encryption + decryption (AES-GCM) |
36
61
  | **AccountService** | Smart account lifecycle (CREATE2, multi-account) |
37
- | **SdkService** | @elytro/sdk wrapper (UserOp building) |
62
+ | **SdkService** | `@elytro/sdk` wrapper (UserOp building) |
38
63
  | **FileStore** | Persistent state (`~/.elytro/`) |
39
64
 
40
- See [docs/architecture.md](docs/architecture.md) for detailed data flow.
41
-
42
65
  ## Security Model
43
66
 
44
- - **No plaintext keys on disk** — vault key stored in macOS Keychain or injected at runtime
67
+ - **No plaintext keys on disk** — vault key stored in system keychain or injected at runtime
45
68
  - **AES-GCM encryption** — all private keys encrypted with vault key before storage
46
69
  - **Consume-once env var** — `ELYTRO_VAULT_SECRET` deleted from process after load
47
70
  - **Memory cleanup** — all key buffers zeroed after use
48
71
 
49
- See [docs/security.md](docs/security.md) for threat model.
50
-
51
72
  ## Configuration
52
73
 
53
- | Variable | Purpose | Required |
54
- | --------------------- | ----------------------------- | -------------- |
55
- | `ELYTRO_VAULT_SECRET` | Base64 vault key (non-macOS) | Yes, non-macOS |
56
- | `ELYTRO_ALCHEMY_KEY` | Alchemy RPC endpoint | For queries |
57
- | `ELYTRO_PIMLICO_KEY` | Bundler + paymaster | For tx send |
58
- | `ELYTRO_ENV` | `development` or `production` | Optional |
74
+ | Variable | Purpose | Required |
75
+ | --------------------- | ----------------------------------------- | -------------------- |
76
+ | `ELYTRO_VAULT_SECRET` | Base64 vault key (non-keychain platforms) | Yes, if no keychain |
77
+ | `ELYTRO_ALCHEMY_KEY` | Alchemy RPC endpoint | Optional (rate limit)|
78
+ | `ELYTRO_PIMLICO_KEY` | Bundler + paymaster | Optional (rate limit)|
79
+ | `ELYTRO_ENV` | `development` or `production` | Optional |
59
80
 
60
- Persist API keys: `bun dev config set alchemy-key <key>`
81
+ Persist API keys:
82
+ ```bash
83
+ elytro config set alchemy-key <key>
84
+ elytro config set pimlico-key <key>
85
+ ```
61
86
 
62
87
  ## Commands
63
88
 
64
89
  ```bash
65
90
  # Account Management
66
- bun dev account create --chain 11155420 [--alias name] [--email addr] [--daily-limit amount]
67
- bun dev account list [alias|address]
68
- bun dev account info [alias|address]
69
- bun dev account switch [alias|address]
70
- bun dev account activate [alias|address] # Deploy to chain
91
+ elytro account create --chain <chainId> [--alias name] [--email addr] [--daily-limit amount]
92
+ elytro account list [alias|address]
93
+ elytro account info [alias|address]
94
+ elytro account switch [alias|address]
95
+ elytro account activate [alias|address] # Deploy to chain
71
96
 
72
97
  # Transactions
73
- bun dev tx send --tx "to:0xAddr,value:0.1" [--tx ...]
74
- bun dev tx build --tx "to:0xAddr,data:0xab..."
75
- bun dev tx simulate --tx "to:0xAddr,value:0.1"
98
+ elytro tx send --tx "to:0xAddr,value:0.1" [--tx ...]
99
+ elytro tx build --tx "to:0xAddr,data:0xab..."
100
+ elytro tx simulate --tx "to:0xAddr,value:0.1"
76
101
 
77
102
  # Queries
78
- bun dev query balance [account] [--token erc20Addr]
79
- bun dev query tokens [account]
80
- bun dev query tx <hash>
81
- bun dev query chain
82
- bun dev query address <address>
103
+ elytro query balance [account] [--token erc20Addr]
104
+ elytro query tokens [account]
105
+ elytro query tx <hash>
106
+ elytro query chain
107
+ elytro query address <address>
108
+
109
+ # Security (2FA + spending limits)
110
+ elytro security status
111
+ elytro security 2fa install [--capability 1|2|3]
112
+ elytro security 2fa uninstall
113
+ elytro security 2fa uninstall --force # Start safety-delay countdown
114
+ elytro security 2fa uninstall --force --execute # Execute after delay
115
+ elytro security email bind <email>
116
+ elytro security email change <email>
117
+ elytro security spending-limit [amount] # View or set daily USD limit
118
+
119
+ # OTP (deferred verification)
120
+ elytro otp submit <id> <code> # Complete a pending OTP verification
121
+ elytro otp cancel [id] # Cancel pending OTP(s)
122
+ elytro otp list # List pending OTPs for current account
123
+
124
+ # Updates
125
+ elytro update # Check and upgrade to latest
126
+ elytro update check # Check without installing
127
+
128
+ # Config
129
+ elytro config set <key> <value>
130
+ elytro config get <key>
131
+ elytro config list
83
132
  ```
84
133
 
134
+ ## Deferred OTP Flow
135
+
136
+ When a command requires OTP verification (e.g. `security email bind`, `tx send` with 2FA), the CLI sends the OTP to your email and exits immediately instead of blocking for input. To complete the action:
137
+
138
+ 1. Check your email for the 6-digit code.
139
+ 2. Run `elytro otp submit <id> <code>`, where `<id>` is printed in the command output (e.g. `elytro otp submit abc123 654321`).
140
+
141
+ The `<id>` is returned in the JSON output as `otpPending.id` and in the stderr hint. Use `elytro otp list` to see all pending OTPs for the current account, or `elytro otp cancel [id]` to cancel.
142
+
85
143
  ## Development
86
144
 
87
145
  ```bash
88
-
146
+ bun install
147
+ bun dev <command> # Run from source
148
+ bun run build # Build to dist/
149
+ bun run test # Smoke tests
89
150
  ```
package/dist/index.js CHANGED
@@ -2018,9 +2018,10 @@ var SecurityHookService = class {
2018
2018
  }
2019
2019
  /**
2020
2020
  * Verify a security OTP challenge.
2021
+ * @param authSessionIdOverride - If provided, use this session instead of getAuthSession (for deferred OTP resume)
2021
2022
  */
2022
- async verifySecurityOtp(walletAddress, chainId, challengeId, otpCode) {
2023
- const sessionId = await this.getAuthSession(walletAddress, chainId);
2023
+ async verifySecurityOtp(walletAddress, chainId, challengeId, otpCode, authSessionIdOverride) {
2024
+ const sessionId = authSessionIdOverride ?? await this.getAuthSession(walletAddress, chainId);
2024
2025
  const result = await this.gqlMutate(GQL_VERIFY_SECURITY_OTP, {
2025
2026
  input: { authSessionId: sessionId, challengeId, otpCode }
2026
2027
  });
@@ -2036,14 +2037,17 @@ var SecurityHookService = class {
2036
2037
  *
2037
2038
  * Handles auth retry automatically.
2038
2039
  */
2039
- async getHookSignature(walletAddress, chainId, entryPoint, userOp) {
2040
+ /**
2041
+ * @param authSessionIdOverride - If provided, use this session (for deferred OTP resume with session-bound challenge)
2042
+ */
2043
+ async getHookSignature(walletAddress, chainId, entryPoint, userOp, authSessionIdOverride) {
2040
2044
  const op = this.formatUserOpForGraphQL(userOp);
2041
2045
  for (let attempt = 0; attempt <= 1; attempt++) {
2042
2046
  try {
2043
- if (attempt > 0) {
2047
+ if (attempt > 0 && !authSessionIdOverride) {
2044
2048
  await this.clearAuthSession(walletAddress, chainId);
2045
2049
  }
2046
- const sessionId = await this.getAuthSession(walletAddress, chainId);
2050
+ const sessionId = authSessionIdOverride ?? await this.getAuthSession(walletAddress, chainId);
2047
2051
  const result = await this.gqlRaw(GQL_AUTHORIZE_USER_OPERATION, {
2048
2052
  input: {
2049
2053
  authSessionId: sessionId,
@@ -2058,15 +2062,17 @@ var SecurityHookService = class {
2058
2062
  if (result.errors && result.errors.length > 0) {
2059
2063
  const ext = result.errors[0].extensions;
2060
2064
  if (ext) {
2065
+ const challengeDetails = typeof ext.challenge === "object" && ext.challenge !== null ? ext.challenge : void 0;
2066
+ const getChallengeValue = (key) => ext[key] ?? (challengeDetails ? challengeDetails[key] : void 0);
2061
2067
  return {
2062
2068
  error: {
2063
2069
  code: ext.code,
2064
- challengeId: ext.challengeId,
2065
- currentSpendUsdCents: ext.currentSpendUsdCents,
2066
- dailyLimitUsdCents: ext.dailyLimitUsdCents,
2067
- maskedEmail: ext.maskedEmail,
2068
- otpExpiresAt: ext.otpExpiresAt,
2069
- projectedSpendUsdCents: ext.projectedSpendUsdCents,
2070
+ challengeId: getChallengeValue("challengeId"),
2071
+ currentSpendUsdCents: ext.currentSpendUsdCents ?? getChallengeValue("currentSpendUsdCents"),
2072
+ dailyLimitUsdCents: ext.dailyLimitUsdCents ?? getChallengeValue("dailyLimitUsdCents"),
2073
+ maskedEmail: ext.maskedEmail ?? getChallengeValue("maskedEmail"),
2074
+ otpExpiresAt: ext.otpExpiresAt ?? getChallengeValue("otpExpiresAt"),
2075
+ projectedSpendUsdCents: ext.projectedSpendUsdCents ?? getChallengeValue("projectedSpendUsdCents"),
2070
2076
  message: result.errors[0].message
2071
2077
  }
2072
2078
  };
@@ -2078,15 +2084,17 @@ var SecurityHookService = class {
2078
2084
  if (err instanceof GraphQLClientError && err.errors?.length) {
2079
2085
  const ext = err.errors[0].extensions;
2080
2086
  if (ext?.code) {
2087
+ const challengeDetails = typeof ext.challenge === "object" && ext.challenge !== null ? ext.challenge : void 0;
2088
+ const getChallengeValue = (key) => ext[key] ?? (challengeDetails ? challengeDetails[key] : void 0);
2081
2089
  return {
2082
2090
  error: {
2083
2091
  code: ext.code,
2084
- challengeId: ext.challengeId,
2085
- currentSpendUsdCents: ext.currentSpendUsdCents,
2086
- dailyLimitUsdCents: ext.dailyLimitUsdCents,
2087
- maskedEmail: ext.maskedEmail,
2088
- otpExpiresAt: ext.otpExpiresAt,
2089
- projectedSpendUsdCents: ext.projectedSpendUsdCents,
2092
+ challengeId: getChallengeValue("challengeId"),
2093
+ currentSpendUsdCents: ext.currentSpendUsdCents ?? getChallengeValue("currentSpendUsdCents"),
2094
+ dailyLimitUsdCents: ext.dailyLimitUsdCents ?? getChallengeValue("dailyLimitUsdCents"),
2095
+ maskedEmail: ext.maskedEmail ?? getChallengeValue("maskedEmail"),
2096
+ otpExpiresAt: ext.otpExpiresAt ?? getChallengeValue("otpExpiresAt"),
2097
+ projectedSpendUsdCents: ext.projectedSpendUsdCents ?? getChallengeValue("projectedSpendUsdCents"),
2090
2098
  message: err.errors[0].message
2091
2099
  }
2092
2100
  };
@@ -2559,9 +2567,6 @@ async function askConfirm(message, defaultValue = false) {
2559
2567
  async function askSelect(message, choices) {
2560
2568
  return select({ message, choices });
2561
2569
  }
2562
- async function askInput(message, defaultValue) {
2563
- return input({ message, default: defaultValue });
2564
- }
2565
2570
 
2566
2571
  // src/commands/account.ts
2567
2572
  init_sponsor();
@@ -2975,7 +2980,92 @@ function createHookServiceForAccount(ctx, chainConfig) {
2975
2980
  // src/commands/tx.ts
2976
2981
  init_sponsor();
2977
2982
  import ora3 from "ora";
2978
- import { isAddress, isHex, formatEther as formatEther3, parseEther as parseEther2, toHex as toHex6 } from "viem";
2983
+ import { isAddress, isHex, formatEther as formatEther3, parseEther as parseEther2, toHex as toHex7 } from "viem";
2984
+
2985
+ // src/services/pendingOtp.ts
2986
+ import { randomBytes } from "crypto";
2987
+ var PENDING_OTPS_KEY = "pending-otps";
2988
+ function generateOtpId() {
2989
+ return randomBytes(4).toString("hex");
2990
+ }
2991
+ async function loadPendingOtps(store) {
2992
+ const data = await store.load(PENDING_OTPS_KEY);
2993
+ return data ?? {};
2994
+ }
2995
+ async function savePendingOtp(store, id, state) {
2996
+ const all = await loadPendingOtps(store);
2997
+ all[id] = state;
2998
+ await store.save(PENDING_OTPS_KEY, all);
2999
+ }
3000
+ async function removePendingOtp(store, id) {
3001
+ const all = await loadPendingOtps(store);
3002
+ delete all[id];
3003
+ await store.save(PENDING_OTPS_KEY, all);
3004
+ }
3005
+ async function clearPendingOtps(store, options) {
3006
+ const all = await loadPendingOtps(store);
3007
+ if (options?.id) {
3008
+ delete all[options.id];
3009
+ } else if (options?.account) {
3010
+ const accountLower = options.account.toLowerCase();
3011
+ for (const id of Object.keys(all)) {
3012
+ if (all[id].account.toLowerCase() === accountLower) {
3013
+ delete all[id];
3014
+ }
3015
+ }
3016
+ } else {
3017
+ for (const id of Object.keys(all)) {
3018
+ delete all[id];
3019
+ }
3020
+ }
3021
+ await store.save(PENDING_OTPS_KEY, all);
3022
+ }
3023
+ async function savePendingOtpAndOutput(store, state) {
3024
+ await savePendingOtp(store, state.id, state);
3025
+ const submitCommand = `elytro otp submit ${state.id} <6-digit-code>`;
3026
+ outputResult({
3027
+ status: "otp_pending",
3028
+ otpPending: {
3029
+ id: state.id,
3030
+ maskedEmail: state.maskedEmail,
3031
+ otpExpiresAt: state.otpExpiresAt,
3032
+ submitCommand
3033
+ }
3034
+ });
3035
+ console.error(
3036
+ `OTP sent to ${state.maskedEmail ?? "your email"}.${state.otpExpiresAt ? ` Expires at ${state.otpExpiresAt}.` : ""}
3037
+ To complete, run:
3038
+ ${submitCommand}`
3039
+ );
3040
+ }
3041
+ async function getPendingOtp(store, id) {
3042
+ const all = await loadPendingOtps(store);
3043
+ return all[id] ?? null;
3044
+ }
3045
+
3046
+ // src/utils/userOpSerialization.ts
3047
+ import { toHex as toHex6 } from "viem";
3048
+ function serializeUserOpForPending(op) {
3049
+ return {
3050
+ sender: op.sender,
3051
+ nonce: toHex6(op.nonce),
3052
+ factory: op.factory,
3053
+ factoryData: op.factoryData,
3054
+ callData: op.callData,
3055
+ callGasLimit: toHex6(op.callGasLimit),
3056
+ verificationGasLimit: toHex6(op.verificationGasLimit),
3057
+ preVerificationGas: toHex6(op.preVerificationGas),
3058
+ maxFeePerGas: toHex6(op.maxFeePerGas),
3059
+ maxPriorityFeePerGas: toHex6(op.maxPriorityFeePerGas),
3060
+ paymaster: op.paymaster,
3061
+ paymasterVerificationGasLimit: op.paymasterVerificationGasLimit ? toHex6(op.paymasterVerificationGasLimit) : null,
3062
+ paymasterPostOpGasLimit: op.paymasterPostOpGasLimit ? toHex6(op.paymasterPostOpGasLimit) : null,
3063
+ paymasterData: op.paymasterData,
3064
+ signature: op.signature
3065
+ };
3066
+ }
3067
+
3068
+ // src/commands/tx.ts
2979
3069
  var ERR_INVALID_PARAMS2 = -32602;
2980
3070
  var ERR_INSUFFICIENT_BALANCE = -32001;
2981
3071
  var ERR_ACCOUNT_NOT_READY2 = -32002;
@@ -3084,7 +3174,7 @@ function detectTxType(specs) {
3084
3174
  function specsToTxs(specs) {
3085
3175
  return specs.map((s) => ({
3086
3176
  to: s.to,
3087
- value: s.value ? toHex6(parseEther2(s.value)) : "0x0",
3177
+ value: s.value ? toHex7(parseEther2(s.value)) : "0x0",
3088
3178
  data: s.data ?? "0x"
3089
3179
  }));
3090
3180
  }
@@ -3227,33 +3317,52 @@ function registerTxCommand(program2, ctx) {
3227
3317
  spinner.stop();
3228
3318
  const errCode = hookResult.error.code;
3229
3319
  if (errCode === "OTP_REQUIRED" || errCode === "SPENDING_LIMIT_EXCEEDED") {
3230
- console.error(JSON.stringify({
3231
- challenge: errCode,
3232
- message: hookResult.error.message ?? `Verification required (${errCode}).`,
3233
- ...hookResult.error.maskedEmail ? { maskedEmail: hookResult.error.maskedEmail } : {},
3234
- ...errCode === "SPENDING_LIMIT_EXCEEDED" && hookResult.error.projectedSpendUsdCents !== void 0 ? { projectedSpendUsd: (hookResult.error.projectedSpendUsdCents / 100).toFixed(2), dailyLimitUsd: ((hookResult.error.dailyLimitUsdCents ?? 0) / 100).toFixed(2) } : {}
3235
- }, null, 2));
3236
- const otpCode = await askInput("Enter the 6-digit OTP code:");
3237
- spinner.start("Verifying OTP...");
3238
- await hookService.verifySecurityOtp(
3239
- accountInfo.address,
3240
- accountInfo.chainId,
3241
- hookResult.error.challengeId,
3242
- otpCode.trim()
3243
- );
3244
- spinner.text = "OTP verified. Retrying authorization...";
3245
- hookResult = await hookService.getHookSignature(
3246
- accountInfo.address,
3247
- accountInfo.chainId,
3248
- ctx.sdk.entryPoint,
3249
- userOp
3250
- );
3251
- if (hookResult.error) {
3320
+ if (!hookResult.error.challengeId) {
3321
+ const otpRequestSpinner = ora3("Requesting OTP challenge...").start();
3322
+ try {
3323
+ const otpChallenge = await hookService.requestSecurityOtp(
3324
+ accountInfo.address,
3325
+ accountInfo.chainId,
3326
+ ctx.sdk.entryPoint,
3327
+ userOp
3328
+ );
3329
+ hookResult.error.challengeId = otpChallenge.challengeId;
3330
+ hookResult.error.maskedEmail ??= otpChallenge.maskedEmail;
3331
+ hookResult.error.otpExpiresAt ??= otpChallenge.otpExpiresAt;
3332
+ otpRequestSpinner.stop();
3333
+ } catch (otpErr) {
3334
+ otpRequestSpinner.fail("Failed to request OTP challenge.");
3335
+ throw new TxError(
3336
+ ERR_SEND_FAILED2,
3337
+ `Unable to request OTP challenge: ${otpErr.message}`
3338
+ );
3339
+ }
3340
+ }
3341
+ if (!hookResult.error.challengeId) {
3252
3342
  throw new TxError(
3253
3343
  ERR_SEND_FAILED2,
3254
- `Hook authorization failed after OTP: ${hookResult.error.message}`
3344
+ "OTP challenge ID was not provided by Elytro API. Please try again."
3255
3345
  );
3256
3346
  }
3347
+ const challengeId = hookResult.error.challengeId;
3348
+ const authSessionId = await hookService.getAuthSession(accountInfo.address, accountInfo.chainId);
3349
+ await savePendingOtpAndOutput(ctx.store, {
3350
+ id: challengeId,
3351
+ account: accountInfo.address,
3352
+ chainId: accountInfo.chainId,
3353
+ action: "tx_send",
3354
+ challengeId,
3355
+ authSessionId,
3356
+ maskedEmail: hookResult.error.maskedEmail,
3357
+ otpExpiresAt: hookResult.error.otpExpiresAt,
3358
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3359
+ data: {
3360
+ userOp: serializeUserOpForPending(userOp),
3361
+ entryPoint: ctx.sdk.entryPoint,
3362
+ txSpec: opts?.tx
3363
+ }
3364
+ });
3365
+ return;
3257
3366
  } else {
3258
3367
  throw new TxError(
3259
3368
  ERR_SEND_FAILED2,
@@ -3481,18 +3590,18 @@ function resolveChainStrict(ctx, chainId) {
3481
3590
  function serializeUserOp(op) {
3482
3591
  return {
3483
3592
  sender: op.sender,
3484
- nonce: toHex6(op.nonce),
3593
+ nonce: toHex7(op.nonce),
3485
3594
  factory: op.factory,
3486
3595
  factoryData: op.factoryData,
3487
3596
  callData: op.callData,
3488
- callGasLimit: toHex6(op.callGasLimit),
3489
- verificationGasLimit: toHex6(op.verificationGasLimit),
3490
- preVerificationGas: toHex6(op.preVerificationGas),
3491
- maxFeePerGas: toHex6(op.maxFeePerGas),
3492
- maxPriorityFeePerGas: toHex6(op.maxPriorityFeePerGas),
3597
+ callGasLimit: toHex7(op.callGasLimit),
3598
+ verificationGasLimit: toHex7(op.verificationGasLimit),
3599
+ preVerificationGas: toHex7(op.preVerificationGas),
3600
+ maxFeePerGas: toHex7(op.maxFeePerGas),
3601
+ maxPriorityFeePerGas: toHex7(op.maxPriorityFeePerGas),
3493
3602
  paymaster: op.paymaster,
3494
- paymasterVerificationGasLimit: op.paymasterVerificationGasLimit ? toHex6(op.paymasterVerificationGasLimit) : null,
3495
- paymasterPostOpGasLimit: op.paymasterPostOpGasLimit ? toHex6(op.paymasterPostOpGasLimit) : null,
3603
+ paymasterVerificationGasLimit: op.paymasterVerificationGasLimit ? toHex7(op.paymasterVerificationGasLimit) : null,
3604
+ paymasterPostOpGasLimit: op.paymasterPostOpGasLimit ? toHex7(op.paymasterPostOpGasLimit) : null,
3496
3605
  paymasterData: op.paymasterData,
3497
3606
  signature: op.signature
3498
3607
  };
@@ -3811,7 +3920,6 @@ var ERR_ACCOUNT_NOT_READY3 = -32002;
3811
3920
  var ERR_HOOK_AUTH_FAILED = -32007;
3812
3921
  var ERR_EMAIL_NOT_BOUND = -32010;
3813
3922
  var ERR_SAFETY_DELAY = -32011;
3814
- var ERR_OTP_VERIFY_FAILED = -32012;
3815
3923
  var ERR_INTERNAL3 = -32e3;
3816
3924
  var SecurityError = class extends Error {
3817
3925
  code;
@@ -3823,7 +3931,16 @@ var SecurityError = class extends Error {
3823
3931
  this.data = data;
3824
3932
  }
3825
3933
  };
3934
+ var OtpDeferredError = class extends Error {
3935
+ constructor() {
3936
+ super("OTP deferred");
3937
+ this.name = "OtpDeferredError";
3938
+ }
3939
+ };
3826
3940
  function handleSecurityError(err) {
3941
+ if (err instanceof OtpDeferredError) {
3942
+ return;
3943
+ }
3827
3944
  if (err instanceof SecurityError) {
3828
3945
  outputError(err.code, err.message, err.data);
3829
3946
  } else {
@@ -3924,7 +4041,7 @@ async function signWithHookAndSend(ctx, chainConfig, account, hookService, userO
3924
4041
  let hookResult = await hookService.getHookSignature(account.address, account.chainId, ctx.sdk.entryPoint, userOp);
3925
4042
  if (hookResult.error) {
3926
4043
  spinner.stop();
3927
- hookResult = await handleOtpChallenge(hookService, account, ctx, userOp, hookResult);
4044
+ hookResult = await handleOtpChallenge(hookService, account, ctx, userOp, hookResult, "2fa_uninstall");
3928
4045
  }
3929
4046
  if (!spinner.isSpinning) spinner.start("Packing signature...");
3930
4047
  const hookAddress = SECURITY_HOOK_ADDRESS_MAP[account.chainId];
@@ -3945,42 +4062,48 @@ async function signWithHookAndSend(ctx, chainConfig, account, hookService, userO
3945
4062
  });
3946
4063
  }
3947
4064
  }
3948
- async function handleOtpChallenge(hookService, account, ctx, userOp, hookResult) {
4065
+ async function handleOtpChallenge(hookService, account, ctx, userOp, hookResult, action) {
3949
4066
  const err = hookResult.error;
3950
4067
  const errCode = err.code ?? "UNKNOWN";
3951
4068
  if (errCode !== "OTP_REQUIRED" && errCode !== "SPENDING_LIMIT_EXCEEDED") {
3952
4069
  throw new SecurityError(ERR_HOOK_AUTH_FAILED, `Hook authorization failed: ${err.message ?? errCode}`);
3953
4070
  }
3954
- console.error(JSON.stringify({
3955
- challenge: errCode,
3956
- message: err.message ?? `Verification required (${errCode}).`,
3957
- ...err.maskedEmail ? { maskedEmail: err.maskedEmail } : {},
3958
- ...errCode === "SPENDING_LIMIT_EXCEEDED" && err.projectedSpendUsdCents !== void 0 ? { projectedSpendUsd: (err.projectedSpendUsdCents / 100).toFixed(2), dailyLimitUsd: ((err.dailyLimitUsdCents ?? 0) / 100).toFixed(2) } : {}
3959
- }, null, 2));
3960
- const otpCode = await askInput("Enter the 6-digit OTP code:");
3961
- const verifySpinner = ora5("Verifying OTP...").start();
3962
- try {
3963
- await hookService.verifySecurityOtp(account.address, account.chainId, err.challengeId, otpCode.trim());
3964
- verifySpinner.text = "OTP verified. Retrying authorization...";
3965
- const retryResult = await hookService.getHookSignature(
3966
- account.address,
3967
- account.chainId,
3968
- ctx.sdk.entryPoint,
3969
- userOp
3970
- );
3971
- verifySpinner.stop();
3972
- if (retryResult.error) {
3973
- throw new SecurityError(ERR_HOOK_AUTH_FAILED, `Authorization failed after OTP: ${retryResult.error.message}`);
3974
- }
3975
- return retryResult;
3976
- } catch (e) {
3977
- verifySpinner.stop();
3978
- if (e instanceof SecurityError) throw e;
3979
- throw new SecurityError(
3980
- ERR_OTP_VERIFY_FAILED,
3981
- `OTP verification failed: ${sanitizeErrorMessage(e.message)}`
3982
- );
3983
- }
4071
+ let challengeId = err.challengeId;
4072
+ let maskedEmail = err.maskedEmail;
4073
+ let otpExpiresAt = err.otpExpiresAt;
4074
+ if (!challengeId) {
4075
+ try {
4076
+ const otpChallenge = await hookService.requestSecurityOtp(
4077
+ account.address,
4078
+ account.chainId,
4079
+ ctx.sdk.entryPoint,
4080
+ userOp
4081
+ );
4082
+ challengeId = otpChallenge.challengeId;
4083
+ maskedEmail ??= otpChallenge.maskedEmail;
4084
+ otpExpiresAt ??= otpChallenge.otpExpiresAt;
4085
+ } catch {
4086
+ challengeId = generateOtpId();
4087
+ }
4088
+ }
4089
+ const id = challengeId;
4090
+ const authSessionId = await hookService.getAuthSession(account.address, account.chainId);
4091
+ await savePendingOtpAndOutput(ctx.store, {
4092
+ id,
4093
+ account: account.address,
4094
+ chainId: account.chainId,
4095
+ action,
4096
+ challengeId,
4097
+ authSessionId,
4098
+ maskedEmail,
4099
+ otpExpiresAt,
4100
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4101
+ data: {
4102
+ userOp: serializeUserOpForPending(userOp),
4103
+ entryPoint: ctx.sdk.entryPoint
4104
+ }
4105
+ });
4106
+ throw new OtpDeferredError();
3984
4107
  }
3985
4108
  function registerSecurityCommand(program2, ctx) {
3986
4109
  const security = program2.command("security").description("SecurityHook (2FA & spending limits)");
@@ -4117,29 +4240,18 @@ function registerSecurityCommand(program2, ctx) {
4117
4240
  throw new SecurityError(ERR_HOOK_AUTH_FAILED, sanitizeErrorMessage(err.message));
4118
4241
  }
4119
4242
  spinner.stop();
4120
- console.error(JSON.stringify({ otpSentTo: bindingResult.maskedEmail, expiresAt: bindingResult.otpExpiresAt }, null, 2));
4121
- const otpCode = await askInput("Enter the 6-digit OTP code:");
4122
- const confirmSpinner = ora5("Confirming email binding...").start();
4123
- try {
4124
- const profile = await hookService.confirmEmailBinding(
4125
- account.address,
4126
- account.chainId,
4127
- bindingResult.bindingId,
4128
- otpCode.trim()
4129
- );
4130
- confirmSpinner.stop();
4131
- outputResult({
4132
- status: "email_bound",
4133
- email: profile.maskedEmail ?? profile.email ?? emailAddr,
4134
- emailVerified: profile.emailVerified
4135
- });
4136
- } catch (err) {
4137
- confirmSpinner.stop();
4138
- throw new SecurityError(
4139
- ERR_OTP_VERIFY_FAILED,
4140
- `OTP verification failed: ${sanitizeErrorMessage(err.message)}`
4141
- );
4142
- }
4243
+ const id = bindingResult.bindingId;
4244
+ await savePendingOtpAndOutput(ctx.store, {
4245
+ id,
4246
+ account: account.address,
4247
+ chainId: account.chainId,
4248
+ action: "email_bind",
4249
+ bindingId: bindingResult.bindingId,
4250
+ maskedEmail: bindingResult.maskedEmail,
4251
+ otpExpiresAt: bindingResult.otpExpiresAt,
4252
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4253
+ data: { email: emailAddr }
4254
+ });
4143
4255
  } catch (err) {
4144
4256
  handleSecurityError(err);
4145
4257
  }
@@ -4157,28 +4269,18 @@ function registerSecurityCommand(program2, ctx) {
4157
4269
  throw new SecurityError(ERR_HOOK_AUTH_FAILED, sanitizeErrorMessage(err.message));
4158
4270
  }
4159
4271
  spinner.stop();
4160
- console.error(JSON.stringify({ otpSentTo: bindingResult.maskedEmail }, null, 2));
4161
- const otpCode = await askInput("Enter the 6-digit OTP code:");
4162
- const confirmSpinner = ora5("Confirming email change...").start();
4163
- try {
4164
- const profile = await hookService.confirmEmailBinding(
4165
- account.address,
4166
- account.chainId,
4167
- bindingResult.bindingId,
4168
- otpCode.trim()
4169
- );
4170
- confirmSpinner.stop();
4171
- outputResult({
4172
- status: "email_changed",
4173
- email: profile.maskedEmail ?? profile.email ?? emailAddr
4174
- });
4175
- } catch (err) {
4176
- confirmSpinner.stop();
4177
- throw new SecurityError(
4178
- ERR_OTP_VERIFY_FAILED,
4179
- `OTP verification failed: ${sanitizeErrorMessage(err.message)}`
4180
- );
4181
- }
4272
+ const id = bindingResult.bindingId;
4273
+ await savePendingOtpAndOutput(ctx.store, {
4274
+ id,
4275
+ account: account.address,
4276
+ chainId: account.chainId,
4277
+ action: "email_change",
4278
+ bindingId: bindingResult.bindingId,
4279
+ maskedEmail: bindingResult.maskedEmail,
4280
+ otpExpiresAt: bindingResult.otpExpiresAt,
4281
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4282
+ data: { email: emailAddr }
4283
+ });
4182
4284
  } catch (err) {
4183
4285
  handleSecurityError(err);
4184
4286
  }
@@ -4190,7 +4292,7 @@ function registerSecurityCommand(program2, ctx) {
4190
4292
  if (!amountStr) {
4191
4293
  await showSpendingLimit(hookService, account);
4192
4294
  } else {
4193
- await setSpendingLimit(hookService, account, amountStr);
4295
+ await setSpendingLimit(ctx.store, hookService, account, amountStr);
4194
4296
  }
4195
4297
  } catch (err) {
4196
4298
  handleSecurityError(err);
@@ -4298,7 +4400,7 @@ async function showSpendingLimit(hookService, account) {
4298
4400
  email: profile.maskedEmail ?? null
4299
4401
  });
4300
4402
  }
4301
- async function setSpendingLimit(hookService, account, amountStr) {
4403
+ async function setSpendingLimit(store, hookService, account, amountStr) {
4302
4404
  const amountUsd = parseFloat(amountStr);
4303
4405
  if (isNaN(amountUsd) || amountUsd < 0) {
4304
4406
  throw new SecurityError(ERR_INTERNAL3, "Invalid amount. Provide a positive number in USD.");
@@ -4317,24 +4419,240 @@ async function setSpendingLimit(hookService, account, amountStr) {
4317
4419
  throw new SecurityError(ERR_HOOK_AUTH_FAILED, sanitizeErrorMessage(msg));
4318
4420
  }
4319
4421
  spinner.stop();
4320
- console.error(JSON.stringify({ otpSentTo: otpResult.maskedEmail }, null, 2));
4321
- const otpCode = await askInput("Enter the 6-digit OTP code:");
4322
- const setSpinner = ora5("Setting daily limit...").start();
4323
- try {
4324
- await hookService.setDailyLimit(account.address, account.chainId, dailyLimitUsdCents, otpCode.trim());
4325
- setSpinner.stop();
4422
+ const id = generateOtpId();
4423
+ await savePendingOtpAndOutput(store, {
4424
+ id,
4425
+ account: account.address,
4426
+ chainId: account.chainId,
4427
+ action: "spending_limit",
4428
+ maskedEmail: otpResult.maskedEmail,
4429
+ otpExpiresAt: otpResult.otpExpiresAt,
4430
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4431
+ data: { dailyLimitUsdCents }
4432
+ });
4433
+ }
4434
+
4435
+ // src/commands/otp.ts
4436
+ import ora6 from "ora";
4437
+ var ERR_OTP_NOT_FOUND = -32013;
4438
+ var ERR_OTP_EXPIRED = -32014;
4439
+ var ERR_ACCOUNT_NOT_READY4 = -32002;
4440
+ function isOtpExpired(otpExpiresAt) {
4441
+ if (!otpExpiresAt) return false;
4442
+ return new Date(otpExpiresAt).getTime() < Date.now();
4443
+ }
4444
+ function registerOtpCommand(program2, ctx) {
4445
+ const otp = program2.command("otp").description("Complete or manage deferred OTP verification");
4446
+ otp.command("submit").description("Complete a pending OTP verification").argument("<id>", "OTP id from the command that triggered OTP").argument("<code>", "6-digit OTP code").action(async (id, code) => {
4447
+ const pending = await getPendingOtp(ctx.store, id);
4448
+ if (!pending) {
4449
+ outputError(
4450
+ ERR_OTP_NOT_FOUND,
4451
+ "Unknown OTP id. It may have expired or been completed.",
4452
+ { id }
4453
+ );
4454
+ return;
4455
+ }
4456
+ if (isOtpExpired(pending.otpExpiresAt)) {
4457
+ await removePendingOtp(ctx.store, id);
4458
+ outputError(ERR_OTP_EXPIRED, "OTP has expired. Please re-run the original command.", {
4459
+ id,
4460
+ action: pending.action
4461
+ });
4462
+ return;
4463
+ }
4464
+ const current = ctx.account.currentAccount;
4465
+ if (!current || current.address.toLowerCase() !== pending.account.toLowerCase()) {
4466
+ outputError(ERR_ACCOUNT_NOT_READY4, `Switch to the account that initiated this OTP first: elytro account switch <alias>`, {
4467
+ pendingAccount: pending.account
4468
+ });
4469
+ return;
4470
+ }
4471
+ const accountInfo = ctx.account.resolveAccount(current.alias ?? current.address);
4472
+ if (!accountInfo) {
4473
+ outputError(ERR_ACCOUNT_NOT_READY4, "Account not found.");
4474
+ return;
4475
+ }
4476
+ const chainConfig = ctx.chain.chains.find((c) => c.id === pending.chainId);
4477
+ if (!chainConfig) {
4478
+ outputError(ERR_ACCOUNT_NOT_READY4, `Chain ${pending.chainId} not configured.`);
4479
+ return;
4480
+ }
4481
+ await ctx.sdk.initForChain(chainConfig);
4482
+ ctx.walletClient.initForChain(chainConfig);
4483
+ const hookService = new SecurityHookService({
4484
+ store: ctx.store,
4485
+ graphqlEndpoint: ctx.chain.graphqlEndpoint,
4486
+ signMessageForAuth: createSignMessageForAuth({
4487
+ signDigest: (digest) => ctx.keyring.signDigest(digest),
4488
+ packRawHash: (hash) => ctx.sdk.packRawHash(hash),
4489
+ packSignature: (rawSig, valData) => ctx.sdk.packUserOpSignature(rawSig, valData)
4490
+ }),
4491
+ readContract: async (params) => ctx.walletClient.readContract(params),
4492
+ getBlockTimestamp: async () => {
4493
+ const blockNum = await ctx.walletClient.raw.getBlockNumber();
4494
+ const block = await ctx.walletClient.raw.getBlock({ blockNumber: blockNum });
4495
+ return block.timestamp;
4496
+ }
4497
+ });
4498
+ const spinner = ora6("Completing OTP verification...").start();
4499
+ try {
4500
+ await completePendingOtp(ctx, hookService, accountInfo, pending, code.trim(), spinner);
4501
+ await removePendingOtp(ctx.store, id);
4502
+ } catch (err) {
4503
+ spinner.stop();
4504
+ outputError(-32e3, err.message);
4505
+ }
4506
+ });
4507
+ otp.command("cancel").description("Cancel pending OTP(s)").argument("[id]", "OTP id to cancel. Omit to cancel all for current account.").action(async (id) => {
4508
+ if (id) {
4509
+ const pending = await getPendingOtp(ctx.store, id);
4510
+ if (!pending) {
4511
+ outputError(ERR_OTP_NOT_FOUND, "Unknown OTP id.", { id });
4512
+ return;
4513
+ }
4514
+ await removePendingOtp(ctx.store, id);
4515
+ outputResult({ status: "cancelled", id });
4516
+ } else {
4517
+ const current = ctx.account.currentAccount;
4518
+ if (!current) {
4519
+ outputError(ERR_ACCOUNT_NOT_READY4, "No account selected.");
4520
+ return;
4521
+ }
4522
+ await clearPendingOtps(ctx.store, { account: current.address });
4523
+ outputResult({ status: "cancelled", scope: "account", account: current.address });
4524
+ }
4525
+ });
4526
+ otp.command("list").description("List pending OTPs for current account").action(async () => {
4527
+ const all = await loadPendingOtps(ctx.store);
4528
+ const current = ctx.account.currentAccount;
4529
+ const pendings = current ? Object.values(all).filter(
4530
+ (p) => p.account.toLowerCase() === current.address.toLowerCase()
4531
+ ) : Object.values(all);
4532
+ if (pendings.length === 0) {
4533
+ outputResult({ pendings: [], total: 0 });
4534
+ return;
4535
+ }
4326
4536
  outputResult({
4327
- status: "daily_limit_set",
4328
- dailyLimitUsd: amountUsd.toFixed(2)
4537
+ pendings: pendings.map((p) => ({
4538
+ id: p.id,
4539
+ action: p.action,
4540
+ maskedEmail: p.maskedEmail,
4541
+ otpExpiresAt: p.otpExpiresAt,
4542
+ submitCommand: `elytro otp submit ${p.id} <6-digit-code>`
4543
+ })),
4544
+ total: pendings.length
4329
4545
  });
4330
- } catch (err) {
4331
- setSpinner.stop();
4332
- throw new SecurityError(
4333
- ERR_OTP_VERIFY_FAILED,
4334
- `Failed to set limit: ${sanitizeErrorMessage(err.message)}`
4335
- );
4546
+ });
4547
+ }
4548
+ async function completePendingOtp(ctx, hookService, account, pending, code, spinner) {
4549
+ switch (pending.action) {
4550
+ case "email_bind":
4551
+ case "email_change": {
4552
+ spinner.stop();
4553
+ if (!pending.bindingId) throw new Error("Missing bindingId for email action.");
4554
+ const email = "email" in pending.data ? pending.data.email : "";
4555
+ const profile = await hookService.confirmEmailBinding(
4556
+ account.address,
4557
+ account.chainId,
4558
+ pending.bindingId,
4559
+ code
4560
+ );
4561
+ outputResult({
4562
+ status: pending.action === "email_bind" ? "email_bound" : "email_changed",
4563
+ email: profile.maskedEmail ?? profile.email ?? email,
4564
+ emailVerified: profile.emailVerified
4565
+ });
4566
+ break;
4567
+ }
4568
+ case "spending_limit": {
4569
+ spinner.stop();
4570
+ const dailyLimitUsdCents = pending.data.dailyLimitUsdCents;
4571
+ await hookService.setDailyLimit(
4572
+ account.address,
4573
+ account.chainId,
4574
+ dailyLimitUsdCents,
4575
+ code
4576
+ );
4577
+ outputResult({
4578
+ status: "daily_limit_set",
4579
+ dailyLimitUsd: (dailyLimitUsdCents / 100).toFixed(2)
4580
+ });
4581
+ break;
4582
+ }
4583
+ case "tx_send":
4584
+ case "2fa_uninstall": {
4585
+ if (!pending.challengeId) throw new Error("Missing challengeId for tx/2fa action.");
4586
+ spinner.text = "Verifying OTP...";
4587
+ const { userOp: userOpJson, entryPoint } = pending.data;
4588
+ const userOp = deserializeUserOp2(userOpJson);
4589
+ const authSessionId = pending.authSessionId;
4590
+ await hookService.verifySecurityOtp(
4591
+ account.address,
4592
+ account.chainId,
4593
+ pending.challengeId,
4594
+ code,
4595
+ authSessionId
4596
+ );
4597
+ spinner.text = "Retrying authorization...";
4598
+ const hookResult = await hookService.getHookSignature(
4599
+ account.address,
4600
+ account.chainId,
4601
+ entryPoint,
4602
+ userOp,
4603
+ authSessionId
4604
+ );
4605
+ if (hookResult.error) {
4606
+ throw new Error(`Authorization failed after OTP: ${hookResult.error.message}`);
4607
+ }
4608
+ const hookAddress = SECURITY_HOOK_ADDRESS_MAP[account.chainId];
4609
+ if (!hookAddress) throw new Error(`SecurityHook not deployed on chain ${account.chainId}.`);
4610
+ const { packedHash, validationData } = await ctx.sdk.getUserOpHash(userOp);
4611
+ const rawSignature = await ctx.keyring.signDigest(packedHash);
4612
+ userOp.signature = await ctx.sdk.packUserOpSignatureWithHook(
4613
+ rawSignature,
4614
+ validationData,
4615
+ hookAddress,
4616
+ hookResult.signature
4617
+ );
4618
+ spinner.text = "Sending UserOp...";
4619
+ const opHash = await ctx.sdk.sendUserOp(userOp);
4620
+ spinner.text = "Waiting for receipt...";
4621
+ const receipt = await ctx.sdk.waitForReceipt(opHash);
4622
+ if (!receipt.success) {
4623
+ throw new Error(`Transaction reverted: ${receipt.reason ?? "unknown"}`);
4624
+ }
4625
+ spinner.stop();
4626
+ outputResult({
4627
+ status: pending.action === "tx_send" ? "sent" : "uninstalled",
4628
+ userOpHash: opHash,
4629
+ transactionHash: receipt.transactionHash
4630
+ });
4631
+ break;
4632
+ }
4633
+ default:
4634
+ throw new Error(`Unknown action: ${pending.action}`);
4336
4635
  }
4337
4636
  }
4637
+ function deserializeUserOp2(raw) {
4638
+ return {
4639
+ sender: raw.sender,
4640
+ nonce: BigInt(raw.nonce ?? "0x0"),
4641
+ factory: raw.factory ?? null,
4642
+ factoryData: raw.factoryData ?? null,
4643
+ callData: raw.callData,
4644
+ callGasLimit: BigInt(raw.callGasLimit ?? "0x0"),
4645
+ verificationGasLimit: BigInt(raw.verificationGasLimit ?? "0x0"),
4646
+ preVerificationGas: BigInt(raw.preVerificationGas ?? "0x0"),
4647
+ maxFeePerGas: BigInt(raw.maxFeePerGas ?? "0x0"),
4648
+ maxPriorityFeePerGas: BigInt(raw.maxPriorityFeePerGas ?? "0x0"),
4649
+ paymaster: raw.paymaster ?? null,
4650
+ paymasterVerificationGasLimit: raw.paymasterVerificationGasLimit ? BigInt(raw.paymasterVerificationGasLimit) : null,
4651
+ paymasterPostOpGasLimit: raw.paymasterPostOpGasLimit ? BigInt(raw.paymasterPostOpGasLimit) : null,
4652
+ paymasterData: raw.paymasterData ?? null,
4653
+ signature: raw.signature ?? "0x"
4654
+ };
4655
+ }
4338
4656
 
4339
4657
  // src/commands/config.ts
4340
4658
  var KEY_MAP = {
@@ -4398,7 +4716,7 @@ import { execSync } from "child_process";
4398
4716
  import { createRequire } from "module";
4399
4717
  function resolveVersion() {
4400
4718
  if (true) {
4401
- return "0.5.2";
4719
+ return "0.6.0";
4402
4720
  }
4403
4721
  try {
4404
4722
  const require2 = createRequire(import.meta.url);
@@ -4411,7 +4729,7 @@ function resolveVersion() {
4411
4729
  var VERSION = resolveVersion();
4412
4730
 
4413
4731
  // src/commands/update.ts
4414
- import ora6 from "ora";
4732
+ import ora7 from "ora";
4415
4733
  import chalk2 from "chalk";
4416
4734
  import { realpathSync } from "fs";
4417
4735
  import { fileURLToPath } from "url";
@@ -4487,7 +4805,7 @@ function registerUpdateCommand(program2) {
4487
4805
  }
4488
4806
  });
4489
4807
  updateCmd.action(async () => {
4490
- const spinner = ora6("Checking for updates\u2026").start();
4808
+ const spinner = ora7("Checking for updates\u2026").start();
4491
4809
  try {
4492
4810
  const latest = await fetchLatestVersion();
4493
4811
  const cmp = compareSemver(VERSION, latest);
@@ -4518,6 +4836,41 @@ function registerUpdateCommand(program2) {
4518
4836
  });
4519
4837
  }
4520
4838
 
4839
+ // src/commands/prune.ts
4840
+ import { rm } from "fs/promises";
4841
+ import { join as join3 } from "path";
4842
+ import { homedir as homedir2 } from "os";
4843
+ var DATA_DIR = join3(homedir2(), ".elytro");
4844
+ async function runPrune() {
4845
+ try {
4846
+ const keyringProvider = new KeyringProvider();
4847
+ try {
4848
+ await keyringProvider.delete();
4849
+ } catch {
4850
+ }
4851
+ const fileProvider = new FileProvider(DATA_DIR);
4852
+ try {
4853
+ await fileProvider.delete();
4854
+ } catch {
4855
+ }
4856
+ try {
4857
+ await rm(DATA_DIR, { recursive: true, force: true });
4858
+ } catch (err) {
4859
+ const code = err.code;
4860
+ if (code !== "ENOENT") {
4861
+ throw err;
4862
+ }
4863
+ }
4864
+ outputResult({
4865
+ status: "pruned",
4866
+ dataDir: DATA_DIR,
4867
+ hint: "All local data cleared. Run `elytro init` to create a new wallet."
4868
+ });
4869
+ } catch (err) {
4870
+ outputError(-32e3, err.message);
4871
+ }
4872
+ }
4873
+
4521
4874
  // src/index.ts
4522
4875
  var program = new Command();
4523
4876
  program.name("elytro").description("Elytro \u2014 ERC-4337 Smart Account Wallet CLI").version(VERSION).addHelpText(
@@ -4525,6 +4878,10 @@ program.name("elytro").description("Elytro \u2014 ERC-4337 Smart Account Wallet
4525
4878
  "\nLearn how to use Elytro skills: https://github.com/Elytro-eth/skills\n"
4526
4879
  );
4527
4880
  async function main() {
4881
+ if (process.argv.includes("prune")) {
4882
+ await runPrune();
4883
+ return;
4884
+ }
4528
4885
  let ctx = null;
4529
4886
  try {
4530
4887
  ctx = await createAppContext();
@@ -4533,6 +4890,7 @@ async function main() {
4533
4890
  registerTxCommand(program, ctx);
4534
4891
  registerQueryCommand(program, ctx);
4535
4892
  registerSecurityCommand(program, ctx);
4893
+ registerOtpCommand(program, ctx);
4536
4894
  registerConfigCommand(program, ctx);
4537
4895
  registerUpdateCommand(program);
4538
4896
  await program.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elytro/cli",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Elytro ERC-4337 Smart Account Wallet CLI",
5
5
  "type": "module",
6
6
  "bin": {