@darksol/terminal 0.2.1 → 0.2.2
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 +263 -189
- package/package.json +1 -1
- package/src/cli.js +16 -1
- package/src/llm/intent.js +126 -0
- package/src/ui/banner.js +3 -2
- package/tests/cli.test.js +0 -72
- package/tests/config.test.js +0 -75
- package/tests/dca.test.js +0 -141
- package/tests/keystore.test.js +0 -94
- package/tests/scripts.test.js +0 -136
- package/tests/trading.test.js +0 -21
- package/tests/ui.test.js +0 -27
package/README.md
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
**All DARKSOL services. One terminal. Zero trust required.**
|
|
11
11
|
|
|
12
|
-
A unified CLI for market intel, trading, on-chain oracle, casino, prepaid cards, builder indexing, and more. Encrypted wallet management. Agent-native. OpenClaw-controllable.
|
|
12
|
+
A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, casino, prepaid cards, builder indexing, secure agent signing, and more. Encrypted wallet management. Agent-native. OpenClaw-controllable.
|
|
13
|
+
|
|
14
|
+
[](https://www.npmjs.com/package/@darksol/terminal)
|
|
15
|
+
[](https://opensource.org/licenses/MIT)
|
|
16
|
+
[](https://nodejs.org/)
|
|
13
17
|
|
|
14
18
|
## Install
|
|
15
19
|
|
|
@@ -29,81 +33,198 @@ darksol wallet create main
|
|
|
29
33
|
# Check balance
|
|
30
34
|
darksol wallet balance
|
|
31
35
|
|
|
32
|
-
# Set chain
|
|
33
|
-
darksol config set chain base
|
|
34
|
-
|
|
35
|
-
# Add custom RPC
|
|
36
|
-
darksol config rpc base https://your-rpc-endpoint.com
|
|
37
|
-
|
|
38
36
|
# Swap tokens
|
|
39
37
|
darksol trade swap -i ETH -o USDC -a 0.1
|
|
40
38
|
|
|
41
|
-
#
|
|
42
|
-
darksol
|
|
43
|
-
|
|
44
|
-
# Create DCA order
|
|
45
|
-
darksol dca create
|
|
46
|
-
|
|
47
|
-
# Market data
|
|
48
|
-
darksol market top
|
|
49
|
-
darksol market token VIRTUAL
|
|
50
|
-
darksol market compare ETH AERO VIRTUAL
|
|
51
|
-
|
|
52
|
-
# Oracle
|
|
53
|
-
darksol oracle flip
|
|
54
|
-
darksol oracle dice 20
|
|
55
|
-
|
|
56
|
-
# Casino
|
|
57
|
-
darksol casino bet coin-flip heads
|
|
58
|
-
darksol casino tables
|
|
59
|
-
|
|
60
|
-
# Execution scripts (automated trading)
|
|
61
|
-
darksol script templates # See available templates
|
|
62
|
-
darksol script create # Create from template (interactive)
|
|
63
|
-
darksol script list # List your scripts
|
|
64
|
-
darksol script run my-buy-script # Execute (requires wallet password)
|
|
65
|
-
darksol script show my-script # View details + code
|
|
66
|
-
darksol script edit my-script # Edit params/wallet/chain
|
|
67
|
-
darksol script clone my-script new-script
|
|
68
|
-
darksol script delete old-script
|
|
69
|
-
|
|
70
|
-
# Prepaid cards
|
|
71
|
-
darksol cards catalog
|
|
72
|
-
|
|
73
|
-
# Builder index
|
|
74
|
-
darksol builders leaderboard
|
|
39
|
+
# AI trading assistant
|
|
40
|
+
darksol ai chat
|
|
75
41
|
|
|
76
|
-
#
|
|
77
|
-
darksol
|
|
42
|
+
# Start agent signer for OpenClaw
|
|
43
|
+
darksol agent start main
|
|
78
44
|
```
|
|
79
45
|
|
|
80
|
-
## Wallet Security
|
|
81
|
-
|
|
82
|
-
- Private keys are **never stored in plaintext**
|
|
83
|
-
- AES-256-GCM encryption with scrypt key derivation
|
|
84
|
-
- Password required for every transaction
|
|
85
|
-
- Keys stored in `~/.darksol/wallets/` (encrypted JSON)
|
|
86
|
-
- No recovery without password — back it up
|
|
87
|
-
|
|
88
46
|
## Modules
|
|
89
47
|
|
|
90
48
|
| Module | Description | Pricing |
|
|
91
49
|
|--------|-------------|---------|
|
|
92
|
-
| `wallet` | Create, import, manage wallets | Free |
|
|
93
|
-
| `trade` | Swap, snipe, token trading | Gas only |
|
|
50
|
+
| `wallet` | Create, import, manage encrypted wallets | Free |
|
|
51
|
+
| `trade` | Swap (Uniswap V3), snipe (V2), token trading | Gas only |
|
|
94
52
|
| `dca` | Dollar-cost averaging engine | Gas only |
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
53
|
+
| `ai` | LLM-powered trading assistant & analysis | Provider dependent |
|
|
54
|
+
| `agent` | Secure agent signer (PK-isolated proxy) | Free |
|
|
55
|
+
| `keys` | API key vault (LLMs, data, RPCs) | Free |
|
|
56
|
+
| `script` | Execution scripts & automated strategies | Free |
|
|
57
|
+
| `skills` | Agent skill directory & installer | Free |
|
|
58
|
+
| `market` | Market intel, top movers, token analysis | x402 micropayments |
|
|
97
59
|
| `oracle` | On-chain random number oracle | $0.05–$0.25 |
|
|
98
60
|
| `casino` | The Clawsino — on-chain betting | $1 flat bets |
|
|
99
|
-
| `cards` | Crypto → prepaid Visa/MC | 3% markup |
|
|
61
|
+
| `cards` | Crypto → prepaid Visa/MC (no KYC) | 3% markup |
|
|
100
62
|
| `builders` | ERC-8021 builder leaderboard | Free |
|
|
101
|
-
| `facilitator` | x402 payment verification | Free |
|
|
63
|
+
| `facilitator` | x402 payment verification & settlement | Free |
|
|
102
64
|
| `config` | Terminal configuration | Free |
|
|
103
65
|
|
|
104
|
-
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🔐 Secure Agent Signer
|
|
69
|
+
|
|
70
|
+
**The killer feature.** A PK-isolated signing proxy for AI agents (OpenClaw, etc.).
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Start the signing proxy
|
|
74
|
+
darksol agent start my-wallet
|
|
75
|
+
|
|
76
|
+
# With spending limits
|
|
77
|
+
darksol agent start my-wallet --max-value 0.5 --daily-limit 2.0
|
|
78
|
+
|
|
79
|
+
# With contract allowlist
|
|
80
|
+
darksol agent start my-wallet --allowlist 0xContract1,0xContract2
|
|
81
|
+
|
|
82
|
+
# View security documentation
|
|
83
|
+
darksol agent docs
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Why it exists:** AI agents need to sign transactions, but exposing private keys to LLMs is dangerous — prompt injection could leak the key. Existing wallets (Bankr, Phantom MCP) can't do x402 payments or real contract signing.
|
|
87
|
+
|
|
88
|
+
**How it works:**
|
|
89
|
+
1. You unlock your wallet ONCE with your password
|
|
90
|
+
2. The key decrypts into memory (never to disk/API)
|
|
91
|
+
3. A local HTTP server at `127.0.0.1:18790` exposes signing endpoints
|
|
92
|
+
4. AI agents call `/send`, `/sign` — never see the key
|
|
93
|
+
5. Every TX is validated against your security policy
|
|
94
|
+
|
|
95
|
+
**Endpoints:**
|
|
96
|
+
|
|
97
|
+
| Endpoint | Method | Description |
|
|
98
|
+
|----------|--------|-------------|
|
|
99
|
+
| `/address` | GET | Wallet address (safe) |
|
|
100
|
+
| `/balance` | GET | ETH balance (safe) |
|
|
101
|
+
| `/chain` | GET | Active chain info |
|
|
102
|
+
| `/send` | POST | Sign + broadcast transaction |
|
|
103
|
+
| `/sign` | POST | Sign transaction (return raw) |
|
|
104
|
+
| `/sign-message` | POST | Sign EIP-191 message |
|
|
105
|
+
| `/sign-typed-data` | POST | Sign EIP-712 typed data (x402) |
|
|
106
|
+
| `/policy` | GET | View spending policy |
|
|
107
|
+
| `/audit` | GET | Operation audit log |
|
|
108
|
+
| `/health` | GET | Health check |
|
|
109
|
+
|
|
110
|
+
**Security guarantees:**
|
|
111
|
+
- ✅ No `/private-key` endpoint exists — literally impossible to extract
|
|
112
|
+
- ✅ Loopback-only (127.0.0.1) — not accessible from network
|
|
113
|
+
- ✅ One-time bearer token auth (shown only in terminal)
|
|
114
|
+
- ✅ Per-TX value limits + daily spending cap
|
|
115
|
+
- ✅ Contract allowlist support
|
|
116
|
+
- ✅ Dangerous selectors blocked (transferOwnership, selfdestruct)
|
|
117
|
+
- ✅ Full audit log of all operations
|
|
118
|
+
- ✅ Prompt injection proof — the LLM cannot access what doesn't exist in any API response
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 🧠 AI Trading Assistant
|
|
123
|
+
|
|
124
|
+
Natural language trading powered by multi-provider LLM support.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Interactive chat with live market data
|
|
128
|
+
darksol ai chat
|
|
105
129
|
|
|
106
|
-
|
|
130
|
+
# One-shot intent parsing
|
|
131
|
+
darksol ai ask "buy 0.5 ETH worth of AERO on Base"
|
|
132
|
+
|
|
133
|
+
# DCA strategy recommendation
|
|
134
|
+
darksol ai strategy VIRTUAL --budget 500 --timeframe "30 days"
|
|
135
|
+
|
|
136
|
+
# AI-powered token analysis
|
|
137
|
+
darksol ai analyze AERO
|
|
138
|
+
|
|
139
|
+
# Use specific provider
|
|
140
|
+
darksol ai chat --provider ollama --model llama3
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Supported providers:** OpenAI, Anthropic, OpenRouter, Ollama (local = free)
|
|
144
|
+
|
|
145
|
+
The AI gets live market context (prices from DexScreener), knows your config (chain, slippage, wallet), and returns structured intents with confidence scores and risk warnings.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 🔑 API Key Vault
|
|
150
|
+
|
|
151
|
+
Encrypted storage for all your API keys.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# List all services and status
|
|
155
|
+
darksol keys list
|
|
156
|
+
|
|
157
|
+
# Add keys (encrypted with AES-256-GCM)
|
|
158
|
+
darksol keys add openai
|
|
159
|
+
darksol keys add coingecko
|
|
160
|
+
darksol keys add alchemy
|
|
161
|
+
|
|
162
|
+
# Remove a key
|
|
163
|
+
darksol keys remove openai
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Supported services:**
|
|
167
|
+
| Category | Services |
|
|
168
|
+
|----------|----------|
|
|
169
|
+
| LLM | OpenAI, Anthropic, OpenRouter, Ollama |
|
|
170
|
+
| Data | CoinGecko Pro, DexScreener, DefiLlama |
|
|
171
|
+
| RPC | Alchemy, Infura, QuickNode |
|
|
172
|
+
| Trading | 1inch, ParaSwap |
|
|
173
|
+
|
|
174
|
+
Keys can also come from environment variables (e.g., `OPENAI_API_KEY`).
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 💰 Trading
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Swap via Uniswap V3
|
|
182
|
+
darksol trade swap -i ETH -o USDC -a 0.1
|
|
183
|
+
|
|
184
|
+
# Snipe a token (Uniswap V2, fast buy)
|
|
185
|
+
darksol trade snipe 0xTOKEN -a 0.05
|
|
186
|
+
|
|
187
|
+
# Snipe with gas boost
|
|
188
|
+
darksol trade snipe 0xTOKEN -a 0.05 -g 2.0
|
|
189
|
+
|
|
190
|
+
# Watch for new pairs
|
|
191
|
+
darksol trade watch
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## 📊 DCA Engine
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
darksol dca create # Interactive order creation
|
|
198
|
+
darksol dca list # List active orders
|
|
199
|
+
darksol dca run # Execute pending orders
|
|
200
|
+
darksol dca cancel <id>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 📈 Market Intel
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
darksol market top # Top movers on Base
|
|
207
|
+
darksol market top -c ethereum # Top movers on Ethereum
|
|
208
|
+
darksol market token VIRTUAL # Full token detail
|
|
209
|
+
darksol market compare ETH AERO VIRTUAL # Side-by-side comparison
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## ⚡ Execution Scripts
|
|
215
|
+
|
|
216
|
+
Automated trading strategies with full wallet access.
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
darksol script templates # Available templates
|
|
220
|
+
darksol script create # Create from template (interactive)
|
|
221
|
+
darksol script list # List scripts
|
|
222
|
+
darksol script run my-buy # Execute (password required)
|
|
223
|
+
darksol script show my-buy # View code + params
|
|
224
|
+
darksol script edit my-buy # Edit params
|
|
225
|
+
darksol script clone my-buy new-buy
|
|
226
|
+
darksol script delete old
|
|
227
|
+
```
|
|
107
228
|
|
|
108
229
|
### Templates
|
|
109
230
|
|
|
@@ -117,171 +238,123 @@ Automated trading strategies with full PK access. Scripts unlock your wallet at
|
|
|
117
238
|
| `transfer` | Transfer ETH or tokens to an address |
|
|
118
239
|
| `empty` | Custom script — full ethers.js context |
|
|
119
240
|
|
|
120
|
-
###
|
|
121
|
-
|
|
122
|
-
Every script gets:
|
|
123
|
-
```javascript
|
|
124
|
-
module.exports = async function({ signer, provider, ethers, config, params }) {
|
|
125
|
-
// signer — ethers.Wallet (unlocked, connected to provider)
|
|
126
|
-
// provider — ethers.JsonRpcProvider for active chain
|
|
127
|
-
// ethers — the ethers library
|
|
128
|
-
// config — { chain, slippage, gasMultiplier, rpcs }
|
|
129
|
-
// params — your custom parameters
|
|
130
|
-
};
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Automation (OpenClaw / cron)
|
|
241
|
+
### Automation
|
|
134
242
|
|
|
135
243
|
```bash
|
|
136
|
-
#
|
|
244
|
+
# Non-interactive execution (for cron/OpenClaw)
|
|
137
245
|
darksol script run my-dca --password "mypass" --yes
|
|
138
|
-
|
|
139
|
-
# JSON output for programmatic use
|
|
140
|
-
darksol config set output json
|
|
141
246
|
```
|
|
142
247
|
|
|
143
|
-
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 🧩 Skills Directory
|
|
144
251
|
|
|
145
|
-
|
|
252
|
+
Install DARKSOL skills for OpenClaw agents.
|
|
146
253
|
|
|
147
254
|
```bash
|
|
148
|
-
#
|
|
149
|
-
darksol
|
|
255
|
+
darksol skills list # Browse available skills
|
|
256
|
+
darksol skills install darksol-terminal # Install to OpenClaw
|
|
257
|
+
darksol skills info darksol-terminal # Skill details
|
|
258
|
+
darksol skills uninstall darksol-terminal
|
|
259
|
+
```
|
|
150
260
|
|
|
151
|
-
|
|
152
|
-
darksol config set chain base
|
|
261
|
+
**Available skills:** darksol-terminal, darksol-facilitator, darksol-prepaid-cards, random-oracle, the-clawsino
|
|
153
262
|
|
|
154
|
-
|
|
155
|
-
darksol config set slippage 1.0
|
|
263
|
+
---
|
|
156
264
|
|
|
157
|
-
|
|
158
|
-
darksol config rpc base https://mainnet.base.org
|
|
159
|
-
darksol config rpc ethereum https://eth.llamarpc.com
|
|
160
|
-
darksol config rpc arbitrum https://arb1.arbitrum.io/rpc
|
|
161
|
-
```
|
|
265
|
+
## 🎲 Services
|
|
162
266
|
|
|
163
|
-
|
|
267
|
+
```bash
|
|
268
|
+
# Oracle — on-chain randomness
|
|
269
|
+
darksol oracle flip
|
|
270
|
+
darksol oracle dice 20
|
|
271
|
+
darksol oracle number 1 100
|
|
164
272
|
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
- Optimism
|
|
273
|
+
# Casino — on-chain betting
|
|
274
|
+
darksol casino bet coin-flip heads
|
|
275
|
+
darksol casino tables
|
|
276
|
+
darksol casino stats
|
|
170
277
|
|
|
171
|
-
|
|
278
|
+
# Prepaid Cards — crypto to Visa/MC
|
|
279
|
+
darksol cards catalog
|
|
280
|
+
darksol cards order -p swype -a 50
|
|
281
|
+
darksol cards status <id>
|
|
172
282
|
|
|
173
|
-
|
|
283
|
+
# Builder Index — ERC-8021 rankings
|
|
284
|
+
darksol builders leaderboard
|
|
285
|
+
darksol builders lookup <code>
|
|
174
286
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
darksol
|
|
178
|
-
darksol wallet balance main
|
|
179
|
-
darksol oracle flip
|
|
287
|
+
# Facilitator — x402 payments
|
|
288
|
+
darksol facilitator health
|
|
289
|
+
darksol facilitator verify <payment>
|
|
180
290
|
```
|
|
181
291
|
|
|
182
|
-
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## 🔒 Wallet Security
|
|
295
|
+
|
|
296
|
+
- Private keys **never stored in plaintext**
|
|
297
|
+
- AES-256-GCM encryption with scrypt key derivation (N=2^18)
|
|
298
|
+
- Password required for every transaction
|
|
299
|
+
- Keys stored in `~/.darksol/wallets/` (encrypted JSON)
|
|
300
|
+
- No recovery without password — back it up
|
|
301
|
+
|
|
183
302
|
```bash
|
|
184
|
-
darksol
|
|
303
|
+
darksol wallet create <name> # Create new (generates keypair)
|
|
304
|
+
darksol wallet import <name> # Import from private key
|
|
305
|
+
darksol wallet list # List all wallets
|
|
306
|
+
darksol wallet balance [name] # ETH + USDC balance
|
|
307
|
+
darksol wallet use <name> # Set active wallet
|
|
308
|
+
darksol wallet export [name] # Export (password required for PK)
|
|
185
309
|
```
|
|
186
310
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
When writing custom execution scripts, you have access to powerful helper utilities:
|
|
190
|
-
|
|
191
|
-
```javascript
|
|
192
|
-
import {
|
|
193
|
-
// Providers & Chain
|
|
194
|
-
getProvider, // Get ethers provider for any chain
|
|
195
|
-
CHAIN_IDS, // { base: 8453, ethereum: 1, ... }
|
|
196
|
-
EXPLORERS, // Block explorer URLs per chain
|
|
197
|
-
txUrl, addressUrl, // Generate explorer links
|
|
198
|
-
|
|
199
|
-
// Tokens
|
|
200
|
-
getERC20, // Get ERC20 contract instance
|
|
201
|
-
getFullTokenInfo, // Name, symbol, decimals, totalSupply
|
|
202
|
-
getTokenBalance, // Formatted balance for any token
|
|
203
|
-
ensureApproval, // Check & approve token spending
|
|
204
|
-
TOKENS, // All known token addresses per chain
|
|
205
|
-
getUSDC, getWETH, // Quick chain-specific lookups
|
|
206
|
-
|
|
207
|
-
// Gas
|
|
208
|
-
estimateGasCost, // Estimate gas in ETH
|
|
209
|
-
getBoostedGas, // Priority gas settings for snipes
|
|
210
|
-
|
|
211
|
-
// Formatting
|
|
212
|
-
formatCompact, // 1234567 → "1.23M"
|
|
213
|
-
formatUSD, // Format as $1,234.56
|
|
214
|
-
formatETH, // Format wei to ETH string
|
|
215
|
-
formatTokenAmount, // Format with symbol
|
|
216
|
-
shortAddress, // 0x1234...5678
|
|
217
|
-
formatDuration, // Seconds → "2h 30m"
|
|
218
|
-
|
|
219
|
-
// Validation
|
|
220
|
-
isValidAddress, // Check Ethereum address
|
|
221
|
-
isValidPrivateKey, // Check private key format
|
|
222
|
-
isValidAmount, // Check positive number
|
|
223
|
-
parseTokenAmount, // String → bigint with decimals
|
|
224
|
-
|
|
225
|
-
// Async
|
|
226
|
-
sleep, // await sleep(1000)
|
|
227
|
-
retry, // Retry with exponential backoff
|
|
228
|
-
waitForTx, // Wait for tx with timeout
|
|
229
|
-
|
|
230
|
-
// Price
|
|
231
|
-
quickPrice, // DexScreener price lookup
|
|
232
|
-
hasLiquidity, // Check minimum liquidity
|
|
233
|
-
} from './utils/helpers.js';
|
|
234
|
-
```
|
|
311
|
+
---
|
|
235
312
|
|
|
236
|
-
|
|
313
|
+
## ⚙️ Configuration
|
|
237
314
|
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
315
|
+
```bash
|
|
316
|
+
darksol config show # View all settings
|
|
317
|
+
darksol config set chain base # Set active chain
|
|
318
|
+
darksol config set slippage 1.0 # Slippage tolerance (%)
|
|
319
|
+
darksol config rpc base https://your-rpc.com
|
|
320
|
+
```
|
|
242
321
|
|
|
243
|
-
|
|
244
|
-
const liquid = await helpers.hasLiquidity(params.token, 5000);
|
|
245
|
-
if (!liquid) throw new Error('Insufficient liquidity');
|
|
322
|
+
### Supported Chains
|
|
246
323
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
324
|
+
| Chain | ID | Default RPC |
|
|
325
|
+
|-------|---:|-------------|
|
|
326
|
+
| Base | 8453 | mainnet.base.org |
|
|
327
|
+
| Ethereum | 1 | eth.llamarpc.com |
|
|
328
|
+
| Polygon | 137 | polygon-rpc.com |
|
|
329
|
+
| Arbitrum | 42161 | arb1.arbitrum.io/rpc |
|
|
330
|
+
| Optimism | 10 | mainnet.optimism.io |
|
|
250
331
|
|
|
251
|
-
|
|
252
|
-
const gas = await helpers.getBoostedGas(provider, 1.5);
|
|
332
|
+
---
|
|
253
333
|
|
|
254
|
-
|
|
255
|
-
const result = await helpers.retry(async () => {
|
|
256
|
-
const tx = await signer.sendTransaction({ ...txParams, ...gas });
|
|
257
|
-
return helpers.waitForTx(tx, 60000);
|
|
258
|
-
}, 3, 2000);
|
|
334
|
+
## 📚 Reference
|
|
259
335
|
|
|
260
|
-
|
|
261
|
-
|
|
336
|
+
```bash
|
|
337
|
+
darksol tips # Trading + scripting tips
|
|
338
|
+
darksol tips --trading # Trading tips only
|
|
339
|
+
darksol networks # Chain reference table
|
|
340
|
+
darksol quickstart # Getting started guide
|
|
341
|
+
darksol lookup 0x... # Look up any address
|
|
262
342
|
```
|
|
263
343
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
```bash
|
|
267
|
-
# Trading tips (slippage, MEV protection, etc.)
|
|
268
|
-
darksol tips --trading
|
|
344
|
+
---
|
|
269
345
|
|
|
270
|
-
|
|
271
|
-
darksol tips --scripts
|
|
346
|
+
## 🤖 OpenClaw Integration
|
|
272
347
|
|
|
273
|
-
|
|
274
|
-
darksol tips
|
|
348
|
+
DARKSOL Terminal is agent-native:
|
|
275
349
|
|
|
276
|
-
|
|
277
|
-
darksol
|
|
350
|
+
1. **Install the skill:** `darksol skills install darksol-terminal`
|
|
351
|
+
2. **Start the agent signer:** `darksol agent start my-wallet`
|
|
352
|
+
3. **Run commands non-interactively** with flags (`-p`, `-y`, `--key`)
|
|
353
|
+
4. **JSON output:** `darksol config set output json`
|
|
278
354
|
|
|
279
|
-
|
|
280
|
-
darksol quickstart
|
|
355
|
+
All commands work without prompts when flags are provided.
|
|
281
356
|
|
|
282
|
-
|
|
283
|
-
darksol lookup 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
|
|
284
|
-
```
|
|
357
|
+
---
|
|
285
358
|
|
|
286
359
|
## Development
|
|
287
360
|
|
|
@@ -289,6 +362,7 @@ darksol lookup 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
|
|
|
289
362
|
git clone https://gitlab.com/darks0l/darksol-terminal
|
|
290
363
|
cd darksol-terminal
|
|
291
364
|
npm install
|
|
365
|
+
npm test # Run test suite (node:test)
|
|
292
366
|
node bin/darksol.js
|
|
293
367
|
```
|
|
294
368
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { buildersLeaderboard, buildersLookup, buildersFeed } from './services/bu
|
|
|
16
16
|
import { createScript, listScripts, runScript, showScript, editScript, deleteScript, cloneScript, listTemplates } from './scripts/engine.js';
|
|
17
17
|
import { showTradingTips, showScriptTips, showNetworkReference, showQuickStart, showWalletSummary, showTokenInfo, showTxResult } from './utils/helpers.js';
|
|
18
18
|
import { addKey, removeKey, listKeys } from './config/keys.js';
|
|
19
|
-
import { parseIntent, startChat, adviseStrategy, analyzeToken } from './llm/intent.js';
|
|
19
|
+
import { parseIntent, startChat, adviseStrategy, analyzeToken, executeIntent } from './llm/intent.js';
|
|
20
20
|
import { startAgentSigner, showAgentDocs } from './wallet/agent-signer.js';
|
|
21
21
|
import { listSkills, installSkill, skillInfo, uninstallSkill } from './services/skills.js';
|
|
22
22
|
|
|
@@ -328,6 +328,21 @@ export function cli(argv) {
|
|
|
328
328
|
}
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
+
ai
|
|
332
|
+
.command('execute <prompt...>')
|
|
333
|
+
.description('Parse intent AND execute the trade')
|
|
334
|
+
.option('-p, --provider <name>', 'LLM provider')
|
|
335
|
+
.option('-m, --model <model>', 'Model name')
|
|
336
|
+
.option('--password <pw>', 'Wallet password (for non-interactive)')
|
|
337
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
338
|
+
.action(async (promptParts, opts) => {
|
|
339
|
+
const prompt = promptParts.join(' ');
|
|
340
|
+
const intent = await parseIntent(prompt, opts);
|
|
341
|
+
if (intent.action !== 'error' && intent.action !== 'unknown') {
|
|
342
|
+
await executeIntent(intent, { password: opts.password, yes: opts.yes });
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
331
346
|
ai
|
|
332
347
|
.command('strategy <token>')
|
|
333
348
|
.description('Get DCA strategy recommendation')
|
package/src/llm/intent.js
CHANGED
|
@@ -307,4 +307,130 @@ Provide: sentiment, liquidity assessment, risk level (1-10), key considerations.
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
// ──────────────────────────────────────────────────
|
|
311
|
+
// INTENT EXECUTOR — Wire AI intents into real trades
|
|
312
|
+
// ──────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Execute a parsed intent (from parseIntent or ai ask)
|
|
316
|
+
* Bridges natural language → actual swap/snipe/dca/transfer
|
|
317
|
+
*
|
|
318
|
+
* @param {object} intent - Parsed intent object from parseIntent
|
|
319
|
+
* @param {object} opts - { password, confirm, yes }
|
|
320
|
+
* @returns {Promise<object>} Execution result
|
|
321
|
+
*/
|
|
322
|
+
export async function executeIntent(intent, opts = {}) {
|
|
323
|
+
if (!intent || intent.action === 'unknown' || intent.action === 'error') {
|
|
324
|
+
error('Cannot execute: intent not recognized');
|
|
325
|
+
return { success: false, reason: 'unknown intent' };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (intent.confidence !== undefined && intent.confidence < 0.6) {
|
|
329
|
+
warn(`Low confidence (${(intent.confidence * 100).toFixed(0)}%) — review the intent before executing`);
|
|
330
|
+
if (!opts.yes) {
|
|
331
|
+
const inquirer = (await import('inquirer')).default;
|
|
332
|
+
const { proceed } = await inquirer.prompt([{
|
|
333
|
+
type: 'confirm',
|
|
334
|
+
name: 'proceed',
|
|
335
|
+
message: theme.accent('Execute anyway?'),
|
|
336
|
+
default: false,
|
|
337
|
+
}]);
|
|
338
|
+
if (!proceed) return { success: false, reason: 'user cancelled' };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Show intent summary for confirmation
|
|
343
|
+
if (!opts.yes) {
|
|
344
|
+
showSection('EXECUTE INTENT');
|
|
345
|
+
kvDisplay([
|
|
346
|
+
['Action', intent.action],
|
|
347
|
+
['Token In', intent.tokenIn || '-'],
|
|
348
|
+
['Token Out', intent.tokenOut || '-'],
|
|
349
|
+
['Amount', intent.amount || '-'],
|
|
350
|
+
['Chain', intent.chain || getConfig('chain') || 'base'],
|
|
351
|
+
['Confidence', intent.confidence ? `${(intent.confidence * 100).toFixed(0)}%` : '-'],
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
if (intent.warnings?.length > 0) {
|
|
355
|
+
console.log('');
|
|
356
|
+
intent.warnings.forEach(w => warn(w));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
console.log('');
|
|
360
|
+
const inquirer = (await import('inquirer')).default;
|
|
361
|
+
const { confirm } = await inquirer.prompt([{
|
|
362
|
+
type: 'confirm',
|
|
363
|
+
name: 'confirm',
|
|
364
|
+
message: theme.gold('Execute this trade?'),
|
|
365
|
+
default: false,
|
|
366
|
+
}]);
|
|
367
|
+
if (!confirm) return { success: false, reason: 'user cancelled' };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
switch (intent.action) {
|
|
372
|
+
case 'swap': {
|
|
373
|
+
const { executeSwap } = await import('../trading/swap.js');
|
|
374
|
+
return await executeSwap({
|
|
375
|
+
tokenIn: intent.tokenIn,
|
|
376
|
+
tokenOut: intent.tokenOut,
|
|
377
|
+
amount: intent.amount,
|
|
378
|
+
chain: intent.chain,
|
|
379
|
+
password: opts.password,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
case 'snipe': {
|
|
384
|
+
const { executeSnipe } = await import('../trading/snipe.js');
|
|
385
|
+
return await executeSnipe({
|
|
386
|
+
token: intent.tokenOut || intent.tokenIn,
|
|
387
|
+
amount: intent.amount,
|
|
388
|
+
chain: intent.chain,
|
|
389
|
+
gasMultiplier: intent.gasMultiplier,
|
|
390
|
+
password: opts.password,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
case 'dca': {
|
|
395
|
+
const { createDCAOrder } = await import('../trading/dca.js');
|
|
396
|
+
return await createDCAOrder({
|
|
397
|
+
token: intent.tokenOut || intent.tokenIn,
|
|
398
|
+
amount: intent.amount,
|
|
399
|
+
interval: intent.interval || '1h',
|
|
400
|
+
orders: intent.orders || 10,
|
|
401
|
+
chain: intent.chain,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
case 'transfer': {
|
|
406
|
+
// Transfer intent — use execution script engine
|
|
407
|
+
info(`Transfer intent detected. Use: darksol script create (template: transfer)`);
|
|
408
|
+
info(`To: ${intent.to || 'specify address'}, Amount: ${intent.amount || 'specify amount'}`);
|
|
409
|
+
return { success: true, action: 'transfer', note: 'Use script system for transfers' };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
case 'info':
|
|
413
|
+
case 'analyze': {
|
|
414
|
+
const token = intent.tokenOut || intent.tokenIn || intent.token;
|
|
415
|
+
if (token) {
|
|
416
|
+
await analyzeToken(token, opts);
|
|
417
|
+
return { success: true, action: 'analyze' };
|
|
418
|
+
}
|
|
419
|
+
info('No token specified for analysis');
|
|
420
|
+
return { success: false, reason: 'no token specified' };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
default:
|
|
424
|
+
warn(`Action "${intent.action}" not yet wired for execution`);
|
|
425
|
+
if (intent.command) {
|
|
426
|
+
info(`Suggested command: ${theme.gold(intent.command)}`);
|
|
427
|
+
}
|
|
428
|
+
return { success: false, reason: `unhandled action: ${intent.action}` };
|
|
429
|
+
}
|
|
430
|
+
} catch (err) {
|
|
431
|
+
error(`Execution failed: ${err.message}`);
|
|
432
|
+
return { success: false, error: err.message };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
310
436
|
export { INTENT_SYSTEM_PROMPT };
|
package/src/ui/banner.js
CHANGED
|
@@ -26,7 +26,7 @@ export function showBanner(opts = {}) {
|
|
|
26
26
|
);
|
|
27
27
|
console.log(
|
|
28
28
|
theme.dim(' ║ ') +
|
|
29
|
-
theme.subtle(' v0.2.
|
|
29
|
+
theme.subtle(' v0.2.2') +
|
|
30
30
|
theme.dim(' ') +
|
|
31
31
|
theme.gold('🌑') +
|
|
32
32
|
theme.dim(' ║')
|
|
@@ -44,7 +44,7 @@ export function showBanner(opts = {}) {
|
|
|
44
44
|
|
|
45
45
|
export function showMiniBanner() {
|
|
46
46
|
console.log('');
|
|
47
|
-
console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(' v0.2.
|
|
47
|
+
console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(' v0.2.2'));
|
|
48
48
|
console.log(theme.dim(' ─────────────────────────────'));
|
|
49
49
|
console.log('');
|
|
50
50
|
}
|
|
@@ -61,3 +61,4 @@ export function showDivider() {
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
|
package/tests/cli.test.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import test, { before, after } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
|
|
8
|
-
function runCli(args, env) {
|
|
9
|
-
return spawnSync(process.execPath, ['bin/darksol.js', ...args], {
|
|
10
|
-
cwd: process.cwd(),
|
|
11
|
-
encoding: 'utf8',
|
|
12
|
-
env,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let tempRoot;
|
|
17
|
-
let env;
|
|
18
|
-
|
|
19
|
-
before(() => {
|
|
20
|
-
tempRoot = mkdtempSync(join(tmpdir(), 'darksol-cli-'));
|
|
21
|
-
const home = join(tempRoot, 'home');
|
|
22
|
-
const appData = join(tempRoot, 'appdata');
|
|
23
|
-
const localAppData = join(tempRoot, 'localappdata');
|
|
24
|
-
mkdirSync(home, { recursive: true });
|
|
25
|
-
mkdirSync(appData, { recursive: true });
|
|
26
|
-
mkdirSync(localAppData, { recursive: true });
|
|
27
|
-
|
|
28
|
-
env = {
|
|
29
|
-
...process.env,
|
|
30
|
-
HOME: home,
|
|
31
|
-
USERPROFILE: home,
|
|
32
|
-
APPDATA: appData,
|
|
33
|
-
LOCALAPPDATA: localAppData,
|
|
34
|
-
FORCE_COLOR: '0',
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
after(() => {
|
|
39
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('CLI help output renders', (t) => {
|
|
43
|
-
const res = runCli(['--help'], env);
|
|
44
|
-
if (res.error && res.error.code === 'EPERM') {
|
|
45
|
-
t.skip('Child process spawn not permitted in this environment');
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
assert.equal(res.status, 0, res.stderr);
|
|
49
|
-
assert.match(res.stdout, /Usage:/);
|
|
50
|
-
assert.match(res.stdout, /\bwallet\b/);
|
|
51
|
-
assert.match(res.stdout, /\btrade\b/);
|
|
52
|
-
assert.match(res.stdout, /\bscript\b/);
|
|
53
|
-
assert.match(res.stdout, /\bconfig\b/);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('command registration includes trade and script subcommands', (t) => {
|
|
57
|
-
const tradeHelp = runCli(['trade', '--help'], env);
|
|
58
|
-
if (tradeHelp.error && tradeHelp.error.code === 'EPERM') {
|
|
59
|
-
t.skip('Child process spawn not permitted in this environment');
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
assert.equal(tradeHelp.status, 0, tradeHelp.stderr);
|
|
63
|
-
assert.match(tradeHelp.stdout, /\bswap\b/);
|
|
64
|
-
assert.match(tradeHelp.stdout, /\bsnipe\b/);
|
|
65
|
-
assert.match(tradeHelp.stdout, /\bwatch\b/);
|
|
66
|
-
|
|
67
|
-
const scriptHelp = runCli(['script', '--help'], env);
|
|
68
|
-
assert.equal(scriptHelp.status, 0, scriptHelp.stderr);
|
|
69
|
-
assert.match(scriptHelp.stdout, /\bcreate\b/);
|
|
70
|
-
assert.match(scriptHelp.stdout, /\brun\b/);
|
|
71
|
-
assert.match(scriptHelp.stdout, /\btemplates\b/);
|
|
72
|
-
});
|
package/tests/config.test.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import test, { before, after } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
|
|
7
|
-
function importFresh(relativePath) {
|
|
8
|
-
const url = new URL(`${relativePath}?t=${Date.now()}-${Math.random()}`, import.meta.url);
|
|
9
|
-
return import(url);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function setTempEnv(tempRoot) {
|
|
13
|
-
const home = join(tempRoot, 'home');
|
|
14
|
-
const appData = join(tempRoot, 'appdata');
|
|
15
|
-
const localAppData = join(tempRoot, 'localappdata');
|
|
16
|
-
mkdirSync(home, { recursive: true });
|
|
17
|
-
mkdirSync(appData, { recursive: true });
|
|
18
|
-
mkdirSync(localAppData, { recursive: true });
|
|
19
|
-
|
|
20
|
-
const prev = {
|
|
21
|
-
HOME: process.env.HOME,
|
|
22
|
-
USERPROFILE: process.env.USERPROFILE,
|
|
23
|
-
APPDATA: process.env.APPDATA,
|
|
24
|
-
LOCALAPPDATA: process.env.LOCALAPPDATA,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
process.env.HOME = home;
|
|
28
|
-
process.env.USERPROFILE = home;
|
|
29
|
-
process.env.APPDATA = appData;
|
|
30
|
-
process.env.LOCALAPPDATA = localAppData;
|
|
31
|
-
|
|
32
|
-
return prev;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function restoreEnv(prev) {
|
|
36
|
-
for (const key of Object.keys(prev)) {
|
|
37
|
-
if (prev[key] === undefined) delete process.env[key];
|
|
38
|
-
else process.env[key] = prev[key];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let tempRoot;
|
|
43
|
-
let prevEnv;
|
|
44
|
-
let store;
|
|
45
|
-
|
|
46
|
-
before(async () => {
|
|
47
|
-
tempRoot = mkdtempSync(join(tmpdir(), 'darksol-config-'));
|
|
48
|
-
prevEnv = setTempEnv(tempRoot);
|
|
49
|
-
store = await importFresh('../src/config/store.js');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
after(() => {
|
|
53
|
-
restoreEnv(prevEnv);
|
|
54
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('get/set config values', () => {
|
|
58
|
-
assert.equal(store.getConfig('chain'), 'base');
|
|
59
|
-
store.setConfig('chain', 'ethereum');
|
|
60
|
-
assert.equal(store.getConfig('chain'), 'ethereum');
|
|
61
|
-
const all = store.getAllConfig();
|
|
62
|
-
assert.equal(all.chain, 'ethereum');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('RPC management works for getRPC/setRPC', () => {
|
|
66
|
-
const defaultBaseRpc = store.getRPC('base');
|
|
67
|
-
assert.ok(typeof defaultBaseRpc === 'string' && defaultBaseRpc.startsWith('http'));
|
|
68
|
-
|
|
69
|
-
const custom = 'https://rpc.example.invalid';
|
|
70
|
-
store.setRPC('base', custom);
|
|
71
|
-
assert.equal(store.getRPC('base'), custom);
|
|
72
|
-
|
|
73
|
-
store.setConfig('chain', 'base');
|
|
74
|
-
assert.equal(store.getRPC(), custom);
|
|
75
|
-
});
|
package/tests/dca.test.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import test, { before, after } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import inquirer from 'inquirer';
|
|
7
|
-
|
|
8
|
-
function importFresh(relativePath) {
|
|
9
|
-
const url = new URL(`${relativePath}?t=${Date.now()}-${Math.random()}`, import.meta.url);
|
|
10
|
-
return import(url);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function setTempEnv(tempRoot) {
|
|
14
|
-
const home = join(tempRoot, 'home');
|
|
15
|
-
const appData = join(tempRoot, 'appdata');
|
|
16
|
-
const localAppData = join(tempRoot, 'localappdata');
|
|
17
|
-
mkdirSync(home, { recursive: true });
|
|
18
|
-
mkdirSync(appData, { recursive: true });
|
|
19
|
-
mkdirSync(localAppData, { recursive: true });
|
|
20
|
-
|
|
21
|
-
const prev = {
|
|
22
|
-
HOME: process.env.HOME,
|
|
23
|
-
USERPROFILE: process.env.USERPROFILE,
|
|
24
|
-
APPDATA: process.env.APPDATA,
|
|
25
|
-
LOCALAPPDATA: process.env.LOCALAPPDATA,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
process.env.HOME = home;
|
|
29
|
-
process.env.USERPROFILE = home;
|
|
30
|
-
process.env.APPDATA = appData;
|
|
31
|
-
process.env.LOCALAPPDATA = localAppData;
|
|
32
|
-
|
|
33
|
-
return { prev, home };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function restoreEnv(prev) {
|
|
37
|
-
for (const key of Object.keys(prev)) {
|
|
38
|
-
if (prev[key] === undefined) delete process.env[key];
|
|
39
|
-
else process.env[key] = prev[key];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function withPromptQueue(queue, fn) {
|
|
44
|
-
const original = inquirer.prompt;
|
|
45
|
-
let idx = 0;
|
|
46
|
-
inquirer.prompt = async () => {
|
|
47
|
-
if (idx >= queue.length) {
|
|
48
|
-
throw new Error('Prompt queue exhausted');
|
|
49
|
-
}
|
|
50
|
-
return queue[idx++];
|
|
51
|
-
};
|
|
52
|
-
try {
|
|
53
|
-
await fn();
|
|
54
|
-
} finally {
|
|
55
|
-
inquirer.prompt = original;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let tempRoot;
|
|
60
|
-
let prevEnv;
|
|
61
|
-
let tempHome;
|
|
62
|
-
let dca;
|
|
63
|
-
let store;
|
|
64
|
-
|
|
65
|
-
before(async () => {
|
|
66
|
-
tempRoot = mkdtempSync(join(tmpdir(), 'darksol-dca-'));
|
|
67
|
-
const env = setTempEnv(tempRoot);
|
|
68
|
-
prevEnv = env.prev;
|
|
69
|
-
tempHome = env.home;
|
|
70
|
-
store = await importFresh('../src/config/store.js');
|
|
71
|
-
dca = await importFresh('../src/trading/dca.js');
|
|
72
|
-
store.setConfig('chain', 'base');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
after(() => {
|
|
76
|
-
restoreEnv(prevEnv);
|
|
77
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('DCA create/cancel/run CRUD in temp directory', async () => {
|
|
81
|
-
await withPromptQueue(
|
|
82
|
-
[
|
|
83
|
-
{
|
|
84
|
-
tokenIn: 'ETH',
|
|
85
|
-
tokenOut: 'USDC',
|
|
86
|
-
amountPerOrder: '0.1',
|
|
87
|
-
interval: 3600,
|
|
88
|
-
totalOrders: '2',
|
|
89
|
-
},
|
|
90
|
-
{ confirm: true },
|
|
91
|
-
],
|
|
92
|
-
async () => {
|
|
93
|
-
await dca.createDCA();
|
|
94
|
-
},
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const candidates = [
|
|
98
|
-
join(tempHome, '.darksol', 'dca', 'orders.json'),
|
|
99
|
-
join(process.env.HOME || '', '.darksol', 'dca', 'orders.json'),
|
|
100
|
-
join(process.env.USERPROFILE || '', '.darksol', 'dca', 'orders.json'),
|
|
101
|
-
];
|
|
102
|
-
const ordersPath = candidates.find((p) => p && existsSync(p));
|
|
103
|
-
assert.ok(ordersPath, 'orders.json should exist');
|
|
104
|
-
let orders = JSON.parse(readFileSync(ordersPath, 'utf8'));
|
|
105
|
-
assert.equal(orders.length, 1);
|
|
106
|
-
assert.equal(orders[0].status, 'active');
|
|
107
|
-
|
|
108
|
-
await dca.cancelDCA(orders[0].id);
|
|
109
|
-
orders = JSON.parse(readFileSync(ordersPath, 'utf8'));
|
|
110
|
-
assert.equal(orders[0].status, 'cancelled');
|
|
111
|
-
|
|
112
|
-
await withPromptQueue(
|
|
113
|
-
[
|
|
114
|
-
{
|
|
115
|
-
tokenIn: 'ETH',
|
|
116
|
-
tokenOut: 'USDC',
|
|
117
|
-
amountPerOrder: '0.2',
|
|
118
|
-
interval: 3600,
|
|
119
|
-
totalOrders: '1',
|
|
120
|
-
},
|
|
121
|
-
{ confirm: true },
|
|
122
|
-
],
|
|
123
|
-
async () => {
|
|
124
|
-
await dca.createDCA();
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
orders = JSON.parse(readFileSync(ordersPath, 'utf8'));
|
|
129
|
-
const active = orders.find((o) => o.status === 'active');
|
|
130
|
-
assert.ok(active);
|
|
131
|
-
active.nextExecution = new Date(Date.now() - 1000).toISOString();
|
|
132
|
-
writeFileSync(ordersPath, JSON.stringify(orders, null, 2));
|
|
133
|
-
|
|
134
|
-
await dca.runDCA({ password: 'unused-in-simulated-run' });
|
|
135
|
-
orders = JSON.parse(readFileSync(ordersPath, 'utf8'));
|
|
136
|
-
const ran = orders.find((o) => o.id === active.id);
|
|
137
|
-
assert.equal(ran.executedOrders, 1);
|
|
138
|
-
assert.equal(ran.status, 'completed');
|
|
139
|
-
assert.equal(ran.history.length, 1);
|
|
140
|
-
assert.equal(ran.history[0].status, 'simulated');
|
|
141
|
-
});
|
package/tests/keystore.test.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import test, { before, after } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
|
|
7
|
-
function importFresh(relativePath) {
|
|
8
|
-
const url = new URL(`${relativePath}?t=${Date.now()}-${Math.random()}`, import.meta.url);
|
|
9
|
-
return import(url);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function setTempEnv(tempRoot) {
|
|
13
|
-
const home = join(tempRoot, 'home');
|
|
14
|
-
const appData = join(tempRoot, 'appdata');
|
|
15
|
-
const localAppData = join(tempRoot, 'localappdata');
|
|
16
|
-
mkdirSync(home, { recursive: true });
|
|
17
|
-
mkdirSync(appData, { recursive: true });
|
|
18
|
-
mkdirSync(localAppData, { recursive: true });
|
|
19
|
-
|
|
20
|
-
const prev = {
|
|
21
|
-
HOME: process.env.HOME,
|
|
22
|
-
USERPROFILE: process.env.USERPROFILE,
|
|
23
|
-
APPDATA: process.env.APPDATA,
|
|
24
|
-
LOCALAPPDATA: process.env.LOCALAPPDATA,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
process.env.HOME = home;
|
|
28
|
-
process.env.USERPROFILE = home;
|
|
29
|
-
process.env.APPDATA = appData;
|
|
30
|
-
process.env.LOCALAPPDATA = localAppData;
|
|
31
|
-
|
|
32
|
-
return { prev, home };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function restoreEnv(prev) {
|
|
36
|
-
for (const key of Object.keys(prev)) {
|
|
37
|
-
if (prev[key] === undefined) delete process.env[key];
|
|
38
|
-
else process.env[key] = prev[key];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let tempRoot;
|
|
43
|
-
let prevEnv;
|
|
44
|
-
let keystore;
|
|
45
|
-
|
|
46
|
-
before(async () => {
|
|
47
|
-
tempRoot = mkdtempSync(join(tmpdir(), 'darksol-keystore-'));
|
|
48
|
-
const env = setTempEnv(tempRoot);
|
|
49
|
-
prevEnv = env.prev;
|
|
50
|
-
keystore = await importFresh('../src/wallet/keystore.js');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
after(() => {
|
|
54
|
-
restoreEnv(prevEnv);
|
|
55
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('encrypt/decrypt roundtrip', () => {
|
|
59
|
-
const privateKey = '0x' + '11'.repeat(32);
|
|
60
|
-
const password = 'strong-password-123';
|
|
61
|
-
const encrypted = keystore.encryptKey(privateKey, password);
|
|
62
|
-
const decrypted = keystore.decryptKey(encrypted, password);
|
|
63
|
-
assert.equal(decrypted, privateKey);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('decrypt with wrong password throws', () => {
|
|
67
|
-
const encrypted = keystore.encryptKey('0x' + '22'.repeat(32), 'correct-password');
|
|
68
|
-
assert.throws(() => keystore.decryptKey(encrypted, 'wrong-password'));
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('wallet CRUD in temp home directory', () => {
|
|
72
|
-
const name = 'alice';
|
|
73
|
-
const address = '0x1234567890abcdef1234567890abcdef12345678';
|
|
74
|
-
const encrypted = keystore.encryptKey('0x' + '33'.repeat(32), 'wallet-pass');
|
|
75
|
-
|
|
76
|
-
const walletFile = keystore.saveWallet(name, address, encrypted, { chain: 'base' });
|
|
77
|
-
assert.ok(walletFile.startsWith(keystore.WALLET_DIR));
|
|
78
|
-
assert.equal(keystore.walletExists(name), true);
|
|
79
|
-
|
|
80
|
-
const loaded = keystore.loadWallet(name);
|
|
81
|
-
assert.equal(loaded.name, name);
|
|
82
|
-
assert.equal(loaded.address, address);
|
|
83
|
-
assert.equal(loaded.chain, 'base');
|
|
84
|
-
assert.ok(loaded.createdAt);
|
|
85
|
-
|
|
86
|
-
const wallets = keystore.listWallets();
|
|
87
|
-
assert.equal(wallets.length, 1);
|
|
88
|
-
assert.equal(wallets[0].name, name);
|
|
89
|
-
assert.equal(wallets[0].address, address);
|
|
90
|
-
|
|
91
|
-
keystore.deleteWallet(name);
|
|
92
|
-
assert.equal(keystore.walletExists(name), false);
|
|
93
|
-
assert.throws(() => keystore.loadWallet(name), /not found/i);
|
|
94
|
-
});
|
package/tests/scripts.test.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import test, { before, after } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import inquirer from 'inquirer';
|
|
7
|
-
|
|
8
|
-
function importFresh(relativePath) {
|
|
9
|
-
const url = new URL(`${relativePath}?t=${Date.now()}-${Math.random()}`, import.meta.url);
|
|
10
|
-
return import(url);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function setTempEnv(tempRoot) {
|
|
14
|
-
const home = join(tempRoot, 'home');
|
|
15
|
-
const appData = join(tempRoot, 'appdata');
|
|
16
|
-
const localAppData = join(tempRoot, 'localappdata');
|
|
17
|
-
mkdirSync(home, { recursive: true });
|
|
18
|
-
mkdirSync(appData, { recursive: true });
|
|
19
|
-
mkdirSync(localAppData, { recursive: true });
|
|
20
|
-
|
|
21
|
-
const prev = {
|
|
22
|
-
HOME: process.env.HOME,
|
|
23
|
-
USERPROFILE: process.env.USERPROFILE,
|
|
24
|
-
APPDATA: process.env.APPDATA,
|
|
25
|
-
LOCALAPPDATA: process.env.LOCALAPPDATA,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
process.env.HOME = home;
|
|
29
|
-
process.env.USERPROFILE = home;
|
|
30
|
-
process.env.APPDATA = appData;
|
|
31
|
-
process.env.LOCALAPPDATA = localAppData;
|
|
32
|
-
|
|
33
|
-
return { prev, home };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function restoreEnv(prev) {
|
|
37
|
-
for (const key of Object.keys(prev)) {
|
|
38
|
-
if (prev[key] === undefined) delete process.env[key];
|
|
39
|
-
else process.env[key] = prev[key];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function withPromptQueue(queue, fn) {
|
|
44
|
-
const original = inquirer.prompt;
|
|
45
|
-
let idx = 0;
|
|
46
|
-
inquirer.prompt = async () => {
|
|
47
|
-
if (idx >= queue.length) {
|
|
48
|
-
throw new Error('Prompt queue exhausted');
|
|
49
|
-
}
|
|
50
|
-
return queue[idx++];
|
|
51
|
-
};
|
|
52
|
-
try {
|
|
53
|
-
await fn();
|
|
54
|
-
} finally {
|
|
55
|
-
inquirer.prompt = original;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let tempRoot;
|
|
60
|
-
let prevEnv;
|
|
61
|
-
let tempHome;
|
|
62
|
-
let engine;
|
|
63
|
-
let store;
|
|
64
|
-
|
|
65
|
-
before(async () => {
|
|
66
|
-
tempRoot = mkdtempSync(join(tmpdir(), 'darksol-scripts-'));
|
|
67
|
-
const env = setTempEnv(tempRoot);
|
|
68
|
-
prevEnv = env.prev;
|
|
69
|
-
tempHome = env.home;
|
|
70
|
-
store = await importFresh('../src/config/store.js');
|
|
71
|
-
engine = await importFresh('../src/scripts/engine.js');
|
|
72
|
-
store.setConfig('activeWallet', 'test-wallet');
|
|
73
|
-
store.setConfig('chain', 'base');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
after(() => {
|
|
77
|
-
restoreEnv(prevEnv);
|
|
78
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('script templates have expected structure', () => {
|
|
82
|
-
for (const [key, tmpl] of Object.entries(engine.TEMPLATES)) {
|
|
83
|
-
assert.ok(key.length > 0);
|
|
84
|
-
assert.equal(typeof tmpl.name, 'string');
|
|
85
|
-
assert.equal(typeof tmpl.description, 'string');
|
|
86
|
-
assert.ok(Array.isArray(tmpl.params));
|
|
87
|
-
assert.equal(typeof tmpl.template, 'string');
|
|
88
|
-
assert.match(tmpl.template, /module\.exports\s*=\s*async function/);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('script CRUD works in temp directory', async () => {
|
|
93
|
-
await withPromptQueue(
|
|
94
|
-
[
|
|
95
|
-
{ templateKey: 'empty' },
|
|
96
|
-
{ scriptName: 'alpha' },
|
|
97
|
-
{ walletName: 'test-wallet' },
|
|
98
|
-
],
|
|
99
|
-
async () => {
|
|
100
|
-
await engine.createScript();
|
|
101
|
-
},
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const scriptsDir = join(tempHome, '.darksol', 'scripts');
|
|
105
|
-
const alphaPath = join(scriptsDir, 'alpha.json');
|
|
106
|
-
assert.equal(existsSync(alphaPath), true);
|
|
107
|
-
|
|
108
|
-
const alpha = JSON.parse(readFileSync(alphaPath, 'utf8'));
|
|
109
|
-
assert.equal(alpha.name, 'alpha');
|
|
110
|
-
assert.equal(alpha.template, 'empty');
|
|
111
|
-
assert.equal(alpha.wallet, 'test-wallet');
|
|
112
|
-
assert.equal(alpha.chain, 'base');
|
|
113
|
-
assert.deepEqual(alpha.params, {});
|
|
114
|
-
|
|
115
|
-
await engine.cloneScript('alpha', 'beta');
|
|
116
|
-
const betaPath = join(scriptsDir, 'beta.json');
|
|
117
|
-
assert.equal(existsSync(betaPath), true);
|
|
118
|
-
|
|
119
|
-
await withPromptQueue(
|
|
120
|
-
[
|
|
121
|
-
{ what: 'description' },
|
|
122
|
-
{ desc: 'updated description' },
|
|
123
|
-
],
|
|
124
|
-
async () => {
|
|
125
|
-
await engine.editScript('alpha');
|
|
126
|
-
},
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const edited = JSON.parse(readFileSync(alphaPath, 'utf8'));
|
|
130
|
-
assert.equal(edited.description, 'updated description');
|
|
131
|
-
|
|
132
|
-
await withPromptQueue([{ confirm: true }], async () => {
|
|
133
|
-
await engine.deleteScript('beta');
|
|
134
|
-
});
|
|
135
|
-
assert.equal(existsSync(betaPath), false);
|
|
136
|
-
});
|
package/tests/trading.test.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { ethers } from 'ethers';
|
|
4
|
-
import { resolveToken } from '../src/trading/swap.js';
|
|
5
|
-
|
|
6
|
-
test('resolveToken resolves known symbols', () => {
|
|
7
|
-
assert.equal(resolveToken('ETH', 'base'), ethers.ZeroAddress);
|
|
8
|
-
assert.equal(resolveToken('usdc', 'base'), '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913');
|
|
9
|
-
assert.equal(resolveToken('WETH', 'ethereum'), '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test('resolveToken accepts direct token addresses', () => {
|
|
13
|
-
const address = '0x1234567890abcdef1234567890abcdef12345678';
|
|
14
|
-
assert.equal(resolveToken(address, 'base'), address);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('resolveToken handles unknown symbols and chain fallback', () => {
|
|
18
|
-
assert.equal(resolveToken('NOT_A_TOKEN', 'base'), null);
|
|
19
|
-
assert.equal(resolveToken('USDC', 'unknown-chain'), '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913');
|
|
20
|
-
assert.equal(resolveToken('0xabc', 'base'), null);
|
|
21
|
-
});
|
package/tests/ui.test.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { formatPrice, formatChange, formatAddress } from '../src/ui/components.js';
|
|
4
|
-
|
|
5
|
-
function stripAnsi(value) {
|
|
6
|
-
return String(value).replace(/\x1B\[[0-9;]*m/g, '');
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
test('formatPrice formats values and handles invalid inputs', () => {
|
|
10
|
-
assert.equal(formatPrice(12.3456), '$12.35');
|
|
11
|
-
assert.equal(formatPrice(0.001234), '$0.001234');
|
|
12
|
-
assert.equal(stripAnsi(formatPrice('invalid')), 'N/A');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('formatChange formats positive/negative/zero values', () => {
|
|
16
|
-
assert.equal(stripAnsi(formatChange(2.3456)), '+2.35%');
|
|
17
|
-
assert.equal(stripAnsi(formatChange(-2.3456)), '-2.35%');
|
|
18
|
-
assert.equal(stripAnsi(formatChange(0)), '+0.00%');
|
|
19
|
-
assert.equal(stripAnsi(formatChange('x')), 'N/A');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('formatAddress shortens addresses predictably', () => {
|
|
23
|
-
const addr = '0x1234567890abcdef1234567890abcdef12345678';
|
|
24
|
-
assert.equal(formatAddress(addr), '0x1234...5678');
|
|
25
|
-
assert.equal(formatAddress(addr, 8), '0x123456...5678');
|
|
26
|
-
assert.equal(stripAnsi(formatAddress('')), 'N/A');
|
|
27
|
-
});
|