@darksol/terminal 0.1.0 → 0.2.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 +99 -0
- package/package.json +12 -3
- package/src/cli.js +159 -0
- package/src/config/keys.js +320 -0
- package/src/llm/engine.js +286 -0
- package/src/llm/intent.js +310 -0
- package/src/ui/banner.js +4 -2
- package/src/utils/helpers.js +677 -0
- package/src/wallet/agent-signer.js +556 -0
package/README.md
CHANGED
|
@@ -184,6 +184,105 @@ JSON output mode for programmatic use:
|
|
|
184
184
|
darksol config set output json
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
## Helper Functions
|
|
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
|
+
```
|
|
235
|
+
|
|
236
|
+
### Example: Custom Script Using Helpers
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
module.exports = async function({ signer, provider, ethers, config, params }) {
|
|
240
|
+
// Import helpers (available in script context)
|
|
241
|
+
const helpers = await import('@darksol/terminal/src/utils/helpers.js');
|
|
242
|
+
|
|
243
|
+
// Check if token has enough liquidity
|
|
244
|
+
const liquid = await helpers.hasLiquidity(params.token, 5000);
|
|
245
|
+
if (!liquid) throw new Error('Insufficient liquidity');
|
|
246
|
+
|
|
247
|
+
// Get price
|
|
248
|
+
const price = await helpers.quickPrice(params.token);
|
|
249
|
+
console.log(`Price: ${helpers.formatUSD(price.price)}`);
|
|
250
|
+
|
|
251
|
+
// Get boosted gas for priority
|
|
252
|
+
const gas = await helpers.getBoostedGas(provider, 1.5);
|
|
253
|
+
|
|
254
|
+
// Execute trade with retry
|
|
255
|
+
const result = await helpers.retry(async () => {
|
|
256
|
+
const tx = await signer.sendTransaction({ ...txParams, ...gas });
|
|
257
|
+
return helpers.waitForTx(tx, 60000);
|
|
258
|
+
}, 3, 2000);
|
|
259
|
+
|
|
260
|
+
return { txHash: result.hash, price: price.price };
|
|
261
|
+
};
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Tips & Reference
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# Trading tips (slippage, MEV protection, etc.)
|
|
268
|
+
darksol tips --trading
|
|
269
|
+
|
|
270
|
+
# Script writing tips
|
|
271
|
+
darksol tips --scripts
|
|
272
|
+
|
|
273
|
+
# Both
|
|
274
|
+
darksol tips
|
|
275
|
+
|
|
276
|
+
# Network reference (chains, IDs, explorers, USDC addresses)
|
|
277
|
+
darksol networks
|
|
278
|
+
|
|
279
|
+
# Getting started guide
|
|
280
|
+
darksol quickstart
|
|
281
|
+
|
|
282
|
+
# Look up any address (auto-detects token vs wallet)
|
|
283
|
+
darksol lookup 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
|
|
284
|
+
```
|
|
285
|
+
|
|
187
286
|
## Development
|
|
188
287
|
|
|
189
288
|
```bash
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darksol/terminal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"darksol": "
|
|
7
|
+
"darksol": "bin/darksol.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./src/cli.js",
|
|
10
10
|
"scripts": {
|
|
@@ -12,7 +12,16 @@
|
|
|
12
12
|
"dev": "node bin/darksol.js dashboard",
|
|
13
13
|
"test": "node --test tests/*.test.js"
|
|
14
14
|
},
|
|
15
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"darksol",
|
|
17
|
+
"crypto",
|
|
18
|
+
"trading",
|
|
19
|
+
"cli",
|
|
20
|
+
"x402",
|
|
21
|
+
"base",
|
|
22
|
+
"ethereum",
|
|
23
|
+
"defi"
|
|
24
|
+
],
|
|
16
25
|
"author": "DARKSOL <chris00claw@gmail.com>",
|
|
17
26
|
"license": "MIT",
|
|
18
27
|
"dependencies": {
|
package/src/cli.js
CHANGED
|
@@ -14,6 +14,10 @@ import { cardsCatalog, cardsOrder, cardsStatus } from './services/cards.js';
|
|
|
14
14
|
import { facilitatorHealth, facilitatorVerify, facilitatorSettle } from './services/facilitator.js';
|
|
15
15
|
import { buildersLeaderboard, buildersLookup, buildersFeed } from './services/builders.js';
|
|
16
16
|
import { createScript, listScripts, runScript, showScript, editScript, deleteScript, cloneScript, listTemplates } from './scripts/engine.js';
|
|
17
|
+
import { showTradingTips, showScriptTips, showNetworkReference, showQuickStart, showWalletSummary, showTokenInfo, showTxResult } from './utils/helpers.js';
|
|
18
|
+
import { addKey, removeKey, listKeys } from './config/keys.js';
|
|
19
|
+
import { parseIntent, startChat, adviseStrategy, analyzeToken } from './llm/intent.js';
|
|
20
|
+
import { startAgentSigner, showAgentDocs } from './wallet/agent-signer.js';
|
|
17
21
|
|
|
18
22
|
export function cli(argv) {
|
|
19
23
|
const program = new Command();
|
|
@@ -288,6 +292,154 @@ export function cli(argv) {
|
|
|
288
292
|
.description('Settle payment on-chain')
|
|
289
293
|
.action((payment) => facilitatorSettle(payment));
|
|
290
294
|
|
|
295
|
+
// ═══════════════════════════════════════
|
|
296
|
+
// AI / LLM COMMANDS
|
|
297
|
+
// ═══════════════════════════════════════
|
|
298
|
+
const ai = program
|
|
299
|
+
.command('ai')
|
|
300
|
+
.description('AI-powered trading assistant & analysis');
|
|
301
|
+
|
|
302
|
+
ai
|
|
303
|
+
.command('chat')
|
|
304
|
+
.description('Start interactive AI trading chat')
|
|
305
|
+
.option('-p, --provider <name>', 'LLM provider (openai, anthropic, openrouter, ollama)')
|
|
306
|
+
.option('-m, --model <model>', 'Model name')
|
|
307
|
+
.action((opts) => startChat(opts));
|
|
308
|
+
|
|
309
|
+
ai
|
|
310
|
+
.command('ask <prompt...>')
|
|
311
|
+
.description('One-shot AI query')
|
|
312
|
+
.option('-p, --provider <name>', 'LLM provider')
|
|
313
|
+
.option('-m, --model <model>', 'Model name')
|
|
314
|
+
.action(async (promptParts, opts) => {
|
|
315
|
+
const prompt = promptParts.join(' ');
|
|
316
|
+
const result = await parseIntent(prompt, opts);
|
|
317
|
+
if (result.action !== 'error' && result.action !== 'unknown') {
|
|
318
|
+
showSection('PARSED INTENT');
|
|
319
|
+
kvDisplay(Object.entries(result)
|
|
320
|
+
.filter(([k]) => !['raw', 'model'].includes(k))
|
|
321
|
+
.map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)])
|
|
322
|
+
);
|
|
323
|
+
if (result.command) {
|
|
324
|
+
console.log('');
|
|
325
|
+
info(`Suggested command: ${theme.gold(result.command)}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
ai
|
|
331
|
+
.command('strategy <token>')
|
|
332
|
+
.description('Get DCA strategy recommendation')
|
|
333
|
+
.requiredOption('-b, --budget <usd>', 'Total budget in USD')
|
|
334
|
+
.option('-t, --timeframe <period>', 'Investment timeframe', '30 days')
|
|
335
|
+
.option('-p, --provider <name>', 'LLM provider')
|
|
336
|
+
.action((token, opts) => adviseStrategy(token, opts.budget, opts.timeframe, opts));
|
|
337
|
+
|
|
338
|
+
ai
|
|
339
|
+
.command('analyze <token>')
|
|
340
|
+
.description('AI-powered token analysis')
|
|
341
|
+
.option('-p, --provider <name>', 'LLM provider')
|
|
342
|
+
.action((token, opts) => analyzeToken(token, opts));
|
|
343
|
+
|
|
344
|
+
// ═══════════════════════════════════════
|
|
345
|
+
// API KEYS COMMANDS
|
|
346
|
+
// ═══════════════════════════════════════
|
|
347
|
+
const keys = program
|
|
348
|
+
.command('keys')
|
|
349
|
+
.description('API key vault — store keys for LLMs, data providers, RPCs');
|
|
350
|
+
|
|
351
|
+
keys
|
|
352
|
+
.command('list')
|
|
353
|
+
.description('List all services and stored keys')
|
|
354
|
+
.action(() => listKeys());
|
|
355
|
+
|
|
356
|
+
keys
|
|
357
|
+
.command('add <service>')
|
|
358
|
+
.description('Add or update an API key')
|
|
359
|
+
.option('-k, --key <key>', 'API key (or enter interactively)')
|
|
360
|
+
.action((service, opts) => addKey(service, opts));
|
|
361
|
+
|
|
362
|
+
keys
|
|
363
|
+
.command('remove <service>')
|
|
364
|
+
.description('Remove a stored key')
|
|
365
|
+
.action((service) => removeKey(service));
|
|
366
|
+
|
|
367
|
+
// ═══════════════════════════════════════
|
|
368
|
+
// AGENT SIGNER COMMANDS
|
|
369
|
+
// ═══════════════════════════════════════
|
|
370
|
+
const agent = program
|
|
371
|
+
.command('agent')
|
|
372
|
+
.description('Secure agent signer — PK-isolated wallet for AI agents');
|
|
373
|
+
|
|
374
|
+
agent
|
|
375
|
+
.command('start [wallet]')
|
|
376
|
+
.description('Start the agent signing proxy')
|
|
377
|
+
.option('--port <port>', 'Server port', '18790')
|
|
378
|
+
.option('--max-value <eth>', 'Max ETH per transaction', '1.0')
|
|
379
|
+
.option('--daily-limit <eth>', 'Daily spending limit in ETH', '5.0')
|
|
380
|
+
.option('--allowlist <contracts>', 'Comma-separated contract allowlist')
|
|
381
|
+
.action((wallet, opts) => startAgentSigner(wallet, opts));
|
|
382
|
+
|
|
383
|
+
agent
|
|
384
|
+
.command('docs')
|
|
385
|
+
.description('Show agent signer security documentation')
|
|
386
|
+
.action(() => showAgentDocs());
|
|
387
|
+
|
|
388
|
+
// ═══════════════════════════════════════
|
|
389
|
+
// TIPS & REFERENCE COMMANDS
|
|
390
|
+
// ═══════════════════════════════════════
|
|
391
|
+
program
|
|
392
|
+
.command('tips')
|
|
393
|
+
.description('Show trading and script writing tips')
|
|
394
|
+
.option('-t, --trading', 'Trading tips only')
|
|
395
|
+
.option('-s, --scripts', 'Script writing tips only')
|
|
396
|
+
.action((opts) => {
|
|
397
|
+
showMiniBanner();
|
|
398
|
+
if (opts.scripts) {
|
|
399
|
+
showScriptTips();
|
|
400
|
+
} else if (opts.trading) {
|
|
401
|
+
showTradingTips();
|
|
402
|
+
} else {
|
|
403
|
+
showTradingTips();
|
|
404
|
+
showScriptTips();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
program
|
|
409
|
+
.command('networks')
|
|
410
|
+
.description('Show supported networks and chain info')
|
|
411
|
+
.action(() => {
|
|
412
|
+
showMiniBanner();
|
|
413
|
+
showNetworkReference();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
program
|
|
417
|
+
.command('quickstart')
|
|
418
|
+
.description('Show getting started guide')
|
|
419
|
+
.action(() => {
|
|
420
|
+
showMiniBanner();
|
|
421
|
+
showQuickStart();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
program
|
|
425
|
+
.command('lookup <address>')
|
|
426
|
+
.description('Look up a token or wallet address on-chain')
|
|
427
|
+
.option('-c, --chain <chain>', 'Chain to query')
|
|
428
|
+
.action(async (address, opts) => {
|
|
429
|
+
showMiniBanner();
|
|
430
|
+
if (address.length === 42 && address.startsWith('0x')) {
|
|
431
|
+
// Could be token or wallet — try token first
|
|
432
|
+
try {
|
|
433
|
+
await showTokenInfo(address, opts.chain);
|
|
434
|
+
} catch {
|
|
435
|
+
await showWalletSummary(address, opts.chain);
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
const { error } = await import('./ui/components.js');
|
|
439
|
+
error('Provide a valid 0x address');
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
291
443
|
// ═══════════════════════════════════════
|
|
292
444
|
// SCRIPT COMMANDS
|
|
293
445
|
// ═══════════════════════════════════════
|
|
@@ -410,6 +562,9 @@ export function cli(argv) {
|
|
|
410
562
|
['wallet', 'Create, import, manage wallets'],
|
|
411
563
|
['trade', 'Swap tokens, snipe, trading'],
|
|
412
564
|
['dca', 'Dollar-cost averaging orders'],
|
|
565
|
+
['ai', 'AI trading assistant & analysis'],
|
|
566
|
+
['agent', 'Secure agent signer (PK-isolated)'],
|
|
567
|
+
['keys', 'API key vault (LLMs, data, RPCs)'],
|
|
413
568
|
['script', 'Execution scripts & strategies'],
|
|
414
569
|
['market', 'Market intel & token data'],
|
|
415
570
|
['oracle', 'On-chain random oracle'],
|
|
@@ -418,6 +573,10 @@ export function cli(argv) {
|
|
|
418
573
|
['builders', 'ERC-8021 builder index'],
|
|
419
574
|
['facilitator', 'x402 payment facilitator'],
|
|
420
575
|
['config', 'Terminal configuration'],
|
|
576
|
+
['tips', 'Trading & scripting tips'],
|
|
577
|
+
['networks', 'Chain reference & explorers'],
|
|
578
|
+
['quickstart', 'Getting started guide'],
|
|
579
|
+
['lookup', 'Look up any address on-chain'],
|
|
421
580
|
];
|
|
422
581
|
|
|
423
582
|
commands.forEach(([cmd, desc]) => {
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { theme } from '../ui/theme.js';
|
|
6
|
+
import { kvDisplay, success, error, warn, info } from '../ui/components.js';
|
|
7
|
+
import { showSection } from '../ui/banner.js';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
9
|
+
|
|
10
|
+
const KEYS_DIR = join(homedir(), '.darksol', 'keys');
|
|
11
|
+
const KEYS_FILE = join(KEYS_DIR, 'vault.json');
|
|
12
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
13
|
+
const SCRYPT_N = 2 ** 16; // lighter for keys (faster unlock)
|
|
14
|
+
const SCRYPT_r = 8;
|
|
15
|
+
const SCRYPT_p = 1;
|
|
16
|
+
const SCRYPT_MAXMEM = 512 * 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
function ensureDir() {
|
|
19
|
+
if (!existsSync(KEYS_DIR)) mkdirSync(KEYS_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function encrypt(value, password) {
|
|
23
|
+
const salt = randomBytes(32);
|
|
24
|
+
const iv = randomBytes(16);
|
|
25
|
+
const key = scryptSync(password, salt, 32, { N: SCRYPT_N, r: SCRYPT_r, p: SCRYPT_p, maxmem: SCRYPT_MAXMEM });
|
|
26
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
27
|
+
let encrypted = cipher.update(value, 'utf8', 'hex');
|
|
28
|
+
encrypted += cipher.final('hex');
|
|
29
|
+
return {
|
|
30
|
+
salt: salt.toString('hex'),
|
|
31
|
+
iv: iv.toString('hex'),
|
|
32
|
+
tag: cipher.getAuthTag().toString('hex'),
|
|
33
|
+
data: encrypted,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function decrypt(entry, password) {
|
|
38
|
+
const key = scryptSync(password, Buffer.from(entry.salt, 'hex'), 32, { N: SCRYPT_N, r: SCRYPT_r, p: SCRYPT_p, maxmem: SCRYPT_MAXMEM });
|
|
39
|
+
const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(entry.iv, 'hex'));
|
|
40
|
+
decipher.setAuthTag(Buffer.from(entry.tag, 'hex'));
|
|
41
|
+
let decrypted = decipher.update(entry.data, 'hex', 'utf8');
|
|
42
|
+
decrypted += decipher.final('utf8');
|
|
43
|
+
return decrypted;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadVault() {
|
|
47
|
+
ensureDir();
|
|
48
|
+
if (!existsSync(KEYS_FILE)) return { version: 1, keys: {} };
|
|
49
|
+
return JSON.parse(readFileSync(KEYS_FILE, 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function saveVault(vault) {
|
|
53
|
+
ensureDir();
|
|
54
|
+
writeFileSync(KEYS_FILE, JSON.stringify(vault, null, 2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ──────────────────────────────────────────────────
|
|
58
|
+
// SUPPORTED API SERVICES
|
|
59
|
+
// ──────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export const SERVICES = {
|
|
62
|
+
// LLM Providers
|
|
63
|
+
openai: {
|
|
64
|
+
name: 'OpenAI',
|
|
65
|
+
category: 'llm',
|
|
66
|
+
description: 'GPT-4o, GPT-5 — natural language trading, strategy advisor',
|
|
67
|
+
envVar: 'OPENAI_API_KEY',
|
|
68
|
+
docsUrl: 'https://platform.openai.com/api-keys',
|
|
69
|
+
validate: (key) => key.startsWith('sk-'),
|
|
70
|
+
},
|
|
71
|
+
anthropic: {
|
|
72
|
+
name: 'Anthropic',
|
|
73
|
+
category: 'llm',
|
|
74
|
+
description: 'Claude Opus, Sonnet — intent parsing, analysis',
|
|
75
|
+
envVar: 'ANTHROPIC_API_KEY',
|
|
76
|
+
docsUrl: 'https://console.anthropic.com/settings/keys',
|
|
77
|
+
validate: (key) => key.startsWith('sk-ant-'),
|
|
78
|
+
},
|
|
79
|
+
openrouter: {
|
|
80
|
+
name: 'OpenRouter',
|
|
81
|
+
category: 'llm',
|
|
82
|
+
description: 'Multi-model gateway — any LLM via one key',
|
|
83
|
+
envVar: 'OPENROUTER_API_KEY',
|
|
84
|
+
docsUrl: 'https://openrouter.ai/keys',
|
|
85
|
+
validate: (key) => key.startsWith('sk-or-'),
|
|
86
|
+
},
|
|
87
|
+
ollama: {
|
|
88
|
+
name: 'Ollama (Local)',
|
|
89
|
+
category: 'llm',
|
|
90
|
+
description: 'Local models — free, private, no API key needed',
|
|
91
|
+
envVar: 'OLLAMA_HOST',
|
|
92
|
+
docsUrl: 'https://ollama.ai',
|
|
93
|
+
validate: (key) => key.startsWith('http'),
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// Data Providers
|
|
97
|
+
coingecko: {
|
|
98
|
+
name: 'CoinGecko Pro',
|
|
99
|
+
category: 'data',
|
|
100
|
+
description: 'Pro/Demo API — higher rate limits, more endpoints',
|
|
101
|
+
envVar: 'COINGECKO_API_KEY',
|
|
102
|
+
docsUrl: 'https://www.coingecko.com/en/api/pricing',
|
|
103
|
+
validate: (key) => key.length > 10,
|
|
104
|
+
},
|
|
105
|
+
dexscreener: {
|
|
106
|
+
name: 'DexScreener',
|
|
107
|
+
category: 'data',
|
|
108
|
+
description: 'Enhanced DEX data — paid tier for higher limits',
|
|
109
|
+
envVar: 'DEXSCREENER_API_KEY',
|
|
110
|
+
docsUrl: 'https://docs.dexscreener.com',
|
|
111
|
+
validate: (key) => key.length > 10,
|
|
112
|
+
},
|
|
113
|
+
defillama: {
|
|
114
|
+
name: 'DefiLlama',
|
|
115
|
+
category: 'data',
|
|
116
|
+
description: 'TVL, yield, protocol data — free, no key needed',
|
|
117
|
+
envVar: null,
|
|
118
|
+
docsUrl: 'https://defillama.com/docs/api',
|
|
119
|
+
validate: () => true,
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// RPC Providers (OAuth/API key)
|
|
123
|
+
alchemy: {
|
|
124
|
+
name: 'Alchemy',
|
|
125
|
+
category: 'rpc',
|
|
126
|
+
description: 'Premium RPC — faster, more reliable, trace APIs',
|
|
127
|
+
envVar: 'ALCHEMY_API_KEY',
|
|
128
|
+
docsUrl: 'https://dashboard.alchemy.com',
|
|
129
|
+
validate: (key) => key.length > 10,
|
|
130
|
+
},
|
|
131
|
+
infura: {
|
|
132
|
+
name: 'Infura',
|
|
133
|
+
category: 'rpc',
|
|
134
|
+
description: 'RPC provider — Ethereum, Polygon, Arbitrum, Optimism',
|
|
135
|
+
envVar: 'INFURA_API_KEY',
|
|
136
|
+
docsUrl: 'https://app.infura.io',
|
|
137
|
+
validate: (key) => key.length > 10,
|
|
138
|
+
},
|
|
139
|
+
quicknode: {
|
|
140
|
+
name: 'QuickNode',
|
|
141
|
+
category: 'rpc',
|
|
142
|
+
description: 'High-performance RPC — WebSocket support, trace',
|
|
143
|
+
envVar: 'QUICKNODE_API_KEY',
|
|
144
|
+
docsUrl: 'https://dashboard.quicknode.com',
|
|
145
|
+
validate: (key) => key.length > 10,
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// Trading & Auth
|
|
149
|
+
oneinch: {
|
|
150
|
+
name: '1inch',
|
|
151
|
+
category: 'trading',
|
|
152
|
+
description: 'DEX aggregator API — best swap routing',
|
|
153
|
+
envVar: 'ONEINCH_API_KEY',
|
|
154
|
+
docsUrl: 'https://portal.1inch.dev',
|
|
155
|
+
validate: (key) => key.length > 10,
|
|
156
|
+
},
|
|
157
|
+
paraswap: {
|
|
158
|
+
name: 'ParaSwap',
|
|
159
|
+
category: 'trading',
|
|
160
|
+
description: 'DEX aggregator — competitive routing',
|
|
161
|
+
envVar: 'PARASWAP_API_KEY',
|
|
162
|
+
docsUrl: 'https://developers.paraswap.network',
|
|
163
|
+
validate: (key) => key.length > 5,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ──────────────────────────────────────────────────
|
|
168
|
+
// KEY MANAGEMENT
|
|
169
|
+
// ──────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Add or update an API key
|
|
173
|
+
*/
|
|
174
|
+
export async function addKey(service, opts = {}) {
|
|
175
|
+
const svc = SERVICES[service];
|
|
176
|
+
if (!svc) {
|
|
177
|
+
error(`Unknown service: ${service}. Run: darksol keys list`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let apiKey = opts.key;
|
|
182
|
+
if (!apiKey) {
|
|
183
|
+
const { key } = await inquirer.prompt([{
|
|
184
|
+
type: 'password',
|
|
185
|
+
name: 'key',
|
|
186
|
+
message: theme.gold(`${svc.name} API key:`),
|
|
187
|
+
mask: '●',
|
|
188
|
+
validate: (v) => {
|
|
189
|
+
if (!v) return 'Key required';
|
|
190
|
+
if (svc.validate && !svc.validate(v)) return `Invalid format for ${svc.name}`;
|
|
191
|
+
return true;
|
|
192
|
+
},
|
|
193
|
+
}]);
|
|
194
|
+
apiKey = key;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Get vault password
|
|
198
|
+
let vaultPass = opts.password;
|
|
199
|
+
if (!vaultPass) {
|
|
200
|
+
const { password } = await inquirer.prompt([{
|
|
201
|
+
type: 'password',
|
|
202
|
+
name: 'password',
|
|
203
|
+
message: theme.gold('Vault password:'),
|
|
204
|
+
mask: '●',
|
|
205
|
+
validate: (v) => v.length >= 6 || 'Minimum 6 characters',
|
|
206
|
+
}]);
|
|
207
|
+
vaultPass = password;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const vault = loadVault();
|
|
211
|
+
vault.keys[service] = {
|
|
212
|
+
encrypted: encrypt(apiKey, vaultPass),
|
|
213
|
+
service: svc.name,
|
|
214
|
+
category: svc.category,
|
|
215
|
+
addedAt: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
saveVault(vault);
|
|
218
|
+
|
|
219
|
+
success(`${svc.name} key stored securely`);
|
|
220
|
+
if (svc.envVar) {
|
|
221
|
+
info(`Also available via env: ${svc.envVar}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get a decrypted API key
|
|
227
|
+
*/
|
|
228
|
+
export async function getKey(service, password) {
|
|
229
|
+
const vault = loadVault();
|
|
230
|
+
const entry = vault.keys[service];
|
|
231
|
+
|
|
232
|
+
if (!entry) {
|
|
233
|
+
// Fall back to environment variable
|
|
234
|
+
const svc = SERVICES[service];
|
|
235
|
+
if (svc?.envVar && process.env[svc.envVar]) {
|
|
236
|
+
return process.env[svc.envVar];
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
return decrypt(entry.encrypted, password);
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get a key without password (tries env var first, then cached session)
|
|
250
|
+
*/
|
|
251
|
+
export function getKeyFromEnv(service) {
|
|
252
|
+
const svc = SERVICES[service];
|
|
253
|
+
if (svc?.envVar && process.env[svc.envVar]) {
|
|
254
|
+
return process.env[svc.envVar];
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Remove a key
|
|
261
|
+
*/
|
|
262
|
+
export async function removeKey(service) {
|
|
263
|
+
const vault = loadVault();
|
|
264
|
+
if (!vault.keys[service]) {
|
|
265
|
+
error(`No key stored for: ${service}`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const svc = SERVICES[service];
|
|
269
|
+
const { confirm } = await inquirer.prompt([{
|
|
270
|
+
type: 'confirm',
|
|
271
|
+
name: 'confirm',
|
|
272
|
+
message: theme.accent(`Remove ${svc?.name || service} key?`),
|
|
273
|
+
default: false,
|
|
274
|
+
}]);
|
|
275
|
+
if (!confirm) return;
|
|
276
|
+
|
|
277
|
+
delete vault.keys[service];
|
|
278
|
+
saveVault(vault);
|
|
279
|
+
success(`${svc?.name || service} key removed`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* List all services and stored keys
|
|
284
|
+
*/
|
|
285
|
+
export function listKeys() {
|
|
286
|
+
const vault = loadVault();
|
|
287
|
+
|
|
288
|
+
showSection('API KEY VAULT');
|
|
289
|
+
|
|
290
|
+
const categories = ['llm', 'data', 'rpc', 'trading'];
|
|
291
|
+
const catNames = { llm: '🧠 LLM PROVIDERS', data: '📊 DATA PROVIDERS', rpc: '🌐 RPC PROVIDERS', trading: '📈 TRADING' };
|
|
292
|
+
|
|
293
|
+
for (const cat of categories) {
|
|
294
|
+
console.log('');
|
|
295
|
+
console.log(theme.gold(` ${catNames[cat]}`));
|
|
296
|
+
|
|
297
|
+
const services = Object.entries(SERVICES).filter(([, s]) => s.category === cat);
|
|
298
|
+
for (const [key, svc] of services) {
|
|
299
|
+
const stored = vault.keys[key];
|
|
300
|
+
const envKey = svc.envVar ? getKeyFromEnv(key) : null;
|
|
301
|
+
let status;
|
|
302
|
+
|
|
303
|
+
if (stored) {
|
|
304
|
+
status = theme.success('● Stored');
|
|
305
|
+
} else if (envKey) {
|
|
306
|
+
status = theme.info('● Env');
|
|
307
|
+
} else {
|
|
308
|
+
status = theme.dim('○ Not set');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log(` ${status} ${theme.label(svc.name.padEnd(18))} ${theme.dim(svc.description)}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log('');
|
|
316
|
+
info('Add a key: darksol keys add <service>');
|
|
317
|
+
info('Services: ' + Object.keys(SERVICES).join(', '));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export { KEYS_DIR, KEYS_FILE };
|