@darksol/terminal 0.4.3 → 0.4.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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": {
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
  // ═══════════════════════════════════════
@@ -965,6 +998,8 @@ function showCommandList() {
965
998
  showSection('COMMANDS');
966
999
  const commands = [
967
1000
  ['wallet', 'Create, import, manage wallets'],
1001
+ ['send', 'Send ETH or tokens'],
1002
+ ['receive', 'Show address to receive funds'],
968
1003
  ['portfolio', 'Multi-chain balance view'],
969
1004
  ['price', 'Quick token price check'],
970
1005
  ['watch', 'Live price monitoring + alerts'],
@@ -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) {