@darksol/terminal 0.11.0 → 0.12.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.
@@ -0,0 +1,166 @@
1
+ import { topMovers } from '../services/market.js';
2
+ import { checkPrices } from '../services/watch.js';
3
+ import { getConfig } from '../config/store.js';
4
+ import { estimateGasCost, getProvider, quickPrice } from '../utils/helpers.js';
5
+
6
+ const DEFAULT_COOLDOWN_MS = 15 * 60 * 1000;
7
+
8
+ const evaluatorDeps = {
9
+ quickPrice,
10
+ topMovers,
11
+ checkPrices,
12
+ getProvider,
13
+ estimateGasCost,
14
+ now: () => Date.now(),
15
+ };
16
+
17
+ function riskProfile(level = 'moderate') {
18
+ const profiles = {
19
+ conservative: { gasGwei: 1.5, cooldownMs: 30 * 60 * 1000, minConfidence: 0.75 },
20
+ moderate: { gasGwei: 3, cooldownMs: 15 * 60 * 1000, minConfidence: 0.6 },
21
+ aggressive: { gasGwei: 10, cooldownMs: 5 * 60 * 1000, minConfidence: 0.45 },
22
+ };
23
+ return profiles[level] || profiles.moderate;
24
+ }
25
+
26
+ function normalizeToken(symbol) {
27
+ return typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
28
+ }
29
+
30
+ async function collectMarketData(strategy, deps = evaluatorDeps) {
31
+ const primaryToken = normalizeToken(
32
+ strategy?.plan?.primaryToken
33
+ || strategy?.plan?.entry?.token
34
+ || strategy?.intent?.tokenOut
35
+ || strategy?.intent?.token
36
+ );
37
+ const quoteToken = normalizeToken(strategy?.plan?.quoteToken || strategy?.intent?.tokenIn || 'USDC');
38
+ const currentPrice = primaryToken ? await deps.quickPrice(primaryToken) : null;
39
+
40
+ try {
41
+ if (primaryToken) {
42
+ await deps.checkPrices([primaryToken]);
43
+ }
44
+ } catch {}
45
+
46
+ return {
47
+ primaryToken,
48
+ quoteToken,
49
+ currentPrice,
50
+ price: currentPrice?.price ? Number(currentPrice.price) : null,
51
+ liquidity: currentPrice?.liquidity ? Number(currentPrice.liquidity) : 0,
52
+ change24h: currentPrice?.change24h !== undefined ? Number(currentPrice.change24h) : null,
53
+ volume24h: currentPrice?.volume24h ? Number(currentPrice.volume24h) : 0,
54
+ timestamp: new Date(deps.now()).toISOString(),
55
+ };
56
+ }
57
+
58
+ export async function evaluateConditions(strategy) {
59
+ const deps = evaluatorDeps;
60
+ const profile = riskProfile(strategy?.riskLevel);
61
+ const marketData = await collectMarketData(strategy, deps);
62
+ const provider = deps.getProvider(strategy?.chains?.[0] || getConfig('chain') || 'base');
63
+ const gas = await deps.estimateGasCost(provider, 180000n);
64
+ const gasGwei = Number(gas.gwei || 0);
65
+ const gasTooHigh = gasGwei > profile.gasGwei;
66
+
67
+ const lastTradeAt = strategy?.lastTradeAt ? new Date(strategy.lastTradeAt).getTime() : 0;
68
+ const cooldownMs = strategy?.plan?.cooldownMs || profile.cooldownMs || DEFAULT_COOLDOWN_MS;
69
+ const inCooldown = Boolean(lastTradeAt) && (deps.now() - lastTradeAt) < cooldownMs;
70
+ const minLiquidity = Number(strategy?.plan?.filters?.minLiquidity || 0);
71
+ const priceBelow = strategy?.plan?.entry?.priceBelow;
72
+ const priceAbove = strategy?.plan?.entry?.priceAbove;
73
+ const hasPrice = typeof marketData.price === 'number' && !Number.isNaN(marketData.price);
74
+
75
+ const meetsLiquidity = marketData.liquidity >= minLiquidity;
76
+ const meetsPriceBelow = !priceBelow || (hasPrice && marketData.price <= priceBelow);
77
+ const meetsPriceAbove = !priceAbove || (hasPrice && marketData.price >= priceAbove);
78
+ const hasBudget = Number(strategy?.budget || 0) > Number(strategy?.spent || 0);
79
+ const maxPerTrade = Number(strategy?.maxPerTrade || strategy?.budget || 0);
80
+ const remainingBudget = Math.max(0, Number(strategy?.budget || 0) - Number(strategy?.spent || 0));
81
+ const tradeAmount = Math.min(maxPerTrade, remainingBudget);
82
+ const withinTradeLimit = tradeAmount > 0 && tradeAmount <= maxPerTrade;
83
+
84
+ return {
85
+ ...marketData,
86
+ gasGwei,
87
+ gasTooHigh,
88
+ inCooldown,
89
+ cooldownMs,
90
+ remainingBudget,
91
+ tradeAmount,
92
+ withinTradeLimit,
93
+ meetsLiquidity,
94
+ meetsEntry: hasBudget && meetsLiquidity && meetsPriceBelow && meetsPriceAbove,
95
+ meetsExit: Boolean(
96
+ hasPrice
97
+ && (
98
+ (strategy?.plan?.exit?.takeProfitPrice && marketData.price >= strategy.plan.exit.takeProfitPrice)
99
+ || (strategy?.plan?.exit?.stopLossPrice && marketData.price <= strategy.plan.exit.stopLossPrice)
100
+ )
101
+ ),
102
+ };
103
+ }
104
+
105
+ export async function shouldTrade(strategy, marketData) {
106
+ const profile = riskProfile(strategy?.riskLevel);
107
+ const conditions = marketData || await evaluateConditions(strategy);
108
+ const amount = Number(conditions.tradeAmount || 0);
109
+
110
+ if (!conditions.currentPrice) {
111
+ return { action: 'hold', reason: 'No market data for target token', confidence: 0.1 };
112
+ }
113
+ if (conditions.gasTooHigh) {
114
+ return { action: 'hold', reason: `Gas too high (${conditions.gasGwei.toFixed(2)} gwei)`, confidence: 0.15 };
115
+ }
116
+ if (!conditions.withinTradeLimit) {
117
+ return { action: 'hold', reason: 'Per-trade or budget limit reached', confidence: 0.1 };
118
+ }
119
+ if (conditions.inCooldown) {
120
+ return { action: 'hold', reason: 'Token cooldown active', confidence: 0.2 };
121
+ }
122
+ if (conditions.meetsExit && Number(strategy?.positionSize || 0) > 0) {
123
+ return { action: 'sell', reason: 'Exit conditions met', confidence: 0.8 };
124
+ }
125
+ if (!conditions.meetsEntry) {
126
+ const reasons = [];
127
+ if (!conditions.meetsLiquidity) reasons.push('liquidity below filter');
128
+ if (strategy?.plan?.entry?.priceBelow && conditions.price > strategy.plan.entry.priceBelow) reasons.push('price above entry threshold');
129
+ if (strategy?.plan?.entry?.priceAbove && conditions.price < strategy.plan.entry.priceAbove) reasons.push('price below momentum threshold');
130
+ if (amount <= 0) reasons.push('budget exhausted');
131
+ return { action: 'hold', reason: reasons.join(', ') || 'Entry conditions not met', confidence: 0.25 };
132
+ }
133
+
134
+ let confidence = 0.55;
135
+ if (strategy?.plan?.entry?.priceBelow && conditions.price <= strategy.plan.entry.priceBelow) confidence += 0.15;
136
+ if (conditions.liquidity >= Number(strategy?.plan?.filters?.minLiquidity || 0) * 2) confidence += 0.1;
137
+ if (typeof conditions.change24h === 'number') {
138
+ if (strategy?.plan?.mode === 'dca') confidence += 0.05;
139
+ else if (conditions.change24h > 0) confidence += 0.1;
140
+ else confidence -= 0.05;
141
+ }
142
+
143
+ confidence = Math.max(0, Math.min(0.99, confidence));
144
+ if (confidence < profile.minConfidence) {
145
+ return { action: 'hold', reason: 'Signal below risk threshold', confidence };
146
+ }
147
+
148
+ return {
149
+ action: 'buy',
150
+ reason: `Entry conditions met for ${conditions.primaryToken} using ${amount.toFixed(2)} ${conditions.quoteToken}`,
151
+ confidence,
152
+ };
153
+ }
154
+
155
+ export function __setEvaluatorDeps(overrides = {}) {
156
+ Object.assign(evaluatorDeps, overrides);
157
+ }
158
+
159
+ export function __resetEvaluatorDeps() {
160
+ evaluatorDeps.quickPrice = quickPrice;
161
+ evaluatorDeps.topMovers = topMovers;
162
+ evaluatorDeps.checkPrices = checkPrices;
163
+ evaluatorDeps.getProvider = getProvider;
164
+ evaluatorDeps.estimateGasCost = estimateGasCost;
165
+ evaluatorDeps.now = () => Date.now();
166
+ }
package/src/cli.js CHANGED
@@ -2,12 +2,15 @@ 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';
@@ -42,6 +45,7 @@ import { runSetupWizard } from './setup/wizard.js';
42
45
  import { displaySoul, hasSoul, resetSoul, runSoulSetup } from './soul/index.js';
