@darksol/terminal 0.3.6 → 0.4.0-beta.1

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.3.6",
3
+ "version": "0.4.0-beta.1",
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
@@ -4,6 +4,10 @@ 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
6
  import { createWallet, importWallet, showWallets, getBalance, useWallet, exportWallet } from './wallet/manager.js';
7
+ import { showPortfolio } from './wallet/portfolio.js';
8
+ import { showHistory } from './wallet/history.js';
9
+ import { showGas } from './services/gas.js';
10
+ import { watchPrice, checkPrices } from './services/watch.js';
7
11
  import { executeSwap } from './trading/swap.js';
8
12
  import { snipeToken, watchSnipe } from './trading/snipe.js';
9
13
  import { createDCA, listDCA, cancelDCA, runDCA } from './trading/dca.js';
@@ -69,6 +73,18 @@ export function cli(argv) {
69
73
  .description('Export wallet details')
70
74
  .action((name) => exportWallet(name));
71
75
 
76
+ wallet
77
+ .command('portfolio [name]')
78
+ .description('View balances across all EVM chains')
79
+ .action((name) => showPortfolio(name));
80
+
81
+ wallet
82
+ .command('history [name]')
83
+ .description('Recent transaction history')
84
+ .option('-c, --chain <chain>', 'Chain to check')
85
+ .option('-l, --limit <n>', 'Number of transactions', '10')
86
+ .action((name, opts) => showHistory(name, opts));
87
+
72
88
  // ═══════════════════════════════════════
73
89
  // TRADING COMMANDS
74
90
  // ═══════════════════════════════════════
@@ -294,6 +310,39 @@ export function cli(argv) {
294
310
  .description('Settle payment on-chain')
295
311
  .action((payment) => facilitatorSettle(payment));
296
312
 
313
+ // ═══════════════════════════════════════
314
+ // PORTFOLIO SHORTCUT
315
+ // ═══════════════════════════════════════
316
+ program
317
+ .command('portfolio [name]')
318
+ .description('Multi-chain balance view (shortcut for: wallet portfolio)')
319
+ .action((name) => showPortfolio(name));
320
+
321
+ // ═══════════════════════════════════════
322
+ // GAS COMMAND
323
+ // ═══════════════════════════════════════
324
+ program
325
+ .command('gas [chain]')
326
+ .description('Show current gas prices and estimated costs')
327
+ .action((chain) => showGas(chain));
328
+
329
+ // ═══════════════════════════════════════
330
+ // PRICE COMMANDS
331
+ // ═══════════════════════════════════════
332
+ program
333
+ .command('price <tokens...>')
334
+ .description('Quick price check for one or more tokens')
335
+ .action((tokens) => checkPrices(tokens));
336
+
337
+ program
338
+ .command('watch <token>')
339
+ .description('Live price monitoring with alerts')
340
+ .option('-i, --interval <sec>', 'Poll interval in seconds', '10')
341
+ .option('--above <price>', 'Alert when price goes above')
342
+ .option('--below <price>', 'Alert when price drops below')
343
+ .option('-d, --duration <min>', 'Run for N minutes then stop')
344
+ .action((token, opts) => watchPrice(token, opts));
345
+
297
346
  // ═══════════════════════════════════════
298
347
  // CHAT SHORTCUT (darksol chat = darksol ai chat)
299
348
  // ═══════════════════════════════════════
@@ -776,6 +825,10 @@ function showCommandList() {
776
825
  showSection('COMMANDS');
777
826
  const commands = [
778
827
  ['wallet', 'Create, import, manage wallets'],
828
+ ['portfolio', 'Multi-chain balance view'],
829
+ ['price', 'Quick token price check'],
830
+ ['watch', 'Live price monitoring + alerts'],
831
+ ['gas', 'Gas prices & cost estimates'],
779
832
  ['trade', 'Swap tokens, snipe, trading'],
780
833
  ['dca', 'Dollar-cost averaging orders'],
781
834
  ['ai chat', 'Standalone AI chat session'],
@@ -0,0 +1,116 @@
1
+ import { ethers } from 'ethers';
2
+ import { getConfig, getRPC } from '../config/store.js';
3
+ import { theme } from '../ui/theme.js';
4
+ import { spinner, kvDisplay, success, error, warn, info } from '../ui/components.js';
5
+ import { showSection } from '../ui/banner.js';
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);
17
+ const provider = new ethers.JsonRpcProvider(rpc);
18
+
19
+ const spin = spinner(`Fetching gas on ${chain}...`).start();
20
+
21
+ try {
22
+ const feeData = await provider.getFeeData();
23
+ const block = await provider.getBlock('latest');
24
+
25
+ spin.succeed('Gas data fetched');
26
+
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
+ console.log('');
33
+ showSection(`GAS — ${chain.toUpperCase()}`);
34
+ 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 || '?'}`],
40
+ ]);
41
+
42
+ // Estimate common operations
43
+ console.log('');
44
+ showSection('ESTIMATED COSTS');
45
+
46
+ const ethPrice = await getETHPrice();
47
+ const estimates = [
48
+ { name: 'ETH Transfer', gas: 21000n },
49
+ { name: 'ERC-20 Transfer', gas: 65000n },
50
+ { name: 'ERC-20 Approve', gas: 46000n },
51
+ { name: 'Uniswap Swap', gas: 200000n },
52
+ { name: 'Uniswap + Approve', gas: 250000n },
53
+ { name: 'Contract Deploy (small)', gas: 500000n },
54
+ ];
55
+
56
+ estimates.forEach(({ name, gas }) => {
57
+ const costWei = gas * (feeData.gasPrice || 0n);
58
+ const costETH = parseFloat(ethers.formatEther(costWei));
59
+ const costUSD = costETH * ethPrice;
60
+ const label = name.padEnd(24);
61
+ const ethStr = costETH < 0.000001 ? '<$0.01' : `${costETH.toFixed(6)} ETH`;
62
+ const usdStr = costUSD < 0.01 ? '<$0.01' : `$${costUSD.toFixed(2)}`;
63
+ console.log(` ${theme.dim(label)} ${ethStr.padEnd(16)} ${theme.gold(usdStr)}`);
64
+ });
65
+
66
+ console.log('');
67
+ info(`ETH price: $${ethPrice.toFixed(2)}`);
68
+ console.log('');
69
+
70
+ return { chain, gasPrice, baseFee, maxFee, maxPriority, ethPrice };
71
+ } catch (err) {
72
+ spin.fail('Failed to fetch gas data');
73
+ error(err.message);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Get ETH price (CoinGecko, fallback estimate)
79
+ */
80
+ async function getETHPrice() {
81
+ try {
82
+ const fetch = (await import('node-fetch')).default;
83
+ const resp = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
84
+ const data = await resp.json();
85
+ return data.ethereum?.usd || 3000;
86
+ } catch {
87
+ return 3000;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Estimate gas for a specific transaction (pre-trade check)
93
+ */
94
+ export async function estimateTradeGas(txParams, chain) {
95
+ chain = chain || getConfig('chain') || 'base';
96
+ const rpc = getRPC(chain);
97
+ const provider = new ethers.JsonRpcProvider(rpc);
98
+
99
+ try {
100
+ const gasEstimate = await provider.estimateGas(txParams);
101
+ const feeData = await provider.getFeeData();
102
+ const costWei = gasEstimate * (feeData.gasPrice || 0n);
103
+ const costETH = parseFloat(ethers.formatEther(costWei));
104
+ const ethPrice = await getETHPrice();
105
+ const costUSD = costETH * ethPrice;
106
+
107
+ return {
108
+ gas: gasEstimate.toString(),
109
+ costETH: costETH.toFixed(6),
110
+ costUSD: costUSD.toFixed(2),
111
+ gasPrice: feeData.gasPrice ? ethers.formatUnits(feeData.gasPrice, 'gwei') : '0',
112
+ };
113
+ } catch (err) {
114
+ return { error: err.message };
115
+ }
116
+ }
@@ -0,0 +1,159 @@
1
+ import fetch from 'node-fetch';
2
+ import { theme } from '../ui/theme.js';
3
+ import { spinner, kvDisplay, success, error, warn, info } from '../ui/components.js';
4
+ import { showSection, showDivider } from '../ui/banner.js';
5
+
6
+ // ══════════════════════════════════════════════════
7
+ // PRICE WATCH — Live price monitoring with alerts
8
+ // ══════════════════════════════════════════════════
9
+
10
+ /**
11
+ * Watch a token's price with optional alert thresholds
12
+ */
13
+ export async function watchPrice(token, opts = {}) {
14
+ const interval = parseInt(opts.interval || '10') * 1000; // seconds → ms
15
+ const above = opts.above ? parseFloat(opts.above) : null;
16
+ const below = opts.below ? parseFloat(opts.below) : null;
17
+ const duration = opts.duration ? parseInt(opts.duration) * 60 * 1000 : null; // minutes → ms
18
+
19
+ console.log('');
20
+ showSection(`PRICE WATCH — ${token.toUpperCase()}`);
21
+
22
+ if (above) info(`Alert above: $${above}`);
23
+ if (below) info(`Alert below: $${below}`);
24
+ info(`Polling every ${interval / 1000}s`);
25
+ if (duration) info(`Running for ${duration / 60000} minutes`);
26
+ console.log('');
27
+ info('Press Ctrl+C to stop');
28
+ console.log('');
29
+
30
+ const startTime = Date.now();
31
+ let lastPrice = null;
32
+ let ticks = 0;
33
+
34
+ const poll = async () => {
35
+ try {
36
+ const resp = await fetch(`https://api.dexscreener.com/latest/dex/search?q=${token}`);
37
+ const data = await resp.json();
38
+ const pair = data.pairs?.[0];
39
+
40
+ if (!pair) {
41
+ console.log(theme.dim(` [${timestamp()}] No data for ${token}`));
42
+ return;
43
+ }
44
+
45
+ const price = parseFloat(pair.priceUsd);
46
+ const change24h = pair.priceChange?.h24 || 0;
47
+ const volume = pair.volume?.h24 || 0;
48
+
49
+ // Price change indicator
50
+ let arrow = ' ';
51
+ if (lastPrice !== null) {
52
+ if (price > lastPrice) arrow = theme.success('▲ ');
53
+ else if (price < lastPrice) arrow = theme.accent('▼ ');
54
+ else arrow = theme.dim('= ');
55
+ }
56
+
57
+ const priceStr = formatWatchPrice(price);
58
+ const changeStr = change24h >= 0 ? theme.success(`+${change24h.toFixed(2)}%`) : theme.accent(`${change24h.toFixed(2)}%`);
59
+ const volStr = volume > 1000000 ? `$${(volume / 1000000).toFixed(1)}M` : `$${(volume / 1000).toFixed(0)}K`;
60
+
61
+ console.log(` ${theme.dim(timestamp())} ${arrow}${theme.gold(priceStr.padEnd(14))} ${changeStr.padEnd(20)} vol: ${theme.dim(volStr)}`);
62
+
63
+ // Alert checks
64
+ if (above && price >= above) {
65
+ console.log('');
66
+ console.log(theme.success(` 🔔 ALERT: ${pair.baseToken.symbol} hit $${price} (above $${above})`));
67
+ console.log('');
68
+ }
69
+
70
+ if (below && price <= below) {
71
+ console.log('');
72
+ console.log(theme.accent(` 🔔 ALERT: ${pair.baseToken.symbol} dropped to $${price} (below $${below})`));
73
+ console.log('');
74
+ }
75
+
76
+ lastPrice = price;
77
+ ticks++;
78
+ } catch (err) {
79
+ console.log(theme.dim(` [${timestamp()}] Error: ${err.message}`));
80
+ }
81
+ };
82
+
83
+ // Initial fetch
84
+ await poll();
85
+
86
+ // Polling loop
87
+ const timer = setInterval(async () => {
88
+ if (duration && (Date.now() - startTime) >= duration) {
89
+ clearInterval(timer);
90
+ console.log('');
91
+ info(`Watch ended after ${duration / 60000} minutes (${ticks} ticks)`);
92
+ return;
93
+ }
94
+ await poll();
95
+ }, interval);
96
+
97
+ // Keep alive
98
+ await new Promise((resolve) => {
99
+ process.on('SIGINT', () => {
100
+ clearInterval(timer);
101
+ console.log('');
102
+ info(`Watched ${ticks} ticks`);
103
+ resolve();
104
+ });
105
+ if (duration) {
106
+ setTimeout(() => {
107
+ clearInterval(timer);
108
+ resolve();
109
+ }, duration + 1000);
110
+ }
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Quick price check (one-shot, multiple tokens)
116
+ */
117
+ export async function checkPrices(tokens) {
118
+ if (!tokens || tokens.length === 0) {
119
+ error('Specify tokens: darksol price ETH AERO VIRTUAL');
120
+ return;
121
+ }
122
+
123
+ console.log('');
124
+ showSection('PRICE CHECK');
125
+
126
+ for (const token of tokens) {
127
+ try {
128
+ const resp = await fetch(`https://api.dexscreener.com/latest/dex/search?q=${token}`);
129
+ const data = await resp.json();
130
+ const pair = data.pairs?.[0];
131
+
132
+ if (!pair) {
133
+ console.log(` ${theme.dim(token.toUpperCase().padEnd(10))} ${theme.dim('Not found')}`);
134
+ continue;
135
+ }
136
+
137
+ const price = parseFloat(pair.priceUsd);
138
+ const change = pair.priceChange?.h24 || 0;
139
+ const changeStr = change >= 0 ? theme.success(`+${change.toFixed(2)}%`) : theme.accent(`${change.toFixed(2)}%`);
140
+
141
+ console.log(` ${theme.gold(pair.baseToken.symbol.padEnd(10))} ${formatWatchPrice(price).padEnd(14)} ${changeStr}`);
142
+ } catch {
143
+ console.log(` ${theme.dim(token.padEnd(10))} ${theme.dim('Error')}`);
144
+ }
145
+ }
146
+
147
+ console.log('');
148
+ }
149
+
150
+ // Helpers
151
+ function timestamp() {
152
+ return new Date().toLocaleTimeString('en-US', { hour12: false });
153
+ }
154
+
155
+ function formatWatchPrice(price) {
156
+ if (price >= 1) return `$${price.toFixed(2)}`;
157
+ if (price >= 0.01) return `$${price.toFixed(4)}`;
158
+ return `$${price.toFixed(8)}`;
159
+ }
package/src/ui/banner.js CHANGED
@@ -26,7 +26,7 @@ export function showBanner(opts = {}) {
26
26
  );
27
27
  console.log(
28
28
  theme.dim(' ║ ') +
29
- theme.subtle(' v0.3.6') +
29
+ theme.subtle(' v0.4.0-beta.1') +
30
30
  theme.dim(' ') +
31
31
  theme.gold('🌑') +
32
32
  theme.dim(' ║')
@@ -44,7 +44,7 @@ export function showBanner(opts = {}) {
44
44
 
45
45
  export function showMiniBanner() {
46
46
  console.log('');
47
- console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(' v0.3.6'));
47
+ console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(' v0.4.0-beta.1'));
48
48
  console.log(theme.dim(' ─────────────────────────────'));
49
49
  console.log('');
50
50
  }
@@ -69,3 +69,4 @@ export function showDivider() {
69
69
 
70
70
 
71
71
 
72
+
@@ -0,0 +1,96 @@
1
+ import fetch from 'node-fetch';
2
+ import { loadWallet } from './keystore.js';
3
+ import { getConfig, getRPC } from '../config/store.js';
4
+ import { theme } from '../ui/theme.js';
5
+ import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
6
+ import { showSection } from '../ui/banner.js';
7
+
8
+ // ══════════════════════════════════════════════════
9
+ // TRANSACTION HISTORY
10
+ // ══════════════════════════════════════════════════
11
+
12
+ const EXPLORER_APIS = {
13
+ base: { api: 'https://api.basescan.org/api', explorer: 'https://basescan.org' },
14
+ ethereum: { api: 'https://api.etherscan.io/api', explorer: 'https://etherscan.io' },
15
+ arbitrum: { api: 'https://api.arbiscan.io/api', explorer: 'https://arbiscan.io' },
16
+ optimism: { api: 'https://api-optimistic.etherscan.io/api', explorer: 'https://optimistic.etherscan.io' },
17
+ polygon: { api: 'https://api.polygonscan.com/api', explorer: 'https://polygonscan.com' },
18
+ };
19
+
20
+ /**
21
+ * Show recent transaction history
22
+ */
23
+ export async function showHistory(walletName, opts = {}) {
24
+ const name = walletName || getConfig('activeWallet');
25
+ if (!name) {
26
+ error('No wallet specified. Use: darksol wallet history <name>');
27
+ return;
28
+ }
29
+
30
+ const walletData = loadWallet(name);
31
+ const address = walletData.address;
32
+ const chain = opts.chain || walletData.chain || getConfig('chain') || 'base';
33
+ const limit = parseInt(opts.limit || '10');
34
+
35
+ const explorerConfig = EXPLORER_APIS[chain];
36
+ if (!explorerConfig) {
37
+ error(`No explorer API for chain: ${chain}`);
38
+ return;
39
+ }
40
+
41
+ const spin = spinner(`Fetching history on ${chain}...`).start();
42
+
43
+ try {
44
+ // Fetch normal transactions
45
+ const url = `${explorerConfig.api}?module=account&action=txlist&address=${address}&startblock=0&endblock=99999999&page=1&offset=${limit}&sort=desc`;
46
+ const resp = await fetch(url);
47
+ const data = await resp.json();
48
+
49
+ if (data.status !== '1' || !data.result?.length) {
50
+ spin.succeed('No transactions found');
51
+ info(`No recent transactions on ${chain}`);
52
+ return;
53
+ }
54
+
55
+ spin.succeed(`Found ${data.result.length} transactions`);
56
+
57
+ console.log('');
58
+ showSection(`HISTORY — ${name} (${chain})`);
59
+ console.log(theme.dim(` ${address}`));
60
+ console.log('');
61
+
62
+ const rows = data.result.map(tx => {
63
+ const isOutgoing = tx.from.toLowerCase() === address.toLowerCase();
64
+ const direction = isOutgoing ? theme.accent('OUT →') : theme.success('← IN');
65
+ const value = parseFloat(tx.value) / 1e18;
66
+ const valueStr = value > 0 ? `${value.toFixed(4)} ETH` : theme.dim('0 ETH');
67
+ const status = tx.isError === '0' ? theme.success('✓') : theme.accent('✗');
68
+ const date = new Date(parseInt(tx.timeStamp) * 1000);
69
+ const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
70
+ const timeStr = date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
71
+ const counterparty = isOutgoing ? tx.to : tx.from;
72
+ const shortAddr = counterparty ? `${counterparty.slice(0, 6)}...${counterparty.slice(-4)}` : '—';
73
+ const method = tx.functionName ? tx.functionName.split('(')[0] : (value > 0 ? 'transfer' : '—');
74
+
75
+ return [
76
+ status,
77
+ direction,
78
+ valueStr,
79
+ shortAddr,
80
+ method.slice(0, 16),
81
+ `${dateStr} ${timeStr}`,
82
+ ];
83
+ });
84
+
85
+ table(['', 'Dir', 'Value', 'Address', 'Method', 'Date'], rows);
86
+
87
+ console.log('');
88
+ info(`Explorer: ${explorerConfig.explorer}/address/${address}`);
89
+ console.log('');
90
+
91
+ } catch (err) {
92
+ spin.fail('Failed to fetch history');
93
+ error(err.message);
94
+ info('Some explorer APIs require an API key for reliable access.');
95
+ }
96
+ }
@@ -0,0 +1,169 @@
1
+ import { ethers } from 'ethers';
2
+ import { loadWallet, listWallets } from './keystore.js';
3
+ import { getConfig, getRPC } from '../config/store.js';
4
+ import { theme } from '../ui/theme.js';
5
+ import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
6
+ import { showSection } from '../ui/banner.js';
7
+
8
+ // ══════════════════════════════════════════════════
9
+ // MULTI-CHAIN PORTFOLIO VIEW
10
+ // ══════════════════════════════════════════════════
11
+
12
+ const CHAINS = {
13
+ base: { name: 'Base', rpc: 'https://mainnet.base.org', usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', explorer: 'https://basescan.org' },
14
+ ethereum: { name: 'Ethereum', rpc: 'https://eth.llamarpc.com', usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', explorer: 'https://etherscan.io' },
15
+ arbitrum: { name: 'Arbitrum', rpc: 'https://arb1.arbitrum.io/rpc', usdc: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', explorer: 'https://arbiscan.io' },
16
+ optimism: { name: 'Optimism', rpc: 'https://mainnet.optimism.io', usdc: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', explorer: 'https://optimistic.etherscan.io' },
17
+ polygon: { name: 'Polygon', rpc: 'https://polygon-rpc.com', usdc: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', explorer: 'https://polygonscan.com' },
18
+ };
19
+
20
+ const ERC20_ABI = [
21
+ 'function balanceOf(address) view returns (uint256)',
22
+ 'function decimals() view returns (uint8)',
23
+ ];
24
+
25
+ /**
26
+ * Show portfolio across all EVM chains
27
+ */
28
+ export async function showPortfolio(walletName, opts = {}) {
29
+ const name = walletName || getConfig('activeWallet');
30
+ if (!name) {
31
+ error('No wallet specified. Use: darksol wallet portfolio <name>');
32
+ return;
33
+ }
34
+
35
+ const walletData = loadWallet(name);
36
+ const address = walletData.address;
37
+
38
+ console.log('');
39
+ showSection(`PORTFOLIO — ${name}`);
40
+ console.log(theme.dim(` ${address}`));
41
+ console.log('');
42
+
43
+ const spin = spinner('Scanning all chains...').start();
44
+ const results = [];
45
+ let totalUSD = 0;
46
+
47
+ // Fetch ETH price for USD conversion
48
+ let ethPrice = 0;
49
+ try {
50
+ const fetch = (await import('node-fetch')).default;
51
+ const priceResp = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
52
+ const priceData = await priceResp.json();
53
+ ethPrice = priceData.ethereum?.usd || 0;
54
+ } catch { ethPrice = 3000; /* fallback estimate */ }
55
+
56
+ // Scan each chain in parallel
57
+ const chainPromises = Object.entries(CHAINS).map(async ([chainId, chain]) => {
58
+ try {
59
+ const rpc = getRPC(chainId) || chain.rpc;
60
+ const provider = new ethers.JsonRpcProvider(rpc);
61
+
62
+ // ETH balance
63
+ const balance = await provider.getBalance(address);
64
+ const ethBal = parseFloat(ethers.formatEther(balance));
65
+
66
+ // USDC balance
67
+ let usdcBal = 0;
68
+ if (chain.usdc) {
69
+ try {
70
+ const usdc = new ethers.Contract(chain.usdc, ERC20_ABI, provider);
71
+ const raw = await usdc.balanceOf(address);
72
+ const decimals = await usdc.decimals();
73
+ usdcBal = parseFloat(ethers.formatUnits(raw, decimals));
74
+ } catch { }
75
+ }
76
+
77
+ const ethUSD = ethBal * ethPrice;
78
+ const chainTotal = ethUSD + usdcBal;
79
+
80
+ return {
81
+ chain: chain.name,
82
+ chainId,
83
+ eth: ethBal,
84
+ usdc: usdcBal,
85
+ ethUSD,
86
+ total: chainTotal,
87
+ explorer: chain.explorer,
88
+ };
89
+ } catch (err) {
90
+ return {
91
+ chain: chain.name,
92
+ chainId,
93
+ eth: 0,
94
+ usdc: 0,
95
+ ethUSD: 0,
96
+ total: 0,
97
+ error: err.message,
98
+ };
99
+ }
100
+ });
101
+
102
+ const chainResults = await Promise.all(chainPromises);
103
+ spin.succeed('Scan complete');
104
+
105
+ // Build table
106
+ const rows = chainResults.map(r => {
107
+ const ethStr = r.eth > 0 ? `${r.eth.toFixed(6)} ETH` : theme.dim('0');
108
+ const usdcStr = r.usdc > 0 ? `$${r.usdc.toFixed(2)}` : theme.dim('$0');
109
+ const totalStr = r.total > 0.01 ? theme.gold(`$${r.total.toFixed(2)}`) : theme.dim('$0');
110
+ const status = r.error ? theme.accent('⚠') : (r.total > 0 ? theme.success('●') : theme.dim('○'));
111
+ totalUSD += r.total;
112
+
113
+ return [
114
+ `${status} ${r.chain}`,
115
+ ethStr,
116
+ usdcStr,
117
+ totalStr,
118
+ ];
119
+ });
120
+
121
+ console.log('');
122
+ table(['Chain', 'ETH', 'USDC', 'Total USD'], rows);
123
+
124
+ // Summary
125
+ console.log('');
126
+ kvDisplay([
127
+ ['Total Value', theme.gold(`$${totalUSD.toFixed(2)}`)],
128
+ ['ETH Price', `$${ethPrice.toFixed(2)}`],
129
+ ['Chains', `${chainResults.filter(r => !r.error).length}/${Object.keys(CHAINS).length} connected`],
130
+ ]);
131
+
132
+ // Show explorer links for chains with balance
133
+ const withBalance = chainResults.filter(r => r.total > 0.01);
134
+ if (withBalance.length > 0) {
135
+ console.log('');
136
+ info('Explorer links:');
137
+ withBalance.forEach(r => {
138
+ console.log(theme.dim(` ${r.chain}: ${r.explorer}/address/${address}`));
139
+ });
140
+ }
141
+
142
+ console.log('');
143
+ return { address, chains: chainResults, totalUSD, ethPrice };
144
+ }
145
+
146
+ /**
147
+ * Quick balance check (non-verbose, for status bar)
148
+ */
149
+ export async function quickBalance(walletName) {
150
+ const name = walletName || getConfig('activeWallet');
151
+ if (!name) return null;
152
+
153
+ try {
154
+ const walletData = loadWallet(name);
155
+ const chain = walletData.chain || getConfig('chain') || 'base';
156
+ const rpc = getRPC(chain) || CHAINS[chain]?.rpc;
157
+ if (!rpc) return null;
158
+
159
+ const provider = new ethers.JsonRpcProvider(rpc);
160
+ const balance = await provider.getBalance(walletData.address);
161
+ return {
162
+ address: walletData.address,
163
+ chain,
164
+ eth: parseFloat(ethers.formatEther(balance)),
165
+ };
166
+ } catch {
167
+ return null;
168
+ }
169
+ }