@elytro/cli 0.5.2 → 0.5.3
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 +83 -37
- package/dist/index.js +45 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,89 +1,135 @@
|
|
|
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
|
+
- **Self-updating** — `elytro update` detects your package manager and upgrades in place
|
|
40
|
+
|
|
41
|
+
## Supported Chains
|
|
42
|
+
|
|
43
|
+
| Chain | Chain ID |
|
|
44
|
+
| ---------------- | --------- |
|
|
45
|
+
| Ethereum | 1 |
|
|
46
|
+
| Optimism | 10 |
|
|
47
|
+
| Arbitrum One | 42161 |
|
|
48
|
+
| Base | 8453 |
|
|
49
|
+
| Sepolia | 11155111 |
|
|
50
|
+
| Optimism Sepolia | 11155420 |
|
|
51
|
+
|
|
52
|
+
Public RPC and bundler endpoints are used by default. Provide your own Alchemy/Pimlico keys for higher rate limits.
|
|
29
53
|
|
|
30
54
|
## Architecture
|
|
31
55
|
|
|
32
56
|
| Component | Purpose |
|
|
33
57
|
| ------------------ | ------------------------------------------------ |
|
|
34
|
-
| **SecretProvider** | Vault key management (Keychain/env var)
|
|
58
|
+
| **SecretProvider** | Vault key management (Keychain / env var) |
|
|
35
59
|
| **KeyringService** | EOA encryption + decryption (AES-GCM) |
|
|
36
60
|
| **AccountService** | Smart account lifecycle (CREATE2, multi-account) |
|
|
37
|
-
| **SdkService** |
|
|
61
|
+
| **SdkService** | `@elytro/sdk` wrapper (UserOp building) |
|
|
38
62
|
| **FileStore** | Persistent state (`~/.elytro/`) |
|
|
39
63
|
|
|
40
|
-
See [docs/architecture.md](docs/architecture.md) for detailed data flow.
|
|
41
|
-
|
|
42
64
|
## Security Model
|
|
43
65
|
|
|
44
|
-
- **No plaintext keys on disk** — vault key stored in
|
|
66
|
+
- **No plaintext keys on disk** — vault key stored in system keychain or injected at runtime
|
|
45
67
|
- **AES-GCM encryption** — all private keys encrypted with vault key before storage
|
|
46
68
|
- **Consume-once env var** — `ELYTRO_VAULT_SECRET` deleted from process after load
|
|
47
69
|
- **Memory cleanup** — all key buffers zeroed after use
|
|
48
70
|
|
|
49
|
-
See [docs/security.md](docs/security.md) for threat model.
|
|
50
|
-
|
|
51
71
|
## Configuration
|
|
52
72
|
|
|
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`
|
|
73
|
+
| Variable | Purpose | Required |
|
|
74
|
+
| --------------------- | ----------------------------------------- | -------------------- |
|
|
75
|
+
| `ELYTRO_VAULT_SECRET` | Base64 vault key (non-keychain platforms) | Yes, if no keychain |
|
|
76
|
+
| `ELYTRO_ALCHEMY_KEY` | Alchemy RPC endpoint | Optional (rate limit)|
|
|
77
|
+
| `ELYTRO_PIMLICO_KEY` | Bundler + paymaster | Optional (rate limit)|
|
|
78
|
+
| `ELYTRO_ENV` | `development` or `production` | Optional |
|
|
59
79
|
|
|
60
|
-
Persist API keys:
|
|
80
|
+
Persist API keys:
|
|
81
|
+
```bash
|
|
82
|
+
elytro config set alchemy-key <key>
|
|
83
|
+
elytro config set pimlico-key <key>
|
|
84
|
+
```
|
|
61
85
|
|
|
62
86
|
## Commands
|
|
63
87
|
|
|
64
88
|
```bash
|
|
65
89
|
# Account Management
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
elytro account create --chain <chainId> [--alias name] [--email addr] [--daily-limit amount]
|
|
91
|
+
elytro account list [alias|address]
|
|
92
|
+
elytro account info [alias|address]
|
|
93
|
+
elytro account switch [alias|address]
|
|
94
|
+
elytro account activate [alias|address] # Deploy to chain
|
|
71
95
|
|
|
72
96
|
# Transactions
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
elytro tx send --tx "to:0xAddr,value:0.1" [--tx ...]
|
|
98
|
+
elytro tx build --tx "to:0xAddr,data:0xab..."
|
|
99
|
+
elytro tx simulate --tx "to:0xAddr,value:0.1"
|
|
76
100
|
|
|
77
101
|
# Queries
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
elytro query balance [account] [--token erc20Addr]
|
|
103
|
+
elytro query tokens [account]
|
|
104
|
+
elytro query tx <hash>
|
|
105
|
+
elytro query chain
|
|
106
|
+
elytro query address <address>
|
|
107
|
+
|
|
108
|
+
# Security (2FA + spending limits)
|
|
109
|
+
elytro security status
|
|
110
|
+
elytro security 2fa install [--capability 1|2|3]
|
|
111
|
+
elytro security 2fa uninstall
|
|
112
|
+
elytro security 2fa uninstall --force # Start safety-delay countdown
|
|
113
|
+
elytro security 2fa uninstall --force --execute # Execute after delay
|
|
114
|
+
elytro security email bind <email>
|
|
115
|
+
elytro security email change <email>
|
|
116
|
+
elytro security spending-limit [amount] # View or set daily USD limit
|
|
117
|
+
|
|
118
|
+
# Updates
|
|
119
|
+
elytro update # Check and upgrade to latest
|
|
120
|
+
elytro update check # Check without installing
|
|
121
|
+
|
|
122
|
+
# Config
|
|
123
|
+
elytro config set <key> <value>
|
|
124
|
+
elytro config get <key>
|
|
125
|
+
elytro config list
|
|
83
126
|
```
|
|
84
127
|
|
|
85
128
|
## Development
|
|
86
129
|
|
|
87
130
|
```bash
|
|
88
|
-
|
|
131
|
+
bun install
|
|
132
|
+
bun dev <command> # Run from source
|
|
133
|
+
bun run build # Build to dist/
|
|
134
|
+
bun run test # Smoke tests
|
|
89
135
|
```
|
package/dist/index.js
CHANGED
|
@@ -2058,15 +2058,17 @@ var SecurityHookService = class {
|
|
|
2058
2058
|
if (result.errors && result.errors.length > 0) {
|
|
2059
2059
|
const ext = result.errors[0].extensions;
|
|
2060
2060
|
if (ext) {
|
|
2061
|
+
const challengeDetails = typeof ext.challenge === "object" && ext.challenge !== null ? ext.challenge : void 0;
|
|
2062
|
+
const getChallengeValue = (key) => ext[key] ?? (challengeDetails ? challengeDetails[key] : void 0);
|
|
2061
2063
|
return {
|
|
2062
2064
|
error: {
|
|
2063
2065
|
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,
|
|
2066
|
+
challengeId: getChallengeValue("challengeId"),
|
|
2067
|
+
currentSpendUsdCents: ext.currentSpendUsdCents ?? getChallengeValue("currentSpendUsdCents"),
|
|
2068
|
+
dailyLimitUsdCents: ext.dailyLimitUsdCents ?? getChallengeValue("dailyLimitUsdCents"),
|
|
2069
|
+
maskedEmail: ext.maskedEmail ?? getChallengeValue("maskedEmail"),
|
|
2070
|
+
otpExpiresAt: ext.otpExpiresAt ?? getChallengeValue("otpExpiresAt"),
|
|
2071
|
+
projectedSpendUsdCents: ext.projectedSpendUsdCents ?? getChallengeValue("projectedSpendUsdCents"),
|
|
2070
2072
|
message: result.errors[0].message
|
|
2071
2073
|
}
|
|
2072
2074
|
};
|
|
@@ -2078,15 +2080,17 @@ var SecurityHookService = class {
|
|
|
2078
2080
|
if (err instanceof GraphQLClientError && err.errors?.length) {
|
|
2079
2081
|
const ext = err.errors[0].extensions;
|
|
2080
2082
|
if (ext?.code) {
|
|
2083
|
+
const challengeDetails = typeof ext.challenge === "object" && ext.challenge !== null ? ext.challenge : void 0;
|
|
2084
|
+
const getChallengeValue = (key) => ext[key] ?? (challengeDetails ? challengeDetails[key] : void 0);
|
|
2081
2085
|
return {
|
|
2082
2086
|
error: {
|
|
2083
2087
|
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,
|
|
2088
|
+
challengeId: getChallengeValue("challengeId"),
|
|
2089
|
+
currentSpendUsdCents: ext.currentSpendUsdCents ?? getChallengeValue("currentSpendUsdCents"),
|
|
2090
|
+
dailyLimitUsdCents: ext.dailyLimitUsdCents ?? getChallengeValue("dailyLimitUsdCents"),
|
|
2091
|
+
maskedEmail: ext.maskedEmail ?? getChallengeValue("maskedEmail"),
|
|
2092
|
+
otpExpiresAt: ext.otpExpiresAt ?? getChallengeValue("otpExpiresAt"),
|
|
2093
|
+
projectedSpendUsdCents: ext.projectedSpendUsdCents ?? getChallengeValue("projectedSpendUsdCents"),
|
|
2090
2094
|
message: err.errors[0].message
|
|
2091
2095
|
}
|
|
2092
2096
|
};
|
|
@@ -3227,10 +3231,38 @@ function registerTxCommand(program2, ctx) {
|
|
|
3227
3231
|
spinner.stop();
|
|
3228
3232
|
const errCode = hookResult.error.code;
|
|
3229
3233
|
if (errCode === "OTP_REQUIRED" || errCode === "SPENDING_LIMIT_EXCEEDED") {
|
|
3234
|
+
if (!hookResult.error.challengeId) {
|
|
3235
|
+
const otpRequestSpinner = ora3("Requesting OTP challenge...").start();
|
|
3236
|
+
try {
|
|
3237
|
+
const otpChallenge = await hookService.requestSecurityOtp(
|
|
3238
|
+
accountInfo.address,
|
|
3239
|
+
accountInfo.chainId,
|
|
3240
|
+
ctx.sdk.entryPoint,
|
|
3241
|
+
userOp
|
|
3242
|
+
);
|
|
3243
|
+
hookResult.error.challengeId = otpChallenge.challengeId;
|
|
3244
|
+
hookResult.error.maskedEmail ??= otpChallenge.maskedEmail;
|
|
3245
|
+
hookResult.error.otpExpiresAt ??= otpChallenge.otpExpiresAt;
|
|
3246
|
+
otpRequestSpinner.stop();
|
|
3247
|
+
} catch (otpErr) {
|
|
3248
|
+
otpRequestSpinner.fail("Failed to request OTP challenge.");
|
|
3249
|
+
throw new TxError(
|
|
3250
|
+
ERR_SEND_FAILED2,
|
|
3251
|
+
`Unable to request OTP challenge: ${otpErr.message}`
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
if (!hookResult.error.challengeId) {
|
|
3256
|
+
throw new TxError(
|
|
3257
|
+
ERR_SEND_FAILED2,
|
|
3258
|
+
"OTP challenge ID was not provided by Elytro API. Please try again."
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3230
3261
|
console.error(JSON.stringify({
|
|
3231
3262
|
challenge: errCode,
|
|
3232
3263
|
message: hookResult.error.message ?? `Verification required (${errCode}).`,
|
|
3233
3264
|
...hookResult.error.maskedEmail ? { maskedEmail: hookResult.error.maskedEmail } : {},
|
|
3265
|
+
...hookResult.error.otpExpiresAt ? { otpExpiresAt: hookResult.error.otpExpiresAt } : {},
|
|
3234
3266
|
...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
3267
|
}, null, 2));
|
|
3236
3268
|
const otpCode = await askInput("Enter the 6-digit OTP code:");
|
|
@@ -4398,7 +4430,7 @@ import { execSync } from "child_process";
|
|
|
4398
4430
|
import { createRequire } from "module";
|
|
4399
4431
|
function resolveVersion() {
|
|
4400
4432
|
if (true) {
|
|
4401
|
-
return "0.5.
|
|
4433
|
+
return "0.5.3";
|
|
4402
4434
|
}
|
|
4403
4435
|
try {
|
|
4404
4436
|
const require2 = createRequire(import.meta.url);
|