43
46
  import { clearMemories, exportMemories, getRecentMemories, searchMemories } from './memory/index.js';
44
47
  import { getAgentStatus, planAgentGoal, runAgentTask } from './agent/index.js';
48
+ import { getAuditLog, getStatus as getAutoStatus, listStrategies as listAutoStrategies, startAutonomous, stopAutonomous } from './agent/autonomous.js';
45
49
  import { daemonStart, daemonStop, daemonStatus, daemonRestart } from './daemon/index.js';
46
50
  import { telegramSetup, telegramStartForeground, telegramStopCommand, telegramStatusCommand, telegramSendCommand } from './services/telegram.js';
47
51
  import { createRequire } from 'module';
@@ -329,6 +333,159 @@ export function cli(argv) {
329
333
  .description('Execute pending DCA orders')
330
334
  .action(() => runDCA());
331
335
 
336
+ const auto = program
337
+ .command('auto')
338
+ .description('Autonomous trader mode - goal-based automated execution');
339
+
340
+ auto
341
+ .command('start <goal>')
342
+ .description('Start an autonomous strategy from a natural language goal')
343
+ .requiredOption('--budget <amount>', 'Total USDC budget')
344
+ .requiredOption('--max-per-trade <amount>', 'Per-trade cap')
345
+ .option('--risk <level>', 'Risk level (conservative|moderate|aggressive)', 'moderate')
346
+ .option('--interval <minutes>', 'Evaluation interval in minutes', '5')
347
+ .option('--chain <chain>', 'Target chain (repeatable)', (value, previous = []) => previous.concat(value), [])
348
+ .option('--dry-run', 'Simulate decisions without executing swaps')
349
+ .action(async (goal, opts) => {
350
+ showMiniBanner();
351
+ showSection('AUTONOMOUS START');
352
+ const strategy = await startAutonomous(goal, {
353
+ budget: parseFloat(opts.budget),
354
+ maxPerTrade: parseFloat(opts.maxPerTrade),
355
+ riskLevel: opts.risk,
356
+ interval: parseFloat(opts.interval),
357
+ chains: opts.chain,
358
+ dryRun: opts.dryRun,
359
+ });
360
+
361
+ kvDisplay([
362
+ ['ID', strategy.id],
363
+ ['Goal', strategy.goal],
364
+ ['Budget', `${strategy.budget} USDC`],
365
+ ['Max / Trade', `${strategy.maxPerTrade} USDC`],
366
+ ['Risk', strategy.riskLevel],
367
+ ['Mode', strategy.dryRun ? 'dry-run' : 'live'],
368
+ ['Next Check', strategy.nextCheckAt],
369
+ ]);
370
+ console.log('');
371
+ });
372
+
373
+ auto
374
+ .command('stop <id>')
375
+ .description('Stop a running autonomous strategy')
376
+ .action((id) => {
377
+ showMiniBanner();
378
+ showSection('AUTONOMOUS STOP');
379
+ const strategy = stopAutonomous(id);
380
+ if (!strategy) {
381
+ warn(`Strategy not found: ${id}`);
382
+ console.log('');
383
+ return;
384
+ }
385
+ success(`Stopped ${strategy.id}`);
386
+ console.log('');
387
+ });
388
+
389
+ auto
390
+ .command('status [id]')
391
+ .description('Show one autonomous strategy or all active strategies')
392
+ .action((id) => {
393
+ showMiniBanner();
394
+ showSection('AUTONOMOUS STATUS');
395
+
396
+ if (!id) {
397
+ const items = getAutoStatus();
398
+ if (!items.length) {
399
+ warn('No autonomous strategies found');
400
+ console.log('');
401
+ return;
402
+ }
403
+ items.forEach((item) => {
404
+ kvDisplay([
405
+ ['ID', item.id],
406
+ ['Status', item.status],
407
+ ['Spent', `${item.spent}/${item.budget} USDC`],
408
+ ['Trades', String(item.tradesExecuted)],
409
+ ['PnL', `${item.pnl}`],
410
+ ['Next Check', item.nextCheckAt || '-'],
411
+ ]);
412
+ console.log('');
413
+ });
414
+ return;
415
+ }
416
+
417
+ const strategy = getAutoStatus(id);
418
+ if (!strategy) {
419
+ warn(`Strategy not found: ${id}`);
420
+ console.log('');
421
+ return;
422
+ }
423
+
424
+ kvDisplay([
425
+ ['ID', strategy.id],
426
+ ['Goal', strategy.goal],
427
+ ['Status', strategy.status],
428
+ ['Spent', `${strategy.spent}/${strategy.budget} USDC`],
429
+ ['Trades', String(strategy.tradesExecuted)],
430
+ ['PnL', `${strategy.pnl}`],
431
+ ['Risk', strategy.riskLevel],
432
+ ['Mode', strategy.dryRun ? 'dry-run' : 'live'],
433
+ ['Next Check', strategy.nextCheckAt || '-'],
434
+ ['Last Decision', strategy.lastDecision || '-'],
435
+ ]);
436
+ console.log('');
437
+ });
438
+
439
+ auto
440
+ .command('log <id>')
441
+ .description('Show the recent autonomous audit log')
442
+ .option('--limit <n>', 'Number of audit entries', '20')
443
+ .action((id, opts) => {
444
+ showMiniBanner();
445
+ showSection('AUTONOMOUS AUDIT');
446
+ const entries = getAuditLog(id, parseInt(opts.limit, 10));
447
+ if (!entries.length) {
448
+ warn('No audit entries found');
449
+ console.log('');
450
+ return;
451
+ }
452
+
453
+ entries.forEach((entry) => {
454
+ const headline = `${entry.timestamp} ${entry.type}${entry.action ? ` ${entry.action}` : ''}`;
455
+ console.log(` ${theme.gold(headline)}`);
456
+ if (entry.reason) console.log(` ${theme.dim(entry.reason)}`);
457
+ if (entry.message) console.log(` ${theme.dim(entry.message)}`);
458
+ if (entry.price !== undefined) console.log(` ${theme.dim(`price: ${entry.price}`)}`);
459
+ console.log('');
460
+ });
461
+ });
462
+
463
+ auto
464
+ .command('list')
465
+ .description('List all autonomous strategies')
466
+ .action(() => {
467
+ showMiniBanner();
468
+ showSection('AUTONOMOUS STRATEGIES');
469
+ const items = listAutoStrategies();
470
+ if (!items.length) {
471
+ warn('No autonomous strategies found');
472
+ console.log('');
473
+ return;
474
+ }
475
+
476
+ items.forEach((item) => {
477
+ kvDisplay([
478
+ ['ID', item.id],
479
+ ['Status', item.status],
480
+ ['Spent', `${item.spent}/${item.budget} USDC`],
481
+ ['Trades', String(item.tradesExecuted)],
482
+ ['PnL', `${item.pnl}`],
483
+ ['Next Check', item.nextCheckAt || '-'],
484
+ ]);
485
+ console.log('');
486
+ });
487
+ });
488
+
332
489
  // ═══════════════════════════════════════
333
490
  // MARKET COMMANDS
334
491
  // ═══════════════════════════════════════
@@ -1132,6 +1289,52 @@ export function cli(argv) {
1132
1289
  .option('-p, --port <port>', 'Health server port', '18792')
1133
1290
  .action((opts) => daemonRestart(opts));
1134
1291
 
1292
+ const whale = program
1293
+ .command('whale')
1294
+ .description('Whale Radar - wallet tracking and mirror trades');
1295
+
1296
+ whale
1297
+ .command('track <address>')
1298
+ .description('Track a wallet for new activity')
1299
+ .option('-c, --chain <chain>', 'Chain to monitor', 'base')
1300
+ .option('-l, --label <label>', 'Friendly wallet label')
1301
+ .option('--notify', 'Enable terminal notifications', true)
1302
+ .action((address, opts) => trackWallet(address, opts));
1303
+
1304
+ whale
1305
+ .command('list')
1306
+ .description('List tracked whale wallets')
1307
+ .action(() => listTracked());
1308
+
1309
+ whale
1310
+ .command('stop <address>')
1311
+ .description('Stop tracking a wallet')
1312
+ .action((address) => stopTracking(address));
1313
+
1314
+ whale
1315
+ .command('mirror <address>')
1316
+ .description('Enable copy-trading for a tracked whale')
1317
+ .option('--max <amount>', 'Max USDC-equivalent per trade')
1318
+ .option('-s, --slippage <pct>', 'Mirror trade slippage %', '2')
1319
+ .option('--dry-run', 'Log mirror trades without executing')
1320
+ .action((address, opts) => mirrorTrade(address, {
1321
+ maxPerTrade: opts.max ? parseFloat(opts.max) : null,
1322
+ slippage: parseFloat(opts.slippage),
1323
+ dryRun: Boolean(opts.dryRun),
1324
+ }));
1325
+
1326
+ whale
1327
+ .command('activity <address>')
1328
+ .description('Show recent activity for a whale wallet')
1329
+ .option('-l, --limit <n>', 'Number of transactions', '10')
1330
+ .option('-c, --chain <chain>', 'Chain to query', 'base')
1331
+ .action((address, opts) => getWhaleActivity(address, parseInt(opts.limit, 10), opts));
1332
+
1333
+ whale
1334
+ .command('feed')
1335
+ .description('Open the live whale event feed')
1336
+ .action(() => startWhaleFeed());
1337
+
1135
1338
  // ═══════════════════════════════════════
1136
1339
  // TELEGRAM COMMANDS
1137
1340
  // ═══════════════════════════════════════
@@ -1389,6 +1592,19 @@ export function cli(argv) {
1389
1592
  }
1390
1593
  });
1391
1594
 
1595
+ program
1596
+ .command('dash')
1597
+ .description('Launch the live terminal dashboard')
1598
+ .option('--refresh <seconds>', 'Refresh interval in seconds', '30')
1599
+ .option('--compact', 'Use the compact 2-panel layout')
1600
+ .action(async (opts) => {
1601
+ const dashboard = createDashboard({
1602
+ refresh: parseInt(opts.refresh, 10),
1603
+ compact: Boolean(opts.compact),
1604
+ });
1605
+ await dashboard.ready;
1606
+ });
1607
+
1392
1608
  // ═══════════════════════════════════════
1393
1609
  // FUZZY / NATURAL LANGUAGE FALLBACK
1394
1610
  // ═══════════════════════════════════════
@@ -1717,6 +1933,7 @@ function showCommandList() {
1717
1933
  ['watch', 'Live price monitoring + alerts'],
1718
1934
  ['gas', 'Gas prices & cost estimates'],
1719
1935
  ['trade', 'Swap tokens, snipe, trading'],
1936
+ ['auto', 'Autonomous trader strategies'],
1720
1937
  ['bridge', 'Cross-chain bridge (LI.FI)'],
1721
1938
  ['dca', 'Dollar-cost averaging orders'],
1722
1939
  ['ai chat', 'Standalone AI chat session'],
@@ -1727,6 +1944,7 @@ function showCommandList() {
1727
1944
  ['memory', 'Persistent cross-session memory'],
1728
1945
  ['script', 'Execution scripts & strategies'],
1729
1946
  ['market', 'Market intel & token data'],
1947
+ ['whale', 'Whale Radar - wallet tracking'],
1730
1948
  ['oracle', 'On-chain random oracle'],
1731
1949
  ['casino', 'The Clawsino - betting'],
1732
1950
  ['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,12 @@ const config = new Conf({
51
51
  maxOrders: 100,
52
52
  },
53
53
  },
54
+ autonomous: {
55
+ type: 'object',
56
+ default: {
57
+ strategies: [],
58
+ },
59
+ },
54
60
  services: {
55
61
  type: 'object',
56
62
  default: {
@@ -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 {