@darksol/terminal 0.11.0 → 0.13.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/src/cli.js CHANGED
@@ -2,17 +2,21 @@ import { Command } from 'commander';
2
2
  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
+ import { createDashboard } from './ui/dashboard.js';
5
6
  import { getConfig, setConfig, getAllConfig, getRPC, setRPC, configPath } from './config/store.js';
6
7
  import { createWallet, importWallet, showWallets, getBalance, useWallet, exportWallet, sendFunds, receiveAddress } from './wallet/manager.js';
7
8
  import { showPortfolio } from './wallet/portfolio.js';
8
9
  import { showHistory } from './wallet/history.js';
9
10
  import { showGas } from './services/gas.js';
10
11
  import { watchPrice, checkPrices } from './services/watch.js';
12
+ import { getWhaleActivity, listTracked, mirrorTrade, stopTracking, trackWallet } from './services/whale.js';
13
+ import { startWhaleFeed } from './services/whale-monitor.js';
11
14
  import { mailSetup, mailCreate, mailInboxes, mailSend, mailList, mailRead, mailReply, mailForward, mailThreads, mailDelete, mailUse, mailStats, mailStatus } from './services/mail.js';
12
15
  import { startWebShell } from './web/server.js';
13
16
  import { executeSwap } from './trading/swap.js';
14
17
  import { snipeToken, watchSnipe } from './trading/snipe.js';
15
18
  import { createDCA, listDCA, cancelDCA, runDCA } from './trading/dca.js';
19
+ import { arbScan, arbMonitor, arbExecute, arbStats, arbConfig, arbAddEndpoint, arbAddPair, arbRemovePair, arbInfo } from './trading/arb.js';
16
20
  import { executeLifiSwap, executeLifiBridge, checkBridgeStatus, showSupportedChains } from './services/lifi.js';
17
21
  import { topMovers, tokenDetail, compareTokens } from './services/market.js';
18
22
  import { oracleFlip, oracleDice, oracleNumber, oracleShuffle, oracleHealth } from './services/oracle.js';
@@ -42,6 +46,7 @@ import { runSetupWizard } from './setup/wizard.js';
42
46
  import { displaySoul, hasSoul, resetSoul, runSoulSetup } from './soul/index.js';
43
47
  import { clearMemories, exportMemories, getRecentMemories, searchMemories } from './memory/index.js';
44
48
  import { getAgentStatus, planAgentGoal, runAgentTask } from './agent/index.js';
49
+ import { getAuditLog, getStatus as getAutoStatus, listStrategies as listAutoStrategies, startAutonomous, stopAutonomous } from './agent/autonomous.js';
45
50
  import { daemonStart, daemonStop, daemonStatus, daemonRestart } from './daemon/index.js';
46
51
  import { telegramSetup, telegramStartForeground, telegramStopCommand, telegramStatusCommand, telegramSendCommand } from './services/telegram.js';
47
52
  import { createRequire } from 'module';
@@ -329,6 +334,225 @@ export function cli(argv) {
329
334
  .description('Execute pending DCA orders')
330
335
  .action(() => runDCA());
331
336
 
337
+ // ═══════════════════════════════════════
338
+ // ARBITRAGE COMMANDS
339
+ // ═══════════════════════════════════════
340
+ const arb = program
341
+ .command('arb')
342
+ .description('Cross-DEX arbitrage - scan, monitor, execute');
343
+
344
+ arb
345
+ .command('scan')
346
+ .description('One-shot scan across DEXs for price differences')
347
+ .option('-c, --chain <chain>', 'Target chain', 'base')
348
+ .option('-p, --pair <pair>', 'Token pair to scan (e.g. WETH/USDC)')
349
+ .option('-m, --min-profit <usd>', 'Minimum profit threshold in USD', '0.50')
350
+ .option('-s, --trade-size <eth>', 'Trade size in ETH', '0.1')
351
+ .action((opts) => arbScan({
352
+ chain: opts.chain,
353
+ pair: opts.pair,
354
+ minProfit: parseFloat(opts.minProfit),
355
+ tradeSize: parseFloat(opts.tradeSize),
356
+ }));
357
+
358
+ arb
359
+ .command('monitor')
360
+ .description('Real-time block-by-block arb monitoring (WSS recommended)')
361
+ .option('-c, --chain <chain>', 'Target chain', 'base')
362
+ .option('-e, --execute', 'Auto-execute profitable arbs')
363
+ .option('-d, --dry-run', 'Dry-run mode (no real transactions)')
364
+ .option('-m, --min-profit <usd>', 'Minimum profit threshold in USD', '0.50')
365
+ .action((opts) => arbMonitor({
366
+ chain: opts.chain,
367
+ execute: opts.execute,
368
+ dryRun: opts.dryRun !== undefined ? opts.dryRun : undefined,
369
+ minProfit: opts.minProfit,
370
+ }));
371
+
372
+ arb
373
+ .command('stats')
374
+ .description('View arb history and performance stats')
375
+ .option('-d, --days <days>', 'Number of days to show', '7')
376
+ .action((opts) => arbStats({ days: opts.days }));
377
+
378
+ arb
379
+ .command('config')
380
+ .description('Interactive arb configuration (thresholds, dry-run, DEXes)')
381
+ .action(() => arbConfig());
382
+
383
+ arb
384
+ .command('add-endpoint <chain> <url>')
385
+ .description('Add a custom WSS or RPC endpoint for faster arb detection')
386
+ .action((chain, url) => arbAddEndpoint({ chain, url }));
387
+
388
+ arb
389
+ .command('add-pair <tokenA> <tokenB>')
390
+ .description('Add a token pair to the arb scan list')
391
+ .action((tokenA, tokenB) => arbAddPair({ tokenA, tokenB }));
392
+
393
+ arb
394
+ .command('remove-pair <tokenA> <tokenB>')
395
+ .description('Remove a token pair from the arb scan list')
396
+ .action((tokenA, tokenB) => arbRemovePair({ tokenA, tokenB }));
397
+
398
+ arb
399
+ .command('info')
400
+ .description('How arbitrage works, setup guide, and risk warnings')
401
+ .action(() => arbInfo());
402
+
403
+ const auto = program
404
+ .command('auto')
405
+ .description('Autonomous trader mode - goal-based automated execution');
406
+
407
+ auto
408
+ .command('start <goal>')
409
+ .description('Start an autonomous strategy from a natural language goal')
410
+ .requiredOption('--budget <amount>', 'Total USDC budget')
411
+ .requiredOption('--max-per-trade <amount>', 'Per-trade cap')
412
+ .option('--risk <level>', 'Risk level (conservative|moderate|aggressive)', 'moderate')
413
+ .option('--interval <minutes>', 'Evaluation interval in minutes', '5')
414
+ .option('--chain <chain>', 'Target chain (repeatable)', (value, previous = []) => previous.concat(value), [])
415
+ .option('--dry-run', 'Simulate decisions without executing swaps')
416
+ .action(async (goal, opts) => {
417
+ showMiniBanner();
418
+ showSection('AUTONOMOUS START');
419
+ const strategy = await startAutonomous(goal, {
420
+ budget: parseFloat(opts.budget),
421
+ maxPerTrade: parseFloat(opts.maxPerTrade),
422
+ riskLevel: opts.risk,
423
+ interval: parseFloat(opts.interval),
424
+ chains: opts.chain,
425
+ dryRun: opts.dryRun,
426
+ });
427
+
428
+ kvDisplay([
429
+ ['ID', strategy.id],
430
+ ['Goal', strategy.goal],
431
+ ['Budget', `${strategy.budget} USDC`],
432
+ ['Max / Trade', `${strategy.maxPerTrade} USDC`],
433
+ ['Risk', strategy.riskLevel],
434
+ ['Mode', strategy.dryRun ? 'dry-run' : 'live'],
435
+ ['Next Check', strategy.nextCheckAt],
436
+ ]);
437
+ console.log('');
438
+ });
439
+
440
+ auto
441
+ .command('stop <id>')
442
+ .description('Stop a running autonomous strategy')
443
+ .action((id) => {
444
+ showMiniBanner();
445
+ showSection('AUTONOMOUS STOP');
446
+ const strategy = stopAutonomous(id);
447
+ if (!strategy) {
448
+ warn(`Strategy not found: ${id}`);
449
+ console.log('');
450
+ return;
451
+ }
452
+ success(`Stopped ${strategy.id}`);
453
+ console.log('');
454
+ });
455
+
456
+ auto
457
+ .command('status [id]')
458
+ .description('Show one autonomous strategy or all active strategies')
459
+ .action((id) => {
460
+ showMiniBanner();
461
+ showSection('AUTONOMOUS STATUS');
462
+
463
+ if (!id) {
464
+ const items = getAutoStatus();
465
+ if (!items.length) {
466
+ warn('No autonomous strategies found');
467
+ console.log('');
468
+ return;
469
+ }
470
+ items.forEach((item) => {
471
+ kvDisplay([
472
+ ['ID', item.id],
473
+ ['Status', item.status],
474
+ ['Spent', `${item.spent}/${item.budget} USDC`],
475
+ ['Trades', String(item.tradesExecuted)],
476
+ ['PnL', `${item.pnl}`],
477
+ ['Next Check', item.nextCheckAt || '-'],
478
+ ]);
479
+ console.log('');
480
+ });
481
+ return;
482
+ }
483
+
484
+ const strategy = getAutoStatus(id);
485
+ if (!strategy) {
486
+ warn(`Strategy not found: ${id}`);
487
+ console.log('');
488
+ return;
489
+ }
490
+
491
+ kvDisplay([
492
+ ['ID', strategy.id],
493
+ ['Goal', strategy.goal],
494
+ ['Status', strategy.status],
495
+ ['Spent', `${strategy.spent}/${strategy.budget} USDC`],
496
+ ['Trades', String(strategy.tradesExecuted)],
497
+ ['PnL', `${strategy.pnl}`],
498
+ ['Risk', strategy.riskLevel],
499
+ ['Mode', strategy.dryRun ? 'dry-run' : 'live'],
500
+ ['Next Check', strategy.nextCheckAt || '-'],
501
+ ['Last Decision', strategy.lastDecision || '-'],
502
+ ]);
503
+ console.log('');
504
+ });
505
+
506
+ auto
507
+ .command('log <id>')
508
+ .description('Show the recent autonomous audit log')
509
+ .option('--limit <n>', 'Number of audit entries', '20')
510
+ .action((id, opts) => {
511
+ showMiniBanner();
512
+ showSection('AUTONOMOUS AUDIT');
513
+ const entries = getAuditLog(id, parseInt(opts.limit, 10));
514
+ if (!entries.length) {
515
+ warn('No audit entries found');
516
+ console.log('');
517
+ return;
518
+ }
519
+
520
+ entries.forEach((entry) => {
521
+ const headline = `${entry.timestamp} ${entry.type}${entry.action ? ` ${entry.action}` : ''}`;
522
+ console.log(` ${theme.gold(headline)}`);
523
+ if (entry.reason) console.log(` ${theme.dim(entry.reason)}`);
524
+ if (entry.message) console.log(` ${theme.dim(entry.message)}`);
525
+ if (entry.price !== undefined) console.log(` ${theme.dim(`price: ${entry.price}`)}`);
526
+ console.log('');
527
+ });
528
+ });
529
+
530
+ auto
531
+ .command('list')
532
+ .description('List all autonomous strategies')
533
+ .action(() => {
534
+ showMiniBanner();
535
+ showSection('AUTONOMOUS STRATEGIES');
536
+ const items = listAutoStrategies();
537
+ if (!items.length) {
538
+ warn('No autonomous strategies found');
539
+ console.log('');
540
+ return;
541
+ }
542
+
543
+ items.forEach((item) => {
544
+ kvDisplay([
545
+ ['ID', item.id],
546
+ ['Status', item.status],
547
+ ['Spent', `${item.spent}/${item.budget} USDC`],
548
+ ['Trades', String(item.tradesExecuted)],
549
+ ['PnL', `${item.pnl}`],
550
+ ['Next Check', item.nextCheckAt || '-'],
551
+ ]);
552
+ console.log('');
553
+ });
554
+ });
555
+
332
556
  // ═══════════════════════════════════════
333
557
  // MARKET COMMANDS
334
558
  // ═══════════════════════════════════════
@@ -1132,6 +1356,52 @@ export function cli(argv) {
1132
1356
  .option('-p, --port <port>', 'Health server port', '18792')
1133
1357
  .action((opts) => daemonRestart(opts));
1134
1358
 
1359
+ const whale = program
1360
+ .command('whale')
1361
+ .description('Whale Radar - wallet tracking and mirror trades');
1362
+
1363
+ whale
1364
+ .command('track <address>')
1365
+ .description('Track a wallet for new activity')
1366
+ .option('-c, --chain <chain>', 'Chain to monitor', 'base')
1367
+ .option('-l, --label <label>', 'Friendly wallet label')
1368
+ .option('--notify', 'Enable terminal notifications', true)
1369
+ .action((address, opts) => trackWallet(address, opts));
1370
+
1371
+ whale
1372
+ .command('list')
1373
+ .description('List tracked whale wallets')
1374
+ .action(() => listTracked());
1375
+
1376
+ whale
1377
+ .command('stop <address>')
1378
+ .description('Stop tracking a wallet')
1379
+ .action((address) => stopTracking(address));
1380
+
1381
+ whale
1382
+ .command('mirror <address>')
1383
+ .description('Enable copy-trading for a tracked whale')
1384
+ .option('--max <amount>', 'Max USDC-equivalent per trade')
1385
+ .option('-s, --slippage <pct>', 'Mirror trade slippage %', '2')
1386
+ .option('--dry-run', 'Log mirror trades without executing')
1387
+ .action((address, opts) => mirrorTrade(address, {
1388
+ maxPerTrade: opts.max ? parseFloat(opts.max) : null,
1389
+ slippage: parseFloat(opts.slippage),
1390
+ dryRun: Boolean(opts.dryRun),
1391
+ }));
1392
+
1393
+ whale
1394
+ .command('activity <address>')
1395
+ .description('Show recent activity for a whale wallet')
1396
+ .option('-l, --limit <n>', 'Number of transactions', '10')
1397
+ .option('-c, --chain <chain>', 'Chain to query', 'base')
1398
+ .action((address, opts) => getWhaleActivity(address, parseInt(opts.limit, 10), opts));
1399
+
1400
+ whale
1401
+ .command('feed')
1402
+ .description('Open the live whale event feed')
1403
+ .action(() => startWhaleFeed());
1404
+
1135
1405
  // ═══════════════════════════════════════
1136
1406
  // TELEGRAM COMMANDS
1137
1407
  // ═══════════════════════════════════════
@@ -1389,6 +1659,19 @@ export function cli(argv) {
1389
1659
  }
1390
1660
  });
1391
1661
 
1662
+ program
1663
+ .command('dash')
1664
+ .description('Launch the live terminal dashboard')
1665
+ .option('--refresh <seconds>', 'Refresh interval in seconds', '30')
1666
+ .option('--compact', 'Use the compact 2-panel layout')
1667
+ .action(async (opts) => {
1668
+ const dashboard = createDashboard({
1669
+ refresh: parseInt(opts.refresh, 10),
1670
+ compact: Boolean(opts.compact),
1671
+ });
1672
+ await dashboard.ready;
1673
+ });
1674
+
1392
1675
  // ═══════════════════════════════════════
1393
1676
  // FUZZY / NATURAL LANGUAGE FALLBACK
1394
1677
  // ═══════════════════════════════════════
@@ -1717,6 +2000,8 @@ function showCommandList() {
1717
2000
  ['watch', 'Live price monitoring + alerts'],
1718
2001
  ['gas', 'Gas prices & cost estimates'],
1719
2002
  ['trade', 'Swap tokens, snipe, trading'],
2003
+ ['arb', 'Cross-DEX arbitrage scanner'],
2004
+ ['auto', 'Autonomous trader strategies'],
1720
2005
  ['bridge', 'Cross-chain bridge (LI.FI)'],
1721
2006
  ['dca', 'Dollar-cost averaging orders'],
1722
2007
  ['ai chat', 'Standalone AI chat session'],
@@ -1727,6 +2012,7 @@ function showCommandList() {
1727
2012
  ['memory', 'Persistent cross-session memory'],
1728
2013
  ['script', 'Execution scripts & strategies'],
1729
2014
  ['market', 'Market intel & token data'],
2015
+ ['whale', 'Whale Radar - wallet tracking'],
1730
2016
  ['oracle', 'On-chain random oracle'],
1731
2017
  ['casino', 'The Clawsino - betting'],
1732
2018
  ['poker', 'GTO Poker Arena — heads-up holdem'],
@@ -126,6 +126,14 @@ export const SERVICES = {
126
126
  docsUrl: 'https://docs.dexscreener.com',
127
127
  validate: (key) => key.length > 10,
128
128
  },
129
+ etherscan: {
130
+ name: 'Etherscan',
131
+ category: 'data',
132
+ description: 'Explorer APIs — Etherscan, Basescan, Arbiscan, Polygonscan',
133
+ envVar: 'ETHERSCAN_API_KEY',
134
+ docsUrl: 'https://etherscan.io/apis',
135
+ validate: (key) => key.length > 10,
136
+ },
129
137
  defillama: {
130
138
  name: 'DefiLlama',
131
139
  category: 'data',
@@ -298,6 +306,14 @@ export function getKeyFromEnv(service) {
298
306
  return null;
299
307
  }
300
308
 
309
+ /**
310
+ * Get an API key without prompting.
311
+ * Prefers auto-stored keys, then environment variables.
312
+ */
313
+ export function getApiKey(service) {
314
+ return getKeyAuto(service) || getKeyFromEnv(service);
315
+ }
316
+
301
317
  /**
302
318
  * Remove a key
303
319
  */
@@ -51,6 +51,40 @@ const config = new Conf({
51
51
  maxOrders: 100,
52
52
  },
53
53
  },
54
+ autonomous: {
55
+ type: 'object',
56
+ default: {
57
+ strategies: [],
58
+ },
59
+ },
60
+ arb: {
61
+ type: 'object',
62
+ default: {
63
+ enabled: false,
64
+ minProfitUsd: 0.50,
65
+ maxTradeSize: 1.0,
66
+ gasCeiling: 0.01,
67
+ cooldownMs: 5000,
68
+ dryRun: true,
69
+ tokenAllowlist: [],
70
+ tokenDenylist: [],
71
+ endpoints: {
72
+ wss: {},
73
+ rpc: {},
74
+ },
75
+ dexes: {
76
+ base: ['uniswapV3', 'aerodrome', 'sushiswap'],
77
+ ethereum: ['uniswapV3', 'sushiswap'],
78
+ arbitrum: ['uniswapV3', 'sushiswap', 'camelot'],
79
+ optimism: ['uniswapV3', 'velodrome'],
80
+ polygon: ['uniswapV3', 'quickswap'],
81
+ },
82
+ pairs: [
83
+ { tokenA: 'WETH', tokenB: 'USDC' },
84
+ { tokenA: 'WETH', tokenB: 'USDT' },
85
+ ],
86
+ },
87
+ },
54
88
  services: {
55
89
  type: 'object',
56
90
  default: {
package/src/llm/intent.js CHANGED
@@ -66,6 +66,8 @@ ACTIONS (use the most specific one):
66
66
  - "gas" — gas price check
67
67
  - "cards" — order a prepaid Visa/Mastercard with crypto (e.g. "order a $50 card", "get me a prepaid card")
68
68
  - "casino" — play a casino game (coinflip, dice, hilo, slots). All bets are $1 USDC. (e.g. "flip a coin", "bet on heads", "play slots", "roll dice over 3")
69
+ - "arb_scan" — scan for cross-DEX arbitrage opportunities (e.g. "find arb opportunities", "scan for price differences", "check arb on base")
70
+ - "arb_monitor" — start real-time arbitrage monitoring (e.g. "monitor arb", "watch for arb opportunities")
69
71
  - "unknown" — can't determine what the user wants
70
72
 
71
73
  CASINO GAMES:
@@ -1,49 +1,49 @@
1
1
  import { ethers } from 'ethers';
2
2
  import { getConfig, getRPC } from '../config/store.js';
3
3
  import { theme } from '../ui/theme.js';
4
- import { spinner, kvDisplay, success, error, warn, info } from '../ui/components.js';
4
+ import { spinner, kvDisplay, error, info } from '../ui/components.js';
5
5
  import { showSection } from '../ui/banner.js';
6
6
 
7
- // ══════════════════════════════════════════════════
8
- // GAS ESTIMATOR
9
- // ══════════════════════════════════════════════════
10
-
11
- /**
12
- * Show current gas prices for active chain
13
- */
14
- export async function showGas(chain) {
15
- chain = chain || getConfig('chain') || 'base';
16
- const rpc = getRPC(chain);
7
+ export async function fetchGasSnapshot(chain) {
8
+ const resolvedChain = chain || getConfig('chain') || 'base';
9
+ const rpc = getRPC(resolvedChain);
17
10
  const provider = new ethers.JsonRpcProvider(rpc);
11
+ const feeData = await provider.getFeeData();
12
+ const block = await provider.getBlock('latest');
13
+
14
+ return {
15
+ chain: resolvedChain,
16
+ blockNumber: block?.number || null,
17
+ feeData,
18
+ gasPrice: feeData.gasPrice ? parseFloat(ethers.formatUnits(feeData.gasPrice, 'gwei')) : 0,
19
+ maxFee: feeData.maxFeePerGas ? parseFloat(ethers.formatUnits(feeData.maxFeePerGas, 'gwei')) : 0,
20
+ maxPriority: feeData.maxPriorityFeePerGas ? parseFloat(ethers.formatUnits(feeData.maxPriorityFeePerGas, 'gwei')) : 0,
21
+ baseFee: block?.baseFeePerGas ? parseFloat(ethers.formatUnits(block.baseFeePerGas, 'gwei')) : 0,
22
+ ethPrice: await getETHPrice(),
23
+ };
24
+ }
18
25
 
19
- const spin = spinner(`Fetching gas on ${chain}...`).start();
26
+ export async function showGas(chain) {
27
+ const resolvedChain = chain || getConfig('chain') || 'base';
28
+ const spin = spinner(`Fetching gas on ${resolvedChain}...`).start();
20
29
 
21
30
  try {
22
- const feeData = await provider.getFeeData();
23
- const block = await provider.getBlock('latest');
24
-
31
+ const snapshot = await fetchGasSnapshot(resolvedChain);
25
32
  spin.succeed('Gas data fetched');
26
33
 
27
- const gasPrice = feeData.gasPrice ? parseFloat(ethers.formatUnits(feeData.gasPrice, 'gwei')) : 0;
28
- const maxFee = feeData.maxFeePerGas ? parseFloat(ethers.formatUnits(feeData.maxFeePerGas, 'gwei')) : 0;
29
- const maxPriority = feeData.maxPriorityFeePerGas ? parseFloat(ethers.formatUnits(feeData.maxPriorityFeePerGas, 'gwei')) : 0;
30
- const baseFee = block?.baseFeePerGas ? parseFloat(ethers.formatUnits(block.baseFeePerGas, 'gwei')) : 0;
31
-
32
34
  console.log('');
33
- showSection(`GAS ${chain.toUpperCase()}`);
35
+ showSection(`GAS - ${resolvedChain.toUpperCase()}`);
34
36
  kvDisplay([
35
- ['Gas Price', `${gasPrice.toFixed(4)} gwei`],
36
- ['Base Fee', `${baseFee.toFixed(4)} gwei`],
37
- ['Max Fee', `${maxFee.toFixed(4)} gwei`],
38
- ['Priority Fee', `${maxPriority.toFixed(4)} gwei`],
39
- ['Block', `#${block?.number || '?'}`],
37
+ ['Gas Price', `${snapshot.gasPrice.toFixed(4)} gwei`],
38
+ ['Base Fee', `${snapshot.baseFee.toFixed(4)} gwei`],
39
+ ['Max Fee', `${snapshot.maxFee.toFixed(4)} gwei`],
40
+ ['Priority Fee', `${snapshot.maxPriority.toFixed(4)} gwei`],
41
+ ['Block', `#${snapshot.blockNumber || '?'}`],
40
42
  ]);
41
43
 
42
- // Estimate common operations
43
44
  console.log('');
44
45
  showSection('ESTIMATED COSTS');
45
46
 
46
- const ethPrice = await getETHPrice();
47
47
  const estimates = [
48
48
  { name: 'ETH Transfer', gas: 21000n },
49
49
  { name: 'ERC-20 Transfer', gas: 65000n },
@@ -54,29 +54,25 @@ export async function showGas(chain) {
54
54
  ];
55
55
 
56
56
  estimates.forEach(({ name, gas }) => {
57
- const costWei = gas * (feeData.gasPrice || 0n);
57
+ const costWei = gas * (snapshot.feeData.gasPrice || 0n);
58
58
  const costETH = parseFloat(ethers.formatEther(costWei));
59
- const costUSD = costETH * ethPrice;
60
- const label = name.padEnd(24);
59
+ const costUSD = costETH * snapshot.ethPrice;
61
60
  const ethStr = costETH < 0.000001 ? '<$0.01' : `${costETH.toFixed(6)} ETH`;
62
61
  const usdStr = costUSD < 0.01 ? '<$0.01' : `$${costUSD.toFixed(2)}`;
63
- console.log(` ${theme.dim(label)} ${ethStr.padEnd(16)} ${theme.gold(usdStr)}`);
62
+ console.log(` ${theme.dim(name.padEnd(24))} ${ethStr.padEnd(16)} ${theme.gold(usdStr)}`);
64
63
  });
65
64
 
66
65
  console.log('');
67
- info(`ETH price: $${ethPrice.toFixed(2)}`);
66
+ info(`ETH price: $${snapshot.ethPrice.toFixed(2)}`);
68
67
  console.log('');
69
68
 
70
- return { chain, gasPrice, baseFee, maxFee, maxPriority, ethPrice };
69
+ return snapshot;
71
70
  } catch (err) {
72
71
  spin.fail('Failed to fetch gas data');
73
72
  error(err.message);
74
73
  }
75
74
  }
76
75
 
77
- /**
78
- * Get ETH price (CoinGecko, fallback estimate)
79
- */
80
76
  async function getETHPrice() {
81
77
  try {
82
78
  const fetch = (await import('node-fetch')).default;
@@ -88,12 +84,9 @@ async function getETHPrice() {
88
84
  }
89
85
  }
90
86
 
91
- /**
92
- * Estimate gas for a specific transaction (pre-trade check)
93
- */
94
87
  export async function estimateTradeGas(txParams, chain) {
95
- chain = chain || getConfig('chain') || 'base';
96
- const rpc = getRPC(chain);
88
+ const resolvedChain = chain || getConfig('chain') || 'base';
89
+ const rpc = getRPC(resolvedChain);
97
90
  const provider = new ethers.JsonRpcProvider(rpc);
98
91
 
99
92
  try {