@darksol/terminal 0.4.3 → 0.4.5
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/package.json +1 -1
- package/src/cli.js +76 -6
- package/src/llm/intent.js +141 -27
- package/src/wallet/manager.js +264 -0
- package/src/web/commands.js +82 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { showBanner, showMiniBanner, showSection } from './ui/banner.js';
|
|
|
3
3
|
import { theme } from './ui/theme.js';
|
|
4
4
|
import { kvDisplay, success, error, warn, info } from './ui/components.js';
|
|
5
5
|
import { getConfig, setConfig, getAllConfig, getRPC, setRPC, configPath } from './config/store.js';
|
|
6
|
-
import { createWallet, importWallet, showWallets, getBalance, useWallet, exportWallet } from './wallet/manager.js';
|
|
6
|
+
import { createWallet, importWallet, showWallets, getBalance, useWallet, exportWallet, sendFunds, receiveAddress } from './wallet/manager.js';
|
|
7
7
|
import { showPortfolio } from './wallet/portfolio.js';
|
|
8
8
|
import { showHistory } from './wallet/history.js';
|
|
9
9
|
import { showGas } from './services/gas.js';
|
|
@@ -73,6 +73,20 @@ export function cli(argv) {
|
|
|
73
73
|
.description('Set active wallet')
|
|
74
74
|
.action((name) => useWallet(name));
|
|
75
75
|
|
|
76
|
+
wallet
|
|
77
|
+
.command('send')
|
|
78
|
+
.description('Send ETH or tokens')
|
|
79
|
+
.option('--to <address>', 'Recipient address')
|
|
80
|
+
.option('-a, --amount <amount>', 'Amount to send')
|
|
81
|
+
.option('-t, --token <token>', 'Token (ETH, USDC, or 0x address)', 'ETH')
|
|
82
|
+
.option('-w, --wallet <name>', 'Wallet to send from')
|
|
83
|
+
.action((opts) => sendFunds(opts));
|
|
84
|
+
|
|
85
|
+
wallet
|
|
86
|
+
.command('receive [name]')
|
|
87
|
+
.description('Show your address for receiving funds')
|
|
88
|
+
.action((name) => receiveAddress(name));
|
|
89
|
+
|
|
76
90
|
wallet
|
|
77
91
|
.command('export [name]')
|
|
78
92
|
.description('Export wallet details')
|
|
@@ -420,6 +434,25 @@ export function cli(argv) {
|
|
|
420
434
|
.description('Multi-chain balance view (shortcut for: wallet portfolio)')
|
|
421
435
|
.action((name) => showPortfolio(name));
|
|
422
436
|
|
|
437
|
+
// ═══════════════════════════════════════
|
|
438
|
+
// SEND SHORTCUT
|
|
439
|
+
// ═══════════════════════════════════════
|
|
440
|
+
program
|
|
441
|
+
.command('send')
|
|
442
|
+
.description('Send ETH or tokens (shortcut for: wallet send)')
|
|
443
|
+
.option('--to <address>', 'Recipient address')
|
|
444
|
+
.option('-a, --amount <amount>', 'Amount')
|
|
445
|
+
.option('-t, --token <token>', 'Token (ETH, USDC, or 0x address)', 'ETH')
|
|
446
|
+
.action((opts) => sendFunds(opts));
|
|
447
|
+
|
|
448
|
+
// ═══════════════════════════════════════
|
|
449
|
+
// RECEIVE SHORTCUT
|
|
450
|
+
// ═══════════════════════════════════════
|
|
451
|
+
program
|
|
452
|
+
.command('receive')
|
|
453
|
+
.description('Show your address for receiving (shortcut for: wallet receive)')
|
|
454
|
+
.action(() => receiveAddress());
|
|
455
|
+
|
|
423
456
|
// ═══════════════════════════════════════
|
|
424
457
|
// GAS COMMAND
|
|
425
458
|
// ═══════════════════════════════════════
|
|
@@ -483,18 +516,53 @@ export function cli(argv) {
|
|
|
483
516
|
.description('One-shot AI query')
|
|
484
517
|
.option('-p, --provider <name>', 'LLM provider')
|
|
485
518
|
.option('-m, --model <model>', 'Model name')
|
|
519
|
+
.option('-x, --execute', 'Auto-execute if confidence > 60%')
|
|
486
520
|
.action(async (promptParts, opts) => {
|
|
487
521
|
const prompt = promptParts.join(' ');
|
|
488
522
|
const result = await parseIntent(prompt, opts);
|
|
489
523
|
if (result.action !== 'error' && result.action !== 'unknown') {
|
|
490
524
|
showSection('PARSED INTENT');
|
|
491
|
-
|
|
492
|
-
.filter(([k]) => !['raw', 'model'].includes(k))
|
|
493
|
-
.map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)])
|
|
494
|
-
);
|
|
525
|
+
const displayEntries = Object.entries(result)
|
|
526
|
+
.filter(([k]) => !['raw', 'model', 'reasoning'].includes(k))
|
|
527
|
+
.map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)]);
|
|
528
|
+
kvDisplay(displayEntries);
|
|
529
|
+
|
|
530
|
+
if (result.reasoning) {
|
|
531
|
+
console.log('');
|
|
532
|
+
info(result.reasoning);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (result.warnings?.length > 0) {
|
|
536
|
+
result.warnings.forEach(w => warn(w));
|
|
537
|
+
}
|
|
538
|
+
|
|
495
539
|
if (result.command) {
|
|
496
540
|
console.log('');
|
|
497
|
-
info(`
|
|
541
|
+
info(`Command: ${theme.gold(result.command)}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Offer to execute actionable intents
|
|
545
|
+
const actionable = ['swap', 'send', 'transfer', 'snipe', 'dca', 'price', 'balance', 'gas', 'analyze'];
|
|
546
|
+
if (actionable.includes(result.action)) {
|
|
547
|
+
if (opts.execute && result.confidence >= 0.6) {
|
|
548
|
+
console.log('');
|
|
549
|
+
await executeIntent(result, {});
|
|
550
|
+
} else if (!opts.execute) {
|
|
551
|
+
console.log('');
|
|
552
|
+
const inquirer = (await import('inquirer')).default;
|
|
553
|
+
const { run } = await inquirer.prompt([{
|
|
554
|
+
type: 'confirm',
|
|
555
|
+
name: 'run',
|
|
556
|
+
message: theme.gold('Execute this?'),
|
|
557
|
+
default: result.confidence >= 0.7,
|
|
558
|
+
}]);
|
|
559
|
+
if (run) await executeIntent(result, {});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
if (result.raw) {
|
|
564
|
+
console.log('');
|
|
565
|
+
console.log(theme.dim(' ') + result.raw);
|
|
498
566
|
}
|
|
499
567
|
}
|
|
500
568
|
});
|
|
@@ -965,6 +1033,8 @@ function showCommandList() {
|
|
|
965
1033
|
showSection('COMMANDS');
|
|
966
1034
|
const commands = [
|
|
967
1035
|
['wallet', 'Create, import, manage wallets'],
|
|
1036
|
+
['send', 'Send ETH or tokens'],
|
|
1037
|
+
['receive', 'Show address to receive funds'],
|
|
968
1038
|
['portfolio', 'Multi-chain balance view'],
|
|
969
1039
|
['price', 'Quick token price check'],
|
|
970
1040
|
['watch', 'Live price monitoring + alerts'],
|
package/src/llm/intent.js
CHANGED
|
@@ -9,42 +9,70 @@ import { showSection } from '../ui/banner.js';
|
|
|
9
9
|
// INTENT SYSTEM PROMPT
|
|
10
10
|
// ──────────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
-
const INTENT_SYSTEM_PROMPT = `You are DARKSOL Terminal's trading AI assistant. You help users execute trades, analyze markets, manage DCA strategies, and navigate the DARKSOL ecosystem.
|
|
12
|
+
const INTENT_SYSTEM_PROMPT = `You are DARKSOL Terminal's trading AI assistant. You help users execute trades, send/receive tokens, analyze markets, manage DCA strategies, and navigate the DARKSOL ecosystem.
|
|
13
13
|
|
|
14
14
|
CAPABILITIES:
|
|
15
|
-
- Parse natural language
|
|
16
|
-
- Analyze token prices, liquidity, and market conditions
|
|
15
|
+
- Parse natural language into structured trade/transfer commands
|
|
16
|
+
- Analyze token prices, liquidity, and market conditions
|
|
17
17
|
- Suggest DCA strategies based on user goals
|
|
18
18
|
- Explain transaction results and gas costs
|
|
19
19
|
- Warn about risks (low liquidity, high slippage, unverified contracts)
|
|
20
20
|
|
|
21
21
|
SUPPORTED CHAINS: Base (default), Ethereum, Polygon, Arbitrum, Optimism
|
|
22
|
-
|
|
23
|
-
RESPONSE RULES:
|
|
24
|
-
- Be concise and direct
|
|
25
|
-
- Always include risk warnings for trades
|
|
26
|
-
- When parsing trade intent, output structured JSON
|
|
27
|
-
- Never reveal private keys or sensitive wallet info
|
|
28
|
-
- If uncertain about a token, say so
|
|
29
|
-
- Use plain numbers, avoid scientific notation
|
|
22
|
+
KNOWN TOKENS: ETH, USDC, USDT, DAI, WETH, AERO, VIRTUAL, ARB, OP, WMATIC
|
|
30
23
|
|
|
31
24
|
USER CONTEXT:
|
|
32
25
|
- Active chain: {{chain}}
|
|
33
26
|
- Active wallet: {{wallet}}
|
|
34
27
|
- Slippage setting: {{slippage}}%
|
|
35
28
|
|
|
36
|
-
|
|
29
|
+
RESPONSE RULES:
|
|
30
|
+
- Be concise and direct
|
|
31
|
+
- Always include risk warnings for trades
|
|
32
|
+
- When parsing trade/transfer intent, output structured JSON
|
|
33
|
+
- Never reveal private keys or sensitive wallet info
|
|
34
|
+
- If uncertain about a token, say so — don't guess contract addresses
|
|
35
|
+
- Use plain numbers, avoid scientific notation
|
|
36
|
+
- For ambiguous amounts, ask for clarification (confidence < 0.5)
|
|
37
|
+
|
|
38
|
+
ACTIONS (use the most specific one):
|
|
39
|
+
- "swap" — exchange one token for another (e.g. "swap ETH to USDC", "buy VIRTUAL with 0.1 ETH")
|
|
40
|
+
- "send" — transfer tokens to an address (e.g. "send 10 USDC to 0x...", "transfer 0.5 ETH to vitalik.eth")
|
|
41
|
+
- "snipe" — fast-buy a new/low-liquidity token with ETH
|
|
42
|
+
- "dca" — set up recurring buys (e.g. "DCA $100 into ETH over 30 days")
|
|
43
|
+
- "price" — check current price (e.g. "price of AERO", "how much is VIRTUAL")
|
|
44
|
+
- "balance" — check wallet balance
|
|
45
|
+
- "info" — general question about a token or protocol
|
|
46
|
+
- "analyze" — deep analysis of a token
|
|
47
|
+
- "gas" — gas price check
|
|
48
|
+
- "unknown" — can't determine what the user wants
|
|
49
|
+
|
|
50
|
+
When parsing, respond with ONLY valid JSON:
|
|
37
51
|
{
|
|
38
|
-
"action": "swap|snipe|dca|
|
|
39
|
-
"tokenIn": "symbol or address",
|
|
40
|
-
"tokenOut": "symbol or address",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
52
|
+
"action": "swap|send|snipe|dca|price|balance|info|analyze|gas|unknown",
|
|
53
|
+
"tokenIn": "symbol or address (for swaps)",
|
|
54
|
+
"tokenOut": "symbol or address (for swaps)",
|
|
55
|
+
"token": "symbol (for send/price/analyze)",
|
|
56
|
+
"amount": "number as string",
|
|
57
|
+
"to": "recipient address (for send)",
|
|
58
|
+
"chain": "chain name if specified, null if not",
|
|
59
|
+
"interval": "for DCA: 1h, 4h, 1d, etc.",
|
|
60
|
+
"orders": "for DCA: number of orders",
|
|
61
|
+
"confidence": 0.0-1.0,
|
|
62
|
+
"reasoning": "brief explanation of interpretation",
|
|
45
63
|
"warnings": ["array of risk warnings"],
|
|
46
|
-
"command": "the CLI command to
|
|
47
|
-
}
|
|
64
|
+
"command": "the exact darksol CLI command to run"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
COMMAND MAPPING:
|
|
68
|
+
- swap → darksol trade swap -i <tokenIn> -o <tokenOut> -a <amount>
|
|
69
|
+
- send → darksol send --to <address> --amount <amount> --token <token>
|
|
70
|
+
- snipe → darksol trade snipe <address> <ethAmount>
|
|
71
|
+
- dca → darksol dca create -t <token> -a <amount> -i <interval> -n <orders>
|
|
72
|
+
- price → darksol price <token>
|
|
73
|
+
- balance → darksol wallet balance
|
|
74
|
+
- gas → darksol gas <chain>
|
|
75
|
+
- analyze → darksol ai analyze <token>`;
|
|
48
76
|
|
|
49
77
|
// ──────────────────────────────────────────────────
|
|
50
78
|
// INTENT PARSER
|
|
@@ -126,7 +154,8 @@ export async function parseIntent(input, opts = {}) {
|
|
|
126
154
|
export async function startChat(opts = {}) {
|
|
127
155
|
showSection('DARKSOL AI — TRADING ASSISTANT');
|
|
128
156
|
console.log(theme.dim(' Natural language trading. Type "exit" to quit.'));
|
|
129
|
-
console.log(theme.dim('
|
|
157
|
+
console.log(theme.dim(' Try: "swap 0.1 ETH to USDC", "send 5 USDC to 0x...", "price of AERO"'));
|
|
158
|
+
console.log(theme.dim(' Actions auto-detected — you\'ll be asked to confirm before execution.'));
|
|
130
159
|
console.log('');
|
|
131
160
|
|
|
132
161
|
const spin = spinner('Initializing AI...').start();
|
|
@@ -187,7 +216,31 @@ export async function startChat(opts = {}) {
|
|
|
187
216
|
enriched += `\n\n[Live data: ${priceData.join(', ')}]`;
|
|
188
217
|
}
|
|
189
218
|
|
|
190
|
-
|
|
219
|
+
// Try to detect actionable intent
|
|
220
|
+
const actionKeywords = /\b(swap|send|transfer|buy|sell|snipe|dca|price|balance|gas)\b/i;
|
|
221
|
+
const isActionable = actionKeywords.test(input);
|
|
222
|
+
|
|
223
|
+
let result;
|
|
224
|
+
let parsedIntent = null;
|
|
225
|
+
|
|
226
|
+
if (isActionable) {
|
|
227
|
+
// Use JSON mode to get structured intent
|
|
228
|
+
const intentResult = await llm.json(
|
|
229
|
+
`Parse this as a trading/transfer instruction:\n\n"${enriched}"`,
|
|
230
|
+
{ ephemeral: true }
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (intentResult.parsed && intentResult.parsed.action && intentResult.parsed.action !== 'unknown') {
|
|
234
|
+
parsedIntent = intentResult.parsed;
|
|
235
|
+
// Also get a human-readable response
|
|
236
|
+
result = await llm.chat(enriched);
|
|
237
|
+
} else {
|
|
238
|
+
result = await llm.chat(enriched);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
result = await llm.chat(enriched);
|
|
242
|
+
}
|
|
243
|
+
|
|
191
244
|
spin2.succeed('');
|
|
192
245
|
|
|
193
246
|
// Display response
|
|
@@ -199,6 +252,41 @@ export async function startChat(opts = {}) {
|
|
|
199
252
|
}
|
|
200
253
|
console.log('');
|
|
201
254
|
|
|
255
|
+
// If actionable intent was detected, offer to execute
|
|
256
|
+
if (parsedIntent) {
|
|
257
|
+
const execActions = ['swap', 'send', 'transfer', 'snipe', 'dca', 'price', 'balance', 'gas'];
|
|
258
|
+
if (execActions.includes(parsedIntent.action)) {
|
|
259
|
+
const displayPairs = [];
|
|
260
|
+
if (parsedIntent.action) displayPairs.push(['Action', parsedIntent.action]);
|
|
261
|
+
if (parsedIntent.tokenIn) displayPairs.push(['From', parsedIntent.tokenIn]);
|
|
262
|
+
if (parsedIntent.tokenOut) displayPairs.push(['To token', parsedIntent.tokenOut]);
|
|
263
|
+
if (parsedIntent.token) displayPairs.push(['Token', parsedIntent.token]);
|
|
264
|
+
if (parsedIntent.amount) displayPairs.push(['Amount', parsedIntent.amount]);
|
|
265
|
+
if (parsedIntent.to) displayPairs.push(['Recipient', parsedIntent.to]);
|
|
266
|
+
if (parsedIntent.confidence) displayPairs.push(['Confidence', `${(parsedIntent.confidence * 100).toFixed(0)}%`]);
|
|
267
|
+
|
|
268
|
+
if (displayPairs.length > 1) {
|
|
269
|
+
showSection('DETECTED INTENT');
|
|
270
|
+
kvDisplay(displayPairs);
|
|
271
|
+
if (parsedIntent.warnings?.length > 0) {
|
|
272
|
+
parsedIntent.warnings.forEach(w => warn(w));
|
|
273
|
+
}
|
|
274
|
+
console.log('');
|
|
275
|
+
|
|
276
|
+
const { execute } = await inquirer.prompt([{
|
|
277
|
+
type: 'confirm',
|
|
278
|
+
name: 'execute',
|
|
279
|
+
message: theme.gold(`Execute ${parsedIntent.action}?`),
|
|
280
|
+
default: false,
|
|
281
|
+
}]);
|
|
282
|
+
|
|
283
|
+
if (execute) {
|
|
284
|
+
await executeIntent(parsedIntent, {});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
202
290
|
} catch (err) {
|
|
203
291
|
spin2.fail('Error');
|
|
204
292
|
error(err.message);
|
|
@@ -402,11 +490,37 @@ export async function executeIntent(intent, opts = {}) {
|
|
|
402
490
|
});
|
|
403
491
|
}
|
|
404
492
|
|
|
493
|
+
case 'send':
|
|
405
494
|
case 'transfer': {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
495
|
+
const { sendFunds } = await import('../wallet/manager.js');
|
|
496
|
+
return await sendFunds({
|
|
497
|
+
to: intent.to,
|
|
498
|
+
amount: intent.amount,
|
|
499
|
+
token: intent.token || intent.tokenIn || 'ETH',
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
case 'price': {
|
|
504
|
+
const token = intent.token || intent.tokenOut || intent.tokenIn;
|
|
505
|
+
if (token) {
|
|
506
|
+
const { checkPrices } = await import('../services/watch.js');
|
|
507
|
+
await checkPrices([token]);
|
|
508
|
+
return { success: true, action: 'price' };
|
|
509
|
+
}
|
|
510
|
+
info('No token specified');
|
|
511
|
+
return { success: false, reason: 'no token specified' };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
case 'balance': {
|
|
515
|
+
const { getBalance } = await import('../wallet/manager.js');
|
|
516
|
+
await getBalance();
|
|
517
|
+
return { success: true, action: 'balance' };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
case 'gas': {
|
|
521
|
+
const { showGas } = await import('../services/gas.js');
|
|
522
|
+
await showGas(intent.chain || getConfig('chain') || 'base');
|
|
523
|
+
return { success: true, action: 'gas' };
|
|
410
524
|
}
|
|
411
525
|
|
|
412
526
|
case 'info':
|
package/src/wallet/manager.js
CHANGED
|
@@ -248,6 +248,270 @@ export function useWallet(name) {
|
|
|
248
248
|
success(`Active wallet set to "${name}"`);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
// ═══════════════════════════════════════
|
|
252
|
+
// SEND — ETH and ERC-20 transfers
|
|
253
|
+
// ═══════════════════════════════════════
|
|
254
|
+
|
|
255
|
+
const COMMON_TOKENS = {
|
|
256
|
+
base: { USDC: { addr: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6 } },
|
|
257
|
+
ethereum: { USDC: { addr: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', decimals: 6 }, USDT: { addr: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6 } },
|
|
258
|
+
arbitrum: { USDC: { addr: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', decimals: 6 } },
|
|
259
|
+
optimism: { USDC: { addr: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', decimals: 6 } },
|
|
260
|
+
polygon: { USDC: { addr: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', decimals: 6 } },
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const ERC20_SEND_ABI = [
|
|
264
|
+
'function transfer(address to, uint256 amount) returns (bool)',
|
|
265
|
+
'function balanceOf(address) view returns (uint256)',
|
|
266
|
+
'function decimals() view returns (uint8)',
|
|
267
|
+
'function symbol() view returns (string)',
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
export async function sendFunds(opts = {}) {
|
|
271
|
+
const name = opts.wallet || getConfig('activeWallet');
|
|
272
|
+
if (!name) {
|
|
273
|
+
error('No active wallet. Set one: darksol wallet use <name>');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const chain = getConfig('chain') || 'base';
|
|
278
|
+
const walletData = loadWallet(name);
|
|
279
|
+
|
|
280
|
+
// Interactive prompt if flags not provided
|
|
281
|
+
let to = opts.to;
|
|
282
|
+
let amount = opts.amount;
|
|
283
|
+
let token = opts.token || 'ETH';
|
|
284
|
+
|
|
285
|
+
console.log('');
|
|
286
|
+
showSection(`SEND — ${name}`);
|
|
287
|
+
console.log(theme.dim(` ${walletData.address}`));
|
|
288
|
+
console.log(theme.dim(` Chain: ${chain}`));
|
|
289
|
+
console.log('');
|
|
290
|
+
|
|
291
|
+
if (!to) {
|
|
292
|
+
({ to } = await inquirer.prompt([{
|
|
293
|
+
type: 'input',
|
|
294
|
+
name: 'to',
|
|
295
|
+
message: theme.gold('Recipient address (0x...):'),
|
|
296
|
+
validate: (v) => {
|
|
297
|
+
if (!v.startsWith('0x') || v.length !== 42) return 'Enter a valid 0x address (42 chars)';
|
|
298
|
+
return true;
|
|
299
|
+
},
|
|
300
|
+
}]));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!amount) {
|
|
304
|
+
// Show available tokens
|
|
305
|
+
const tokenChoices = ['ETH'];
|
|
306
|
+
const chainTokens = COMMON_TOKENS[chain] || {};
|
|
307
|
+
Object.keys(chainTokens).forEach(t => tokenChoices.push(t));
|
|
308
|
+
tokenChoices.push('Custom token (paste address)');
|
|
309
|
+
|
|
310
|
+
({ token } = await inquirer.prompt([{
|
|
311
|
+
type: 'list',
|
|
312
|
+
name: 'token',
|
|
313
|
+
message: theme.gold('What to send?'),
|
|
314
|
+
choices: tokenChoices,
|
|
315
|
+
}]));
|
|
316
|
+
|
|
317
|
+
if (token === 'Custom token (paste address)') {
|
|
318
|
+
({ token } = await inquirer.prompt([{
|
|
319
|
+
type: 'input',
|
|
320
|
+
name: 'token',
|
|
321
|
+
message: theme.gold('Token contract address (0x...):'),
|
|
322
|
+
validate: (v) => v.startsWith('0x') && v.length === 42 || 'Invalid address',
|
|
323
|
+
}]));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
({ amount } = await inquirer.prompt([{
|
|
327
|
+
type: 'input',
|
|
328
|
+
name: 'amount',
|
|
329
|
+
message: theme.gold(`Amount to send (${token}):`),
|
|
330
|
+
validate: (v) => parseFloat(v) > 0 || 'Enter a positive amount',
|
|
331
|
+
}]));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Password
|
|
335
|
+
const { password } = await inquirer.prompt([{
|
|
336
|
+
type: 'password',
|
|
337
|
+
name: 'password',
|
|
338
|
+
message: theme.gold('Wallet password:'),
|
|
339
|
+
mask: '●',
|
|
340
|
+
}]);
|
|
341
|
+
|
|
342
|
+
const spin = spinner('Preparing transaction...').start();
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const { signer, provider, address } = await getSigner(name, password);
|
|
346
|
+
|
|
347
|
+
const isETH = token.toUpperCase() === 'ETH';
|
|
348
|
+
const isSymbol = !token.startsWith('0x');
|
|
349
|
+
let tokenAddr = null;
|
|
350
|
+
let tokenDecimals = 18;
|
|
351
|
+
let tokenSymbol = token.toUpperCase();
|
|
352
|
+
|
|
353
|
+
if (!isETH) {
|
|
354
|
+
// Resolve token
|
|
355
|
+
const chainTokens = COMMON_TOKENS[chain] || {};
|
|
356
|
+
if (isSymbol && chainTokens[token.toUpperCase()]) {
|
|
357
|
+
const info = chainTokens[token.toUpperCase()];
|
|
358
|
+
tokenAddr = info.addr;
|
|
359
|
+
tokenDecimals = info.decimals;
|
|
360
|
+
tokenSymbol = token.toUpperCase();
|
|
361
|
+
} else if (token.startsWith('0x')) {
|
|
362
|
+
tokenAddr = token;
|
|
363
|
+
const contract = new ethers.Contract(tokenAddr, ERC20_SEND_ABI, provider);
|
|
364
|
+
tokenDecimals = Number(await contract.decimals());
|
|
365
|
+
tokenSymbol = await contract.symbol();
|
|
366
|
+
} else {
|
|
367
|
+
spin.fail('Unknown token');
|
|
368
|
+
error(`Token "${token}" not recognized. Use a symbol (USDC) or contract address.`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Check balance
|
|
374
|
+
let balanceStr;
|
|
375
|
+
if (isETH) {
|
|
376
|
+
const balance = await provider.getBalance(address);
|
|
377
|
+
const amountWei = ethers.parseEther(amount);
|
|
378
|
+
balanceStr = `${parseFloat(ethers.formatEther(balance)).toFixed(6)} ETH`;
|
|
379
|
+
if (balance < amountWei) {
|
|
380
|
+
spin.fail('Insufficient balance');
|
|
381
|
+
error(`Need ${amount} ETH, have ${balanceStr}`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
const contract = new ethers.Contract(tokenAddr, ERC20_SEND_ABI, provider);
|
|
386
|
+
const balance = await contract.balanceOf(address);
|
|
387
|
+
const amountParsed = ethers.parseUnits(amount, tokenDecimals);
|
|
388
|
+
balanceStr = `${parseFloat(ethers.formatUnits(balance, tokenDecimals)).toFixed(tokenDecimals > 6 ? 6 : 2)} ${tokenSymbol}`;
|
|
389
|
+
if (balance < amountParsed) {
|
|
390
|
+
spin.fail('Insufficient balance');
|
|
391
|
+
error(`Need ${amount} ${tokenSymbol}, have ${balanceStr}`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Estimate gas
|
|
397
|
+
let gasEstimate;
|
|
398
|
+
const feeData = await provider.getFeeData();
|
|
399
|
+
if (isETH) {
|
|
400
|
+
gasEstimate = 21000n;
|
|
401
|
+
} else {
|
|
402
|
+
const contract = new ethers.Contract(tokenAddr, ERC20_SEND_ABI, signer);
|
|
403
|
+
gasEstimate = await contract.transfer.estimateGas(to, ethers.parseUnits(amount, tokenDecimals));
|
|
404
|
+
}
|
|
405
|
+
const gasCostWei = gasEstimate * (feeData.gasPrice || 0n);
|
|
406
|
+
const gasCostEth = parseFloat(ethers.formatEther(gasCostWei));
|
|
407
|
+
|
|
408
|
+
spin.succeed('Transaction ready');
|
|
409
|
+
|
|
410
|
+
// Confirmation
|
|
411
|
+
console.log('');
|
|
412
|
+
showSection('SEND PREVIEW');
|
|
413
|
+
kvDisplay([
|
|
414
|
+
['From', `${name} (${address.slice(0, 6)}...${address.slice(-4)})`],
|
|
415
|
+
['To', to],
|
|
416
|
+
['Amount', `${amount} ${tokenSymbol}`],
|
|
417
|
+
['Balance', balanceStr],
|
|
418
|
+
['Est. Gas', `${gasCostEth.toFixed(6)} ETH`],
|
|
419
|
+
['Chain', chain],
|
|
420
|
+
]);
|
|
421
|
+
console.log('');
|
|
422
|
+
|
|
423
|
+
const { confirm } = await inquirer.prompt([{
|
|
424
|
+
type: 'confirm',
|
|
425
|
+
name: 'confirm',
|
|
426
|
+
message: theme.accent('Send this transaction?'),
|
|
427
|
+
default: false,
|
|
428
|
+
}]);
|
|
429
|
+
|
|
430
|
+
if (!confirm) {
|
|
431
|
+
warn('Transaction cancelled');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const txSpin = spinner('Sending...').start();
|
|
436
|
+
|
|
437
|
+
let tx;
|
|
438
|
+
if (isETH) {
|
|
439
|
+
tx = await signer.sendTransaction({
|
|
440
|
+
to,
|
|
441
|
+
value: ethers.parseEther(amount),
|
|
442
|
+
});
|
|
443
|
+
} else {
|
|
444
|
+
const contract = new ethers.Contract(tokenAddr, ERC20_SEND_ABI, signer);
|
|
445
|
+
tx = await contract.transfer(to, ethers.parseUnits(amount, tokenDecimals));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
txSpin.text = 'Waiting for confirmation...';
|
|
449
|
+
const receipt = await tx.wait();
|
|
450
|
+
|
|
451
|
+
txSpin.succeed(theme.success('Transaction confirmed!'));
|
|
452
|
+
|
|
453
|
+
console.log('');
|
|
454
|
+
showSection('TRANSACTION RECEIPT');
|
|
455
|
+
kvDisplay([
|
|
456
|
+
['TX Hash', receipt.hash],
|
|
457
|
+
['Block', receipt.blockNumber.toString()],
|
|
458
|
+
['Gas Used', receipt.gasUsed.toString()],
|
|
459
|
+
['Status', receipt.status === 1 ? theme.success('✓ Success') : theme.error('✗ Failed')],
|
|
460
|
+
]);
|
|
461
|
+
console.log('');
|
|
462
|
+
|
|
463
|
+
} catch (err) {
|
|
464
|
+
spin.fail('Send failed');
|
|
465
|
+
if (err.message.includes('incorrect password') || err.message.includes('bad decrypt')) {
|
|
466
|
+
error('Wrong password');
|
|
467
|
+
} else {
|
|
468
|
+
error(err.message);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ═══════════════════════════════════════
|
|
474
|
+
// RECEIVE — Show address + QR-friendly display
|
|
475
|
+
// ═══════════════════════════════════════
|
|
476
|
+
|
|
477
|
+
export async function receiveAddress(walletName) {
|
|
478
|
+
const name = walletName || getConfig('activeWallet');
|
|
479
|
+
if (!name) {
|
|
480
|
+
error('No active wallet. Set one: darksol wallet use <name>');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const walletData = loadWallet(name);
|
|
485
|
+
const chain = getConfig('chain') || 'base';
|
|
486
|
+
|
|
487
|
+
console.log('');
|
|
488
|
+
showSection(`RECEIVE — ${name}`);
|
|
489
|
+
console.log('');
|
|
490
|
+
console.log(theme.gold(' Your address:'));
|
|
491
|
+
console.log('');
|
|
492
|
+
console.log(theme.gold.bold(` ${walletData.address}`));
|
|
493
|
+
console.log('');
|
|
494
|
+
|
|
495
|
+
// Visual box around address for easy copy
|
|
496
|
+
const addr = walletData.address;
|
|
497
|
+
const boxWidth = addr.length + 4;
|
|
498
|
+
console.log(theme.dim(` ┌${'─'.repeat(boxWidth)}┐`));
|
|
499
|
+
console.log(theme.dim(` │ `) + theme.gold(addr) + theme.dim(` │`));
|
|
500
|
+
console.log(theme.dim(` └${'─'.repeat(boxWidth)}┘`));
|
|
501
|
+
console.log('');
|
|
502
|
+
|
|
503
|
+
console.log(theme.dim(' This address works on ALL EVM chains:'));
|
|
504
|
+
console.log(theme.dim(' Base • Ethereum • Arbitrum • Optimism • Polygon'));
|
|
505
|
+
console.log('');
|
|
506
|
+
console.log(theme.dim(` Active chain: ${theme.gold(chain)}`));
|
|
507
|
+
console.log(theme.dim(' Make sure the sender is on the same chain!'));
|
|
508
|
+
console.log('');
|
|
509
|
+
|
|
510
|
+
warn('Double-check the address before sharing.');
|
|
511
|
+
warn('Only send EVM-compatible tokens to this address.');
|
|
512
|
+
console.log('');
|
|
513
|
+
}
|
|
514
|
+
|
|
251
515
|
// Export wallet (show address only, never PK without password)
|
|
252
516
|
export async function exportWallet(name) {
|
|
253
517
|
if (!name) {
|
package/src/web/commands.js
CHANGED
|
@@ -72,6 +72,10 @@ export async function handleCommand(cmd, ws) {
|
|
|
72
72
|
return await cmdCasino(args, ws);
|
|
73
73
|
case 'facilitator':
|
|
74
74
|
return await cmdFacilitator(args, ws);
|
|
75
|
+
case 'send':
|
|
76
|
+
return await cmdSend(args, ws);
|
|
77
|
+
case 'receive':
|
|
78
|
+
return await cmdReceive(ws);
|
|
75
79
|
default:
|
|
76
80
|
return {
|
|
77
81
|
output: `\r\n ${ANSI.red}✗ Unknown command: ${cmd}${ANSI.reset}\r\n ${ANSI.dim}Type ${ANSI.gold}help${ANSI.dim} for available commands.${ANSI.reset}\r\n\r\n`,
|
|
@@ -504,6 +508,84 @@ async function cmdConfig(ws) {
|
|
|
504
508
|
return {};
|
|
505
509
|
}
|
|
506
510
|
|
|
511
|
+
// ══════════════════════════════════════════════════
|
|
512
|
+
// SEND / RECEIVE (web shell — info only, actual sends require CLI)
|
|
513
|
+
// ══════════════════════════════════════════════════
|
|
514
|
+
async function cmdSend(args, ws) {
|
|
515
|
+
const chain = getConfig('chain') || 'base';
|
|
516
|
+
const wallet = getConfig('activeWallet');
|
|
517
|
+
|
|
518
|
+
ws.sendLine(`${ANSI.gold} ◆ SEND TOKENS${ANSI.reset}`);
|
|
519
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
520
|
+
ws.sendLine('');
|
|
521
|
+
|
|
522
|
+
if (!wallet) {
|
|
523
|
+
ws.sendLine(` ${ANSI.red}No wallet configured.${ANSI.reset}`);
|
|
524
|
+
ws.sendLine(` Create one: ${ANSI.gold}darksol wallet create <name>${ANSI.reset}`);
|
|
525
|
+
ws.sendLine('');
|
|
526
|
+
return {};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
ws.sendLine(` ${ANSI.white}Send ETH or any ERC-20 token from your wallet.${ANSI.reset}`);
|
|
530
|
+
ws.sendLine('');
|
|
531
|
+
ws.sendLine(` ${ANSI.darkGold}Usage:${ANSI.reset}`);
|
|
532
|
+
ws.sendLine(` ${ANSI.gold}darksol send --to 0x... --amount 0.1 --token ETH${ANSI.reset}`);
|
|
533
|
+
ws.sendLine(` ${ANSI.gold}darksol send --to 0x... --amount 50 --token USDC${ANSI.reset}`);
|
|
534
|
+
ws.sendLine(` ${ANSI.gold}darksol send${ANSI.reset} ${ANSI.dim}(interactive mode — prompts for everything)${ANSI.reset}`);
|
|
535
|
+
ws.sendLine('');
|
|
536
|
+
ws.sendLine(` ${ANSI.darkGold}Features:${ANSI.reset}`);
|
|
537
|
+
ws.sendLine(` ${ANSI.dim}•${ANSI.reset} ETH and any ERC-20 token`);
|
|
538
|
+
ws.sendLine(` ${ANSI.dim}•${ANSI.reset} Balance check before sending`);
|
|
539
|
+
ws.sendLine(` ${ANSI.dim}•${ANSI.reset} Gas estimation in preview`);
|
|
540
|
+
ws.sendLine(` ${ANSI.dim}•${ANSI.reset} Confirmation prompt before execution`);
|
|
541
|
+
ws.sendLine(` ${ANSI.dim}•${ANSI.reset} On-chain receipt after confirmation`);
|
|
542
|
+
ws.sendLine('');
|
|
543
|
+
ws.sendLine(` ${ANSI.darkGold}Active:${ANSI.reset} ${ANSI.white}${wallet}${ANSI.reset} on ${ANSI.white}${chain}${ANSI.reset}`);
|
|
544
|
+
ws.sendLine('');
|
|
545
|
+
ws.sendLine(` ${ANSI.dim}⚠ Sending requires the CLI. Install: npm i -g @darksol/terminal${ANSI.reset}`);
|
|
546
|
+
ws.sendLine('');
|
|
547
|
+
return {};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function cmdReceive(ws) {
|
|
551
|
+
const chain = getConfig('chain') || 'base';
|
|
552
|
+
const wallet = getConfig('activeWallet');
|
|
553
|
+
|
|
554
|
+
ws.sendLine(`${ANSI.gold} ◆ RECEIVE${ANSI.reset}`);
|
|
555
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
556
|
+
ws.sendLine('');
|
|
557
|
+
|
|
558
|
+
if (!wallet) {
|
|
559
|
+
ws.sendLine(` ${ANSI.red}No wallet configured.${ANSI.reset}`);
|
|
560
|
+
ws.sendLine(` Create one: ${ANSI.gold}darksol wallet create <name>${ANSI.reset}`);
|
|
561
|
+
ws.sendLine('');
|
|
562
|
+
return {};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
const { loadWallet } = await import('../wallet/keystore.js');
|
|
567
|
+
const walletData = loadWallet(wallet);
|
|
568
|
+
const addr = walletData.address;
|
|
569
|
+
|
|
570
|
+
ws.sendLine(` ${ANSI.white}Your address:${ANSI.reset}`);
|
|
571
|
+
ws.sendLine('');
|
|
572
|
+
ws.sendLine(` ${ANSI.dim}┌${'─'.repeat(addr.length + 4)}┐${ANSI.reset}`);
|
|
573
|
+
ws.sendLine(` ${ANSI.dim}│ ${ANSI.gold}${addr}${ANSI.dim} │${ANSI.reset}`);
|
|
574
|
+
ws.sendLine(` ${ANSI.dim}└${'─'.repeat(addr.length + 4)}┘${ANSI.reset}`);
|
|
575
|
+
ws.sendLine('');
|
|
576
|
+
ws.sendLine(` ${ANSI.dim}Works on ALL EVM chains:${ANSI.reset}`);
|
|
577
|
+
ws.sendLine(` ${ANSI.dim}Base • Ethereum • Arbitrum • Optimism • Polygon${ANSI.reset}`);
|
|
578
|
+
ws.sendLine('');
|
|
579
|
+
ws.sendLine(` ${ANSI.darkGold}Active chain:${ANSI.reset} ${ANSI.white}${chain}${ANSI.reset}`);
|
|
580
|
+
ws.sendLine(` ${ANSI.red}Make sure the sender is on the same chain!${ANSI.reset}`);
|
|
581
|
+
ws.sendLine('');
|
|
582
|
+
} catch {
|
|
583
|
+
ws.sendLine(` ${ANSI.dim}Run: darksol wallet receive${ANSI.reset}`);
|
|
584
|
+
ws.sendLine('');
|
|
585
|
+
}
|
|
586
|
+
return {};
|
|
587
|
+
}
|
|
588
|
+
|
|
507
589
|
// ══════════════════════════════════════════════════
|
|
508
590
|
// HELPERS
|
|
509
591
|
// ══════════════════════════════════════════════════
|