@elytro/cli 0.5.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.js +470 -176
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ elytro query balance
|
|
|
36
36
|
- **Transaction simulation** — Preview gas, paymaster sponsorship, and balance impact before sending
|
|
37
37
|
- **Cross-chain support** — Manage accounts across Ethereum, Optimism, Arbitrum, Base, and testnets
|
|
38
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>`
|
|
39
40
|
- **Self-updating** — `elytro update` detects your package manager and upgrades in place
|
|
40
41
|
|
|
41
42
|
## Supported Chains
|
|
@@ -115,6 +116,11 @@ elytro security email bind <email>
|
|
|
115
116
|
elytro security email change <email>
|
|
116
117
|
elytro security spending-limit [amount] # View or set daily USD limit
|
|
117
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
|
+
|
|
118
124
|
# Updates
|
|
119
125
|
elytro update # Check and upgrade to latest
|
|
120
126
|
elytro update check # Check without installing
|
|
@@ -125,6 +131,15 @@ elytro config get <key>
|
|
|
125
131
|
elytro config list
|
|
126
132
|
```
|
|
127
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
|
+
|
|
128
143
|
## Development
|
|
129
144
|
|
|
130
145
|
```bash
|
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
|
-
|
|
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,
|
|
@@ -2556,16 +2560,10 @@ import ora2 from "ora";
|
|
|
2556
2560
|
import { formatEther as formatEther2, padHex as padHex2 } from "viem";
|
|
2557
2561
|
|
|
2558
2562
|
// src/utils/prompt.ts
|
|
2559
|
-
import { password,
|
|
2560
|
-
async function askConfirm(message, defaultValue = false) {
|
|
2561
|
-
return confirm({ message, default: defaultValue });
|
|
2562
|
-
}
|
|
2563
|
+
import { password, select, input } from "@inquirer/prompts";
|
|
2563
2564
|
async function askSelect(message, choices) {
|
|
2564
2565
|
return select({ message, choices });
|
|
2565
2566
|
}
|
|
2566
|
-
async function askInput(message, defaultValue) {
|
|
2567
|
-
return input({ message, default: defaultValue });
|
|
2568
|
-
}
|
|
2569
2567
|
|
|
2570
2568
|
// src/commands/account.ts
|
|
2571
2569
|
init_sponsor();
|
|
@@ -2979,7 +2977,92 @@ function createHookServiceForAccount(ctx, chainConfig) {
|
|
|
2979
2977
|
// src/commands/tx.ts
|
|
2980
2978
|
init_sponsor();
|
|
2981
2979
|
import ora3 from "ora";
|
|
2982
|
-
import { isAddress, isHex, formatEther as formatEther3, parseEther as parseEther2, toHex as
|
|
2980
|
+
import { isAddress, isHex, formatEther as formatEther3, parseEther as parseEther2, toHex as toHex7 } from "viem";
|
|
2981
|
+
|
|
2982
|
+
// src/services/pendingOtp.ts
|
|
2983
|
+
import { randomBytes } from "crypto";
|
|
2984
|
+
var PENDING_OTPS_KEY = "pending-otps";
|
|
2985
|
+
function generateOtpId() {
|
|
2986
|
+
return randomBytes(4).toString("hex");
|
|
2987
|
+
}
|
|
2988
|
+
async function loadPendingOtps(store) {
|
|
2989
|
+
const data = await store.load(PENDING_OTPS_KEY);
|
|
2990
|
+
return data ?? {};
|
|
2991
|
+
}
|
|
2992
|
+
async function savePendingOtp(store, id, state) {
|
|
2993
|
+
const all = await loadPendingOtps(store);
|
|
2994
|
+
all[id] = state;
|
|
2995
|
+
await store.save(PENDING_OTPS_KEY, all);
|
|
2996
|
+
}
|
|
2997
|
+
async function removePendingOtp(store, id) {
|
|
2998
|
+
const all = await loadPendingOtps(store);
|
|
2999
|
+
delete all[id];
|
|
3000
|
+
await store.save(PENDING_OTPS_KEY, all);
|
|
3001
|
+
}
|
|
3002
|
+
async function clearPendingOtps(store, options) {
|
|
3003
|
+
const all = await loadPendingOtps(store);
|
|
3004
|
+
if (options?.id) {
|
|
3005
|
+
delete all[options.id];
|
|
3006
|
+
} else if (options?.account) {
|
|
3007
|
+
const accountLower = options.account.toLowerCase();
|
|
3008
|
+
for (const id of Object.keys(all)) {
|
|
3009
|
+
if (all[id].account.toLowerCase() === accountLower) {
|
|
3010
|
+
delete all[id];
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
} else {
|
|
3014
|
+
for (const id of Object.keys(all)) {
|
|
3015
|
+
delete all[id];
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
await store.save(PENDING_OTPS_KEY, all);
|
|
3019
|
+
}
|
|
3020
|
+
async function savePendingOtpAndOutput(store, state) {
|
|
3021
|
+
await savePendingOtp(store, state.id, state);
|
|
3022
|
+
const submitCommand = `elytro otp submit ${state.id} <6-digit-code>`;
|
|
3023
|
+
outputResult({
|
|
3024
|
+
status: "otp_pending",
|
|
3025
|
+
otpPending: {
|
|
3026
|
+
id: state.id,
|
|
3027
|
+
maskedEmail: state.maskedEmail,
|
|
3028
|
+
otpExpiresAt: state.otpExpiresAt,
|
|
3029
|
+
submitCommand
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
console.error(
|
|
3033
|
+
`OTP sent to ${state.maskedEmail ?? "your email"}.${state.otpExpiresAt ? ` Expires at ${state.otpExpiresAt}.` : ""}
|
|
3034
|
+
To complete, run:
|
|
3035
|
+
${submitCommand}`
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
async function getPendingOtp(store, id) {
|
|
3039
|
+
const all = await loadPendingOtps(store);
|
|
3040
|
+
return all[id] ?? null;
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
// src/utils/userOpSerialization.ts
|
|
3044
|
+
import { toHex as toHex6 } from "viem";
|
|
3045
|
+
function serializeUserOpForPending(op) {
|
|
3046
|
+
return {
|
|
3047
|
+
sender: op.sender,
|
|
3048
|
+
nonce: toHex6(op.nonce),
|
|
3049
|
+
factory: op.factory,
|
|
3050
|
+
factoryData: op.factoryData,
|
|
3051
|
+
callData: op.callData,
|
|
3052
|
+
callGasLimit: toHex6(op.callGasLimit),
|
|
3053
|
+
verificationGasLimit: toHex6(op.verificationGasLimit),
|
|
3054
|
+
preVerificationGas: toHex6(op.preVerificationGas),
|
|
3055
|
+
maxFeePerGas: toHex6(op.maxFeePerGas),
|
|
3056
|
+
maxPriorityFeePerGas: toHex6(op.maxPriorityFeePerGas),
|
|
3057
|
+
paymaster: op.paymaster,
|
|
3058
|
+
paymasterVerificationGasLimit: op.paymasterVerificationGasLimit ? toHex6(op.paymasterVerificationGasLimit) : null,
|
|
3059
|
+
paymasterPostOpGasLimit: op.paymasterPostOpGasLimit ? toHex6(op.paymasterPostOpGasLimit) : null,
|
|
3060
|
+
paymasterData: op.paymasterData,
|
|
3061
|
+
signature: op.signature
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
// src/commands/tx.ts
|
|
2983
3066
|
var ERR_INVALID_PARAMS2 = -32602;
|
|
2984
3067
|
var ERR_INSUFFICIENT_BALANCE = -32001;
|
|
2985
3068
|
var ERR_ACCOUNT_NOT_READY2 = -32002;
|
|
@@ -3088,7 +3171,7 @@ function detectTxType(specs) {
|
|
|
3088
3171
|
function specsToTxs(specs) {
|
|
3089
3172
|
return specs.map((s) => ({
|
|
3090
3173
|
to: s.to,
|
|
3091
|
-
value: s.value ?
|
|
3174
|
+
value: s.value ? toHex7(parseEther2(s.value)) : "0x0",
|
|
3092
3175
|
data: s.data ?? "0x"
|
|
3093
3176
|
}));
|
|
3094
3177
|
}
|
|
@@ -3186,11 +3269,6 @@ function registerTxCommand(program2, ctx) {
|
|
|
3186
3269
|
estimatedGas: estimatedGas.toString()
|
|
3187
3270
|
}
|
|
3188
3271
|
}, null, 2));
|
|
3189
|
-
const confirmed = await askConfirm("Sign and send this transaction?");
|
|
3190
|
-
if (!confirmed) {
|
|
3191
|
-
outputResult({ status: "cancelled" });
|
|
3192
|
-
return;
|
|
3193
|
-
}
|
|
3194
3272
|
const spinner = ora3("Signing UserOperation...").start();
|
|
3195
3273
|
let opHash;
|
|
3196
3274
|
try {
|
|
@@ -3258,34 +3336,25 @@ function registerTxCommand(program2, ctx) {
|
|
|
3258
3336
|
"OTP challenge ID was not provided by Elytro API. Please try again."
|
|
3259
3337
|
);
|
|
3260
3338
|
}
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
ctx.sdk.entryPoint,
|
|
3281
|
-
userOp
|
|
3282
|
-
);
|
|
3283
|
-
if (hookResult.error) {
|
|
3284
|
-
throw new TxError(
|
|
3285
|
-
ERR_SEND_FAILED2,
|
|
3286
|
-
`Hook authorization failed after OTP: ${hookResult.error.message}`
|
|
3287
|
-
);
|
|
3288
|
-
}
|
|
3339
|
+
const challengeId = hookResult.error.challengeId;
|
|
3340
|
+
const authSessionId = await hookService.getAuthSession(accountInfo.address, accountInfo.chainId);
|
|
3341
|
+
await savePendingOtpAndOutput(ctx.store, {
|
|
3342
|
+
id: challengeId,
|
|
3343
|
+
account: accountInfo.address,
|
|
3344
|
+
chainId: accountInfo.chainId,
|
|
3345
|
+
action: "tx_send",
|
|
3346
|
+
challengeId,
|
|
3347
|
+
authSessionId,
|
|
3348
|
+
maskedEmail: hookResult.error.maskedEmail,
|
|
3349
|
+
otpExpiresAt: hookResult.error.otpExpiresAt,
|
|
3350
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3351
|
+
data: {
|
|
3352
|
+
userOp: serializeUserOpForPending(userOp),
|
|
3353
|
+
entryPoint: ctx.sdk.entryPoint,
|
|
3354
|
+
txSpec: opts?.tx
|
|
3355
|
+
}
|
|
3356
|
+
});
|
|
3357
|
+
return;
|
|
3289
3358
|
} else {
|
|
3290
3359
|
throw new TxError(
|
|
3291
3360
|
ERR_SEND_FAILED2,
|
|
@@ -3513,18 +3582,18 @@ function resolveChainStrict(ctx, chainId) {
|
|
|
3513
3582
|
function serializeUserOp(op) {
|
|
3514
3583
|
return {
|
|
3515
3584
|
sender: op.sender,
|
|
3516
|
-
nonce:
|
|
3585
|
+
nonce: toHex7(op.nonce),
|
|
3517
3586
|
factory: op.factory,
|
|
3518
3587
|
factoryData: op.factoryData,
|
|
3519
3588
|
callData: op.callData,
|
|
3520
|
-
callGasLimit:
|
|
3521
|
-
verificationGasLimit:
|
|
3522
|
-
preVerificationGas:
|
|
3523
|
-
maxFeePerGas:
|
|
3524
|
-
maxPriorityFeePerGas:
|
|
3589
|
+
callGasLimit: toHex7(op.callGasLimit),
|
|
3590
|
+
verificationGasLimit: toHex7(op.verificationGasLimit),
|
|
3591
|
+
preVerificationGas: toHex7(op.preVerificationGas),
|
|
3592
|
+
maxFeePerGas: toHex7(op.maxFeePerGas),
|
|
3593
|
+
maxPriorityFeePerGas: toHex7(op.maxPriorityFeePerGas),
|
|
3525
3594
|
paymaster: op.paymaster,
|
|
3526
|
-
paymasterVerificationGasLimit: op.paymasterVerificationGasLimit ?
|
|
3527
|
-
paymasterPostOpGasLimit: op.paymasterPostOpGasLimit ?
|
|
3595
|
+
paymasterVerificationGasLimit: op.paymasterVerificationGasLimit ? toHex7(op.paymasterVerificationGasLimit) : null,
|
|
3596
|
+
paymasterPostOpGasLimit: op.paymasterPostOpGasLimit ? toHex7(op.paymasterPostOpGasLimit) : null,
|
|
3528
3597
|
paymasterData: op.paymasterData,
|
|
3529
3598
|
signature: op.signature
|
|
3530
3599
|
};
|
|
@@ -3843,7 +3912,6 @@ var ERR_ACCOUNT_NOT_READY3 = -32002;
|
|
|
3843
3912
|
var ERR_HOOK_AUTH_FAILED = -32007;
|
|
3844
3913
|
var ERR_EMAIL_NOT_BOUND = -32010;
|
|
3845
3914
|
var ERR_SAFETY_DELAY = -32011;
|
|
3846
|
-
var ERR_OTP_VERIFY_FAILED = -32012;
|
|
3847
3915
|
var ERR_INTERNAL3 = -32e3;
|
|
3848
3916
|
var SecurityError = class extends Error {
|
|
3849
3917
|
code;
|
|
@@ -3855,7 +3923,16 @@ var SecurityError = class extends Error {
|
|
|
3855
3923
|
this.data = data;
|
|
3856
3924
|
}
|
|
3857
3925
|
};
|
|
3926
|
+
var OtpDeferredError = class extends Error {
|
|
3927
|
+
constructor() {
|
|
3928
|
+
super("OTP deferred");
|
|
3929
|
+
this.name = "OtpDeferredError";
|
|
3930
|
+
}
|
|
3931
|
+
};
|
|
3858
3932
|
function handleSecurityError(err) {
|
|
3933
|
+
if (err instanceof OtpDeferredError) {
|
|
3934
|
+
return;
|
|
3935
|
+
}
|
|
3859
3936
|
if (err instanceof SecurityError) {
|
|
3860
3937
|
outputError(err.code, err.message, err.data);
|
|
3861
3938
|
} else {
|
|
@@ -3956,7 +4033,7 @@ async function signWithHookAndSend(ctx, chainConfig, account, hookService, userO
|
|
|
3956
4033
|
let hookResult = await hookService.getHookSignature(account.address, account.chainId, ctx.sdk.entryPoint, userOp);
|
|
3957
4034
|
if (hookResult.error) {
|
|
3958
4035
|
spinner.stop();
|
|
3959
|
-
hookResult = await handleOtpChallenge(hookService, account, ctx, userOp, hookResult);
|
|
4036
|
+
hookResult = await handleOtpChallenge(hookService, account, ctx, userOp, hookResult, "2fa_uninstall");
|
|
3960
4037
|
}
|
|
3961
4038
|
if (!spinner.isSpinning) spinner.start("Packing signature...");
|
|
3962
4039
|
const hookAddress = SECURITY_HOOK_ADDRESS_MAP[account.chainId];
|
|
@@ -3977,42 +4054,48 @@ async function signWithHookAndSend(ctx, chainConfig, account, hookService, userO
|
|
|
3977
4054
|
});
|
|
3978
4055
|
}
|
|
3979
4056
|
}
|
|
3980
|
-
async function handleOtpChallenge(hookService, account, ctx, userOp, hookResult) {
|
|
4057
|
+
async function handleOtpChallenge(hookService, account, ctx, userOp, hookResult, action) {
|
|
3981
4058
|
const err = hookResult.error;
|
|
3982
4059
|
const errCode = err.code ?? "UNKNOWN";
|
|
3983
4060
|
if (errCode !== "OTP_REQUIRED" && errCode !== "SPENDING_LIMIT_EXCEEDED") {
|
|
3984
4061
|
throw new SecurityError(ERR_HOOK_AUTH_FAILED, `Hook authorization failed: ${err.message ?? errCode}`);
|
|
3985
4062
|
}
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4063
|
+
let challengeId = err.challengeId;
|
|
4064
|
+
let maskedEmail = err.maskedEmail;
|
|
4065
|
+
let otpExpiresAt = err.otpExpiresAt;
|
|
4066
|
+
if (!challengeId) {
|
|
4067
|
+
try {
|
|
4068
|
+
const otpChallenge = await hookService.requestSecurityOtp(
|
|
4069
|
+
account.address,
|
|
4070
|
+
account.chainId,
|
|
4071
|
+
ctx.sdk.entryPoint,
|
|
4072
|
+
userOp
|
|
4073
|
+
);
|
|
4074
|
+
challengeId = otpChallenge.challengeId;
|
|
4075
|
+
maskedEmail ??= otpChallenge.maskedEmail;
|
|
4076
|
+
otpExpiresAt ??= otpChallenge.otpExpiresAt;
|
|
4077
|
+
} catch {
|
|
4078
|
+
challengeId = generateOtpId();
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
const id = challengeId;
|
|
4082
|
+
const authSessionId = await hookService.getAuthSession(account.address, account.chainId);
|
|
4083
|
+
await savePendingOtpAndOutput(ctx.store, {
|
|
4084
|
+
id,
|
|
4085
|
+
account: account.address,
|
|
4086
|
+
chainId: account.chainId,
|
|
4087
|
+
action,
|
|
4088
|
+
challengeId,
|
|
4089
|
+
authSessionId,
|
|
4090
|
+
maskedEmail,
|
|
4091
|
+
otpExpiresAt,
|
|
4092
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4093
|
+
data: {
|
|
4094
|
+
userOp: serializeUserOpForPending(userOp),
|
|
4095
|
+
entryPoint: ctx.sdk.entryPoint
|
|
4096
|
+
}
|
|
4097
|
+
});
|
|
4098
|
+
throw new OtpDeferredError();
|
|
4016
4099
|
}
|
|
4017
4100
|
function registerSecurityCommand(program2, ctx) {
|
|
4018
4101
|
const security = program2.command("security").description("SecurityHook (2FA & spending limits)");
|
|
@@ -4084,13 +4167,6 @@ function registerSecurityCommand(program2, ctx) {
|
|
|
4084
4167
|
if (![1, 2, 3].includes(capabilityFlags)) {
|
|
4085
4168
|
throw new SecurityError(ERR_INTERNAL3, "Invalid capability flags. Use 1, 2, or 3.");
|
|
4086
4169
|
}
|
|
4087
|
-
const confirmed = await askConfirm(
|
|
4088
|
-
`Install SecurityHook on ${account.alias} (${address(account.address)})? Capability: ${CAPABILITY_LABELS[capabilityFlags]}, Safety Delay: ${DEFAULT_SAFETY_DELAY}s`
|
|
4089
|
-
);
|
|
4090
|
-
if (!confirmed) {
|
|
4091
|
-
outputResult({ status: "cancelled" });
|
|
4092
|
-
return;
|
|
4093
|
-
}
|
|
4094
4170
|
const installTx = encodeInstallHook(account.address, hookAddress, DEFAULT_SAFETY_DELAY, capabilityFlags);
|
|
4095
4171
|
const buildSpinner = ora5("Building UserOp...").start();
|
|
4096
4172
|
try {
|
|
@@ -4149,29 +4225,18 @@ function registerSecurityCommand(program2, ctx) {
|
|
|
4149
4225
|
throw new SecurityError(ERR_HOOK_AUTH_FAILED, sanitizeErrorMessage(err.message));
|
|
4150
4226
|
}
|
|
4151
4227
|
spinner.stop();
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
)
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
status: "email_bound",
|
|
4165
|
-
email: profile.maskedEmail ?? profile.email ?? emailAddr,
|
|
4166
|
-
emailVerified: profile.emailVerified
|
|
4167
|
-
});
|
|
4168
|
-
} catch (err) {
|
|
4169
|
-
confirmSpinner.stop();
|
|
4170
|
-
throw new SecurityError(
|
|
4171
|
-
ERR_OTP_VERIFY_FAILED,
|
|
4172
|
-
`OTP verification failed: ${sanitizeErrorMessage(err.message)}`
|
|
4173
|
-
);
|
|
4174
|
-
}
|
|
4228
|
+
const id = bindingResult.bindingId;
|
|
4229
|
+
await savePendingOtpAndOutput(ctx.store, {
|
|
4230
|
+
id,
|
|
4231
|
+
account: account.address,
|
|
4232
|
+
chainId: account.chainId,
|
|
4233
|
+
action: "email_bind",
|
|
4234
|
+
bindingId: bindingResult.bindingId,
|
|
4235
|
+
maskedEmail: bindingResult.maskedEmail,
|
|
4236
|
+
otpExpiresAt: bindingResult.otpExpiresAt,
|
|
4237
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4238
|
+
data: { email: emailAddr }
|
|
4239
|
+
});
|
|
4175
4240
|
} catch (err) {
|
|
4176
4241
|
handleSecurityError(err);
|
|
4177
4242
|
}
|
|
@@ -4189,28 +4254,18 @@ function registerSecurityCommand(program2, ctx) {
|
|
|
4189
4254
|
throw new SecurityError(ERR_HOOK_AUTH_FAILED, sanitizeErrorMessage(err.message));
|
|
4190
4255
|
}
|
|
4191
4256
|
spinner.stop();
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
)
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
status: "email_changed",
|
|
4205
|
-
email: profile.maskedEmail ?? profile.email ?? emailAddr
|
|
4206
|
-
});
|
|
4207
|
-
} catch (err) {
|
|
4208
|
-
confirmSpinner.stop();
|
|
4209
|
-
throw new SecurityError(
|
|
4210
|
-
ERR_OTP_VERIFY_FAILED,
|
|
4211
|
-
`OTP verification failed: ${sanitizeErrorMessage(err.message)}`
|
|
4212
|
-
);
|
|
4213
|
-
}
|
|
4257
|
+
const id = bindingResult.bindingId;
|
|
4258
|
+
await savePendingOtpAndOutput(ctx.store, {
|
|
4259
|
+
id,
|
|
4260
|
+
account: account.address,
|
|
4261
|
+
chainId: account.chainId,
|
|
4262
|
+
action: "email_change",
|
|
4263
|
+
bindingId: bindingResult.bindingId,
|
|
4264
|
+
maskedEmail: bindingResult.maskedEmail,
|
|
4265
|
+
otpExpiresAt: bindingResult.otpExpiresAt,
|
|
4266
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4267
|
+
data: { email: emailAddr }
|
|
4268
|
+
});
|
|
4214
4269
|
} catch (err) {
|
|
4215
4270
|
handleSecurityError(err);
|
|
4216
4271
|
}
|
|
@@ -4222,7 +4277,7 @@ function registerSecurityCommand(program2, ctx) {
|
|
|
4222
4277
|
if (!amountStr) {
|
|
4223
4278
|
await showSpendingLimit(hookService, account);
|
|
4224
4279
|
} else {
|
|
4225
|
-
await setSpendingLimit(hookService, account, amountStr);
|
|
4280
|
+
await setSpendingLimit(ctx.store, hookService, account, amountStr);
|
|
4226
4281
|
}
|
|
4227
4282
|
} catch (err) {
|
|
4228
4283
|
handleSecurityError(err);
|
|
@@ -4242,11 +4297,6 @@ async function handleForceExecute(ctx, chainConfig, account, currentStatus) {
|
|
|
4242
4297
|
`Safety delay not elapsed. Available after ${currentStatus.forceUninstall.availableAfter}.`
|
|
4243
4298
|
);
|
|
4244
4299
|
}
|
|
4245
|
-
const confirmed = await askConfirm(`Execute force uninstall on ${account.alias} (${address(account.address)})? This will remove the SecurityHook.`);
|
|
4246
|
-
if (!confirmed) {
|
|
4247
|
-
outputResult({ status: "cancelled" });
|
|
4248
|
-
return;
|
|
4249
|
-
}
|
|
4250
4300
|
const uninstallTx = encodeUninstallHook(account.address, currentStatus.hookAddress);
|
|
4251
4301
|
const spinner = ora5("Executing force uninstall...").start();
|
|
4252
4302
|
try {
|
|
@@ -4268,13 +4318,6 @@ async function handleForceStart(ctx, chainConfig, account, currentStatus, hookAd
|
|
|
4268
4318
|
});
|
|
4269
4319
|
return;
|
|
4270
4320
|
}
|
|
4271
|
-
const confirmed = await askConfirm(
|
|
4272
|
-
`Start force-uninstall countdown on ${account.alias} (${address(account.address)})? You must wait ${DEFAULT_SAFETY_DELAY}s before executing.`
|
|
4273
|
-
);
|
|
4274
|
-
if (!confirmed) {
|
|
4275
|
-
outputResult({ status: "cancelled" });
|
|
4276
|
-
return;
|
|
4277
|
-
}
|
|
4278
4321
|
const preUninstallTx = encodeForcePreUninstall(hookAddress);
|
|
4279
4322
|
const spinner = ora5("Starting force-uninstall countdown...").start();
|
|
4280
4323
|
try {
|
|
@@ -4292,11 +4335,6 @@ async function handleForceStart(ctx, chainConfig, account, currentStatus, hookAd
|
|
|
4292
4335
|
}
|
|
4293
4336
|
}
|
|
4294
4337
|
async function handleNormalUninstall(ctx, chainConfig, account, hookService, hookAddress) {
|
|
4295
|
-
const confirmed = await askConfirm(`Uninstall SecurityHook from ${account.alias} (${address(account.address)})? (requires 2FA approval)`);
|
|
4296
|
-
if (!confirmed) {
|
|
4297
|
-
outputResult({ status: "cancelled" });
|
|
4298
|
-
return;
|
|
4299
|
-
}
|
|
4300
4338
|
const uninstallTx = encodeUninstallHook(account.address, hookAddress);
|
|
4301
4339
|
const spinner = ora5("Building UserOp...").start();
|
|
4302
4340
|
try {
|
|
@@ -4330,7 +4368,7 @@ async function showSpendingLimit(hookService, account) {
|
|
|
4330
4368
|
email: profile.maskedEmail ?? null
|
|
4331
4369
|
});
|
|
4332
4370
|
}
|
|
4333
|
-
async function setSpendingLimit(hookService, account, amountStr) {
|
|
4371
|
+
async function setSpendingLimit(store, hookService, account, amountStr) {
|
|
4334
4372
|
const amountUsd = parseFloat(amountStr);
|
|
4335
4373
|
if (isNaN(amountUsd) || amountUsd < 0) {
|
|
4336
4374
|
throw new SecurityError(ERR_INTERNAL3, "Invalid amount. Provide a positive number in USD.");
|
|
@@ -4349,24 +4387,240 @@ async function setSpendingLimit(hookService, account, amountStr) {
|
|
|
4349
4387
|
throw new SecurityError(ERR_HOOK_AUTH_FAILED, sanitizeErrorMessage(msg));
|
|
4350
4388
|
}
|
|
4351
4389
|
spinner.stop();
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4390
|
+
const id = generateOtpId();
|
|
4391
|
+
await savePendingOtpAndOutput(store, {
|
|
4392
|
+
id,
|
|
4393
|
+
account: account.address,
|
|
4394
|
+
chainId: account.chainId,
|
|
4395
|
+
action: "spending_limit",
|
|
4396
|
+
maskedEmail: otpResult.maskedEmail,
|
|
4397
|
+
otpExpiresAt: otpResult.otpExpiresAt,
|
|
4398
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4399
|
+
data: { dailyLimitUsdCents }
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
// src/commands/otp.ts
|
|
4404
|
+
import ora6 from "ora";
|
|
4405
|
+
var ERR_OTP_NOT_FOUND = -32013;
|
|
4406
|
+
var ERR_OTP_EXPIRED = -32014;
|
|
4407
|
+
var ERR_ACCOUNT_NOT_READY4 = -32002;
|
|
4408
|
+
function isOtpExpired(otpExpiresAt) {
|
|
4409
|
+
if (!otpExpiresAt) return false;
|
|
4410
|
+
return new Date(otpExpiresAt).getTime() < Date.now();
|
|
4411
|
+
}
|
|
4412
|
+
function registerOtpCommand(program2, ctx) {
|
|
4413
|
+
const otp = program2.command("otp").description("Complete or manage deferred OTP verification");
|
|
4414
|
+
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) => {
|
|
4415
|
+
const pending = await getPendingOtp(ctx.store, id);
|
|
4416
|
+
if (!pending) {
|
|
4417
|
+
outputError(
|
|
4418
|
+
ERR_OTP_NOT_FOUND,
|
|
4419
|
+
"Unknown OTP id. It may have expired or been completed.",
|
|
4420
|
+
{ id }
|
|
4421
|
+
);
|
|
4422
|
+
return;
|
|
4423
|
+
}
|
|
4424
|
+
if (isOtpExpired(pending.otpExpiresAt)) {
|
|
4425
|
+
await removePendingOtp(ctx.store, id);
|
|
4426
|
+
outputError(ERR_OTP_EXPIRED, "OTP has expired. Please re-run the original command.", {
|
|
4427
|
+
id,
|
|
4428
|
+
action: pending.action
|
|
4429
|
+
});
|
|
4430
|
+
return;
|
|
4431
|
+
}
|
|
4432
|
+
const current = ctx.account.currentAccount;
|
|
4433
|
+
if (!current || current.address.toLowerCase() !== pending.account.toLowerCase()) {
|
|
4434
|
+
outputError(ERR_ACCOUNT_NOT_READY4, `Switch to the account that initiated this OTP first: elytro account switch <alias>`, {
|
|
4435
|
+
pendingAccount: pending.account
|
|
4436
|
+
});
|
|
4437
|
+
return;
|
|
4438
|
+
}
|
|
4439
|
+
const accountInfo = ctx.account.resolveAccount(current.alias ?? current.address);
|
|
4440
|
+
if (!accountInfo) {
|
|
4441
|
+
outputError(ERR_ACCOUNT_NOT_READY4, "Account not found.");
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
const chainConfig = ctx.chain.chains.find((c) => c.id === pending.chainId);
|
|
4445
|
+
if (!chainConfig) {
|
|
4446
|
+
outputError(ERR_ACCOUNT_NOT_READY4, `Chain ${pending.chainId} not configured.`);
|
|
4447
|
+
return;
|
|
4448
|
+
}
|
|
4449
|
+
await ctx.sdk.initForChain(chainConfig);
|
|
4450
|
+
ctx.walletClient.initForChain(chainConfig);
|
|
4451
|
+
const hookService = new SecurityHookService({
|
|
4452
|
+
store: ctx.store,
|
|
4453
|
+
graphqlEndpoint: ctx.chain.graphqlEndpoint,
|
|
4454
|
+
signMessageForAuth: createSignMessageForAuth({
|
|
4455
|
+
signDigest: (digest) => ctx.keyring.signDigest(digest),
|
|
4456
|
+
packRawHash: (hash) => ctx.sdk.packRawHash(hash),
|
|
4457
|
+
packSignature: (rawSig, valData) => ctx.sdk.packUserOpSignature(rawSig, valData)
|
|
4458
|
+
}),
|
|
4459
|
+
readContract: async (params) => ctx.walletClient.readContract(params),
|
|
4460
|
+
getBlockTimestamp: async () => {
|
|
4461
|
+
const blockNum = await ctx.walletClient.raw.getBlockNumber();
|
|
4462
|
+
const block = await ctx.walletClient.raw.getBlock({ blockNumber: blockNum });
|
|
4463
|
+
return block.timestamp;
|
|
4464
|
+
}
|
|
4465
|
+
});
|
|
4466
|
+
const spinner = ora6("Completing OTP verification...").start();
|
|
4467
|
+
try {
|
|
4468
|
+
await completePendingOtp(ctx, hookService, accountInfo, pending, code.trim(), spinner);
|
|
4469
|
+
await removePendingOtp(ctx.store, id);
|
|
4470
|
+
} catch (err) {
|
|
4471
|
+
spinner.stop();
|
|
4472
|
+
outputError(-32e3, err.message);
|
|
4473
|
+
}
|
|
4474
|
+
});
|
|
4475
|
+
otp.command("cancel").description("Cancel pending OTP(s)").argument("[id]", "OTP id to cancel. Omit to cancel all for current account.").action(async (id) => {
|
|
4476
|
+
if (id) {
|
|
4477
|
+
const pending = await getPendingOtp(ctx.store, id);
|
|
4478
|
+
if (!pending) {
|
|
4479
|
+
outputError(ERR_OTP_NOT_FOUND, "Unknown OTP id.", { id });
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
await removePendingOtp(ctx.store, id);
|
|
4483
|
+
outputResult({ status: "cancelled", id });
|
|
4484
|
+
} else {
|
|
4485
|
+
const current = ctx.account.currentAccount;
|
|
4486
|
+
if (!current) {
|
|
4487
|
+
outputError(ERR_ACCOUNT_NOT_READY4, "No account selected.");
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4490
|
+
await clearPendingOtps(ctx.store, { account: current.address });
|
|
4491
|
+
outputResult({ status: "cancelled", scope: "account", account: current.address });
|
|
4492
|
+
}
|
|
4493
|
+
});
|
|
4494
|
+
otp.command("list").description("List pending OTPs for current account").action(async () => {
|
|
4495
|
+
const all = await loadPendingOtps(ctx.store);
|
|
4496
|
+
const current = ctx.account.currentAccount;
|
|
4497
|
+
const pendings = current ? Object.values(all).filter(
|
|
4498
|
+
(p) => p.account.toLowerCase() === current.address.toLowerCase()
|
|
4499
|
+
) : Object.values(all);
|
|
4500
|
+
if (pendings.length === 0) {
|
|
4501
|
+
outputResult({ pendings: [], total: 0 });
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4358
4504
|
outputResult({
|
|
4359
|
-
|
|
4360
|
-
|
|
4505
|
+
pendings: pendings.map((p) => ({
|
|
4506
|
+
id: p.id,
|
|
4507
|
+
action: p.action,
|
|
4508
|
+
maskedEmail: p.maskedEmail,
|
|
4509
|
+
otpExpiresAt: p.otpExpiresAt,
|
|
4510
|
+
submitCommand: `elytro otp submit ${p.id} <6-digit-code>`
|
|
4511
|
+
})),
|
|
4512
|
+
total: pendings.length
|
|
4361
4513
|
});
|
|
4362
|
-
}
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4514
|
+
});
|
|
4515
|
+
}
|
|
4516
|
+
async function completePendingOtp(ctx, hookService, account, pending, code, spinner) {
|
|
4517
|
+
switch (pending.action) {
|
|
4518
|
+
case "email_bind":
|
|
4519
|
+
case "email_change": {
|
|
4520
|
+
spinner.stop();
|
|
4521
|
+
if (!pending.bindingId) throw new Error("Missing bindingId for email action.");
|
|
4522
|
+
const email = "email" in pending.data ? pending.data.email : "";
|
|
4523
|
+
const profile = await hookService.confirmEmailBinding(
|
|
4524
|
+
account.address,
|
|
4525
|
+
account.chainId,
|
|
4526
|
+
pending.bindingId,
|
|
4527
|
+
code
|
|
4528
|
+
);
|
|
4529
|
+
outputResult({
|
|
4530
|
+
status: pending.action === "email_bind" ? "email_bound" : "email_changed",
|
|
4531
|
+
email: profile.maskedEmail ?? profile.email ?? email,
|
|
4532
|
+
emailVerified: profile.emailVerified
|
|
4533
|
+
});
|
|
4534
|
+
break;
|
|
4535
|
+
}
|
|
4536
|
+
case "spending_limit": {
|
|
4537
|
+
spinner.stop();
|
|
4538
|
+
const dailyLimitUsdCents = pending.data.dailyLimitUsdCents;
|
|
4539
|
+
await hookService.setDailyLimit(
|
|
4540
|
+
account.address,
|
|
4541
|
+
account.chainId,
|
|
4542
|
+
dailyLimitUsdCents,
|
|
4543
|
+
code
|
|
4544
|
+
);
|
|
4545
|
+
outputResult({
|
|
4546
|
+
status: "daily_limit_set",
|
|
4547
|
+
dailyLimitUsd: (dailyLimitUsdCents / 100).toFixed(2)
|
|
4548
|
+
});
|
|
4549
|
+
break;
|
|
4550
|
+
}
|
|
4551
|
+
case "tx_send":
|
|
4552
|
+
case "2fa_uninstall": {
|
|
4553
|
+
if (!pending.challengeId) throw new Error("Missing challengeId for tx/2fa action.");
|
|
4554
|
+
spinner.text = "Verifying OTP...";
|
|
4555
|
+
const { userOp: userOpJson, entryPoint } = pending.data;
|
|
4556
|
+
const userOp = deserializeUserOp2(userOpJson);
|
|
4557
|
+
const authSessionId = pending.authSessionId;
|
|
4558
|
+
await hookService.verifySecurityOtp(
|
|
4559
|
+
account.address,
|
|
4560
|
+
account.chainId,
|
|
4561
|
+
pending.challengeId,
|
|
4562
|
+
code,
|
|
4563
|
+
authSessionId
|
|
4564
|
+
);
|
|
4565
|
+
spinner.text = "Retrying authorization...";
|
|
4566
|
+
const hookResult = await hookService.getHookSignature(
|
|
4567
|
+
account.address,
|
|
4568
|
+
account.chainId,
|
|
4569
|
+
entryPoint,
|
|
4570
|
+
userOp,
|
|
4571
|
+
authSessionId
|
|
4572
|
+
);
|
|
4573
|
+
if (hookResult.error) {
|
|
4574
|
+
throw new Error(`Authorization failed after OTP: ${hookResult.error.message}`);
|
|
4575
|
+
}
|
|
4576
|
+
const hookAddress = SECURITY_HOOK_ADDRESS_MAP[account.chainId];
|
|
4577
|
+
if (!hookAddress) throw new Error(`SecurityHook not deployed on chain ${account.chainId}.`);
|
|
4578
|
+
const { packedHash, validationData } = await ctx.sdk.getUserOpHash(userOp);
|
|
4579
|
+
const rawSignature = await ctx.keyring.signDigest(packedHash);
|
|
4580
|
+
userOp.signature = await ctx.sdk.packUserOpSignatureWithHook(
|
|
4581
|
+
rawSignature,
|
|
4582
|
+
validationData,
|
|
4583
|
+
hookAddress,
|
|
4584
|
+
hookResult.signature
|
|
4585
|
+
);
|
|
4586
|
+
spinner.text = "Sending UserOp...";
|
|
4587
|
+
const opHash = await ctx.sdk.sendUserOp(userOp);
|
|
4588
|
+
spinner.text = "Waiting for receipt...";
|
|
4589
|
+
const receipt = await ctx.sdk.waitForReceipt(opHash);
|
|
4590
|
+
if (!receipt.success) {
|
|
4591
|
+
throw new Error(`Transaction reverted: ${receipt.reason ?? "unknown"}`);
|
|
4592
|
+
}
|
|
4593
|
+
spinner.stop();
|
|
4594
|
+
outputResult({
|
|
4595
|
+
status: pending.action === "tx_send" ? "sent" : "uninstalled",
|
|
4596
|
+
userOpHash: opHash,
|
|
4597
|
+
transactionHash: receipt.transactionHash
|
|
4598
|
+
});
|
|
4599
|
+
break;
|
|
4600
|
+
}
|
|
4601
|
+
default:
|
|
4602
|
+
throw new Error(`Unknown action: ${pending.action}`);
|
|
4368
4603
|
}
|
|
4369
4604
|
}
|
|
4605
|
+
function deserializeUserOp2(raw) {
|
|
4606
|
+
return {
|
|
4607
|
+
sender: raw.sender,
|
|
4608
|
+
nonce: BigInt(raw.nonce ?? "0x0"),
|
|
4609
|
+
factory: raw.factory ?? null,
|
|
4610
|
+
factoryData: raw.factoryData ?? null,
|
|
4611
|
+
callData: raw.callData,
|
|
4612
|
+
callGasLimit: BigInt(raw.callGasLimit ?? "0x0"),
|
|
4613
|
+
verificationGasLimit: BigInt(raw.verificationGasLimit ?? "0x0"),
|
|
4614
|
+
preVerificationGas: BigInt(raw.preVerificationGas ?? "0x0"),
|
|
4615
|
+
maxFeePerGas: BigInt(raw.maxFeePerGas ?? "0x0"),
|
|
4616
|
+
maxPriorityFeePerGas: BigInt(raw.maxPriorityFeePerGas ?? "0x0"),
|
|
4617
|
+
paymaster: raw.paymaster ?? null,
|
|
4618
|
+
paymasterVerificationGasLimit: raw.paymasterVerificationGasLimit ? BigInt(raw.paymasterVerificationGasLimit) : null,
|
|
4619
|
+
paymasterPostOpGasLimit: raw.paymasterPostOpGasLimit ? BigInt(raw.paymasterPostOpGasLimit) : null,
|
|
4620
|
+
paymasterData: raw.paymasterData ?? null,
|
|
4621
|
+
signature: raw.signature ?? "0x"
|
|
4622
|
+
};
|
|
4623
|
+
}
|
|
4370
4624
|
|
|
4371
4625
|
// src/commands/config.ts
|
|
4372
4626
|
var KEY_MAP = {
|
|
@@ -4430,7 +4684,7 @@ import { execSync } from "child_process";
|
|
|
4430
4684
|
import { createRequire } from "module";
|
|
4431
4685
|
function resolveVersion() {
|
|
4432
4686
|
if (true) {
|
|
4433
|
-
return "0.
|
|
4687
|
+
return "0.6.1";
|
|
4434
4688
|
}
|
|
4435
4689
|
try {
|
|
4436
4690
|
const require2 = createRequire(import.meta.url);
|
|
@@ -4443,7 +4697,7 @@ function resolveVersion() {
|
|
|
4443
4697
|
var VERSION = resolveVersion();
|
|
4444
4698
|
|
|
4445
4699
|
// src/commands/update.ts
|
|
4446
|
-
import
|
|
4700
|
+
import ora7 from "ora";
|
|
4447
4701
|
import chalk2 from "chalk";
|
|
4448
4702
|
import { realpathSync } from "fs";
|
|
4449
4703
|
import { fileURLToPath } from "url";
|
|
@@ -4519,7 +4773,7 @@ function registerUpdateCommand(program2) {
|
|
|
4519
4773
|
}
|
|
4520
4774
|
});
|
|
4521
4775
|
updateCmd.action(async () => {
|
|
4522
|
-
const spinner =
|
|
4776
|
+
const spinner = ora7("Checking for updates\u2026").start();
|
|
4523
4777
|
try {
|
|
4524
4778
|
const latest = await fetchLatestVersion();
|
|
4525
4779
|
const cmp = compareSemver(VERSION, latest);
|
|
@@ -4550,6 +4804,41 @@ function registerUpdateCommand(program2) {
|
|
|
4550
4804
|
});
|
|
4551
4805
|
}
|
|
4552
4806
|
|
|
4807
|
+
// src/commands/prune.ts
|
|
4808
|
+
import { rm } from "fs/promises";
|
|
4809
|
+
import { join as join3 } from "path";
|
|
4810
|
+
import { homedir as homedir2 } from "os";
|
|
4811
|
+
var DATA_DIR = join3(homedir2(), ".elytro");
|
|
4812
|
+
async function runPrune() {
|
|
4813
|
+
try {
|
|
4814
|
+
const keyringProvider = new KeyringProvider();
|
|
4815
|
+
try {
|
|
4816
|
+
await keyringProvider.delete();
|
|
4817
|
+
} catch {
|
|
4818
|
+
}
|
|
4819
|
+
const fileProvider = new FileProvider(DATA_DIR);
|
|
4820
|
+
try {
|
|
4821
|
+
await fileProvider.delete();
|
|
4822
|
+
} catch {
|
|
4823
|
+
}
|
|
4824
|
+
try {
|
|
4825
|
+
await rm(DATA_DIR, { recursive: true, force: true });
|
|
4826
|
+
} catch (err) {
|
|
4827
|
+
const code = err.code;
|
|
4828
|
+
if (code !== "ENOENT") {
|
|
4829
|
+
throw err;
|
|
4830
|
+
}
|
|
4831
|
+
}
|
|
4832
|
+
outputResult({
|
|
4833
|
+
status: "pruned",
|
|
4834
|
+
dataDir: DATA_DIR,
|
|
4835
|
+
hint: "All local data cleared. Run `elytro init` to create a new wallet."
|
|
4836
|
+
});
|
|
4837
|
+
} catch (err) {
|
|
4838
|
+
outputError(-32e3, err.message);
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4553
4842
|
// src/index.ts
|
|
4554
4843
|
var program = new Command();
|
|
4555
4844
|
program.name("elytro").description("Elytro \u2014 ERC-4337 Smart Account Wallet CLI").version(VERSION).addHelpText(
|
|
@@ -4557,6 +4846,10 @@ program.name("elytro").description("Elytro \u2014 ERC-4337 Smart Account Wallet
|
|
|
4557
4846
|
"\nLearn how to use Elytro skills: https://github.com/Elytro-eth/skills\n"
|
|
4558
4847
|
);
|
|
4559
4848
|
async function main() {
|
|
4849
|
+
if (process.argv.includes("prune")) {
|
|
4850
|
+
await runPrune();
|
|
4851
|
+
return;
|
|
4852
|
+
}
|
|
4560
4853
|
let ctx = null;
|
|
4561
4854
|
try {
|
|
4562
4855
|
ctx = await createAppContext();
|
|
@@ -4565,6 +4858,7 @@ async function main() {
|
|
|
4565
4858
|
registerTxCommand(program, ctx);
|
|
4566
4859
|
registerQueryCommand(program, ctx);
|
|
4567
4860
|
registerSecurityCommand(program, ctx);
|
|
4861
|
+
registerOtpCommand(program, ctx);
|
|
4568
4862
|
registerConfigCommand(program, ctx);
|
|
4569
4863
|
registerUpdateCommand(program);
|
|
4570
4864
|
await program.parseAsync(process.argv);
|