@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.
- package/README.md +98 -37
- package/dist/index.js +508 -150
- 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
|
|
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
|
-
|
|
19
|
+
elytro init
|
|
10
20
|
|
|
11
|
-
# Create a smart account on Sepolia
|
|
12
|
-
|
|
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
|
-
|
|
25
|
+
elytro tx send --tx "to:0xRecipient,value:0.1"
|
|
16
26
|
|
|
17
27
|
# Check balance
|
|
18
|
-
|
|
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
|
|
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
|
|
28
|
-
- **
|
|
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** |
|
|
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
|
|
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
|
|
54
|
-
| --------------------- |
|
|
55
|
-
| `ELYTRO_VAULT_SECRET` | Base64 vault key (non-
|
|
56
|
-
| `ELYTRO_ALCHEMY_KEY` | Alchemy RPC endpoint
|
|
57
|
-
| `ELYTRO_PIMLICO_KEY` | Bundler + paymaster
|
|
58
|
-
| `ELYTRO_ENV` | `development` or `production`
|
|
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:
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
3231
|
-
challenge
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
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
|
-
|
|
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:
|
|
3593
|
+
nonce: toHex7(op.nonce),
|
|
3485
3594
|
factory: op.factory,
|
|
3486
3595
|
factoryData: op.factoryData,
|
|
3487
3596
|
callData: op.callData,
|
|
3488
|
-
callGasLimit:
|
|
3489
|
-
verificationGasLimit:
|
|
3490
|
-
preVerificationGas:
|
|
3491
|
-
maxFeePerGas:
|
|
3492
|
-
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 ?
|
|
3495
|
-
paymasterPostOpGasLimit: op.paymasterPostOpGasLimit ?
|
|
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
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
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
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
)
|
|
4130
|
-
|
|
4131
|
-
|
|
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
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
)
|
|
4170
|
-
|
|
4171
|
-
|
|
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
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
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
|
-
|
|
4328
|
-
|
|
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
|
-
}
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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);
|