@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.
@@ -1,93 +1,94 @@
1
1
  import fetch from 'node-fetch';
2
2
  import { loadWallet } from './keystore.js';
3
- import { getConfig, getRPC } from '../config/store.js';
3
+ import { getConfig } from '../config/store.js';
4
4
  import { theme } from '../ui/theme.js';
5
- import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
5
+ import { spinner, error, info, table } from '../ui/components.js';
6
6
  import { showSection } from '../ui/banner.js';
7
7
 
8
- // ══════════════════════════════════════════════════
9
- // TRANSACTION HISTORY
10
- // ══════════════════════════════════════════════════
11
-
12
8
  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' },
9
+ base: { api: 'https://api.basescan.org/api', explorer: 'https://basescan.org' },
10
+ ethereum: { api: 'https://api.etherscan.io/api', explorer: 'https://etherscan.io' },
11
+ arbitrum: { api: 'https://api.arbiscan.io/api', explorer: 'https://arbiscan.io' },
12
+ optimism: { api: 'https://api-optimistic.etherscan.io/api', explorer: 'https://optimistic.etherscan.io' },
13
+ polygon: { api: 'https://api.polygonscan.com/api', explorer: 'https://polygonscan.com' },
18
14
  };
19
15
 
20
- /**
21
- * Show recent transaction history
22
- */
23
- export async function showHistory(walletName, opts = {}) {
16
+ export async function fetchHistorySnapshot(walletName, opts = {}) {
24
17
  const name = walletName || getConfig('activeWallet');
25
18
  if (!name) {
26
- error('No wallet specified. Use: darksol wallet history <name>');
27
- return;
19
+ throw new Error('No wallet specified. Use: darksol wallet history <name>');
28
20
  }
29
21
 
30
22
  const walletData = loadWallet(name);
31
23
  const address = walletData.address;
32
24
  const chain = opts.chain || walletData.chain || getConfig('chain') || 'base';
33
- const limit = parseInt(opts.limit || '10');
34
-
25
+ const limit = parseInt(opts.limit || '10', 10);
35
26
  const explorerConfig = EXPLORER_APIS[chain];
27
+
36
28
  if (!explorerConfig) {
37
- error(`No explorer API for chain: ${chain}`);
29
+ throw new Error(`No explorer API for chain: ${chain}`);
30
+ }
31
+
32
+ const url = `${explorerConfig.api}?module=account&action=txlist&address=${address}&startblock=0&endblock=99999999&page=1&offset=${limit}&sort=desc`;
33
+ const resp = await fetch(url);
34
+ const data = await resp.json();
35
+
36
+ return {
37
+ name,
38
+ address,
39
+ chain,
40
+ explorer: explorerConfig.explorer,
41
+ status: data.status,
42
+ transactions: Array.isArray(data.result) ? data.result : [],
43
+ };
44
+ }
45
+
46
+ export async function showHistory(walletName, opts = {}) {
47
+ const name = walletName || getConfig('activeWallet');
48
+ if (!name) {
49
+ error('No wallet specified. Use: darksol wallet history <name>');
38
50
  return;
39
51
  }
40
52
 
53
+ const chain = opts.chain || getConfig('chain') || 'base';
41
54
  const spin = spinner(`Fetching history on ${chain}...`).start();
42
55
 
43
56
  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();
57
+ const snapshot = await fetchHistorySnapshot(name, opts);
58
+ const { address, chain: resolvedChain, explorer, status, transactions } = snapshot;
48
59
 
49
- if (data.status !== '1' || !data.result?.length) {
60
+ if (status !== '1' || !transactions.length) {
50
61
  spin.succeed('No transactions found');
51
- info(`No recent transactions on ${chain}`);
62
+ info(`No recent transactions on ${resolvedChain}`);
52
63
  return;
53
64
  }
54
65
 
55
- spin.succeed(`Found ${data.result.length} transactions`);
66
+ spin.succeed(`Found ${transactions.length} transactions`);
56
67
 
57
68
  console.log('');
58
- showSection(`HISTORY ${name} (${chain})`);
69
+ showSection(`HISTORY - ${name} (${resolvedChain})`);
59
70
  console.log(theme.dim(` ${address}`));
60
71
  console.log('');
61
72
 
62
- const rows = data.result.map(tx => {
73
+ const rows = transactions.map((tx) => {
63
74
  const isOutgoing = tx.from.toLowerCase() === address.toLowerCase();
64
- const direction = isOutgoing ? theme.accent('OUT ') : theme.success(' IN');
75
+ const direction = isOutgoing ? theme.accent('OUT ->') : theme.success('<- IN');
65
76
  const value = parseFloat(tx.value) / 1e18;
66
77
  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);
78
+ const statusMark = tx.isError === '0' ? theme.success('OK') : theme.accent('XX');
79
+ const date = new Date(parseInt(tx.timeStamp, 10) * 1000);
69
80
  const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
70
81
  const timeStr = date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
71
82
  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
+ const shortAddr = counterparty ? `${counterparty.slice(0, 6)}...${counterparty.slice(-4)}` : '--';
84
+ const method = tx.functionName ? tx.functionName.split('(')[0] : (value > 0 ? 'transfer' : '--');
85
+ return [statusMark, direction, valueStr, shortAddr, method.slice(0, 16), `${dateStr} ${timeStr}`];
83
86
  });
84
87
 
85
88
  table(['', 'Dir', 'Value', 'Address', 'Method', 'Date'], rows);
86
-
87
89
  console.log('');
88
- info(`Explorer: ${explorerConfig.explorer}/address/${address}`);
90
+ info(`Explorer: ${explorer}/address/${address}`);
89
91
  console.log('');
90
-
91
92
  } catch (err) {
92
93
  spin.fail('Failed to fetch history');
93
94
  error(err.message);
@@ -1,20 +1,16 @@
1
1
  import { ethers } from 'ethers';
2
- import { loadWallet, listWallets } from './keystore.js';
2
+ import { loadWallet } from './keystore.js';
3
3
  import { getConfig, getRPC } from '../config/store.js';
4
4
  import { theme } from '../ui/theme.js';
5
- import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
5
+ import { spinner, kvDisplay, error, info, table } from '../ui/components.js';
6
6
  import { showSection } from '../ui/banner.js';
7
7
 
8
- // ══════════════════════════════════════════════════
9
- // MULTI-CHAIN PORTFOLIO VIEW
10
- // ══════════════════════════════════════════════════
11
-
12
8
  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' },
9
+ base: { name: 'Base', rpc: 'https://mainnet.base.org', usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', explorer: 'https://basescan.org' },
10
+ ethereum: { name: 'Ethereum', rpc: 'https://eth.llamarpc.com', usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', explorer: 'https://etherscan.io' },
11
+ arbitrum: { name: 'Arbitrum', rpc: 'https://arb1.arbitrum.io/rpc', usdc: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', explorer: 'https://arbiscan.io' },
12
+ optimism: { name: 'Optimism', rpc: 'https://mainnet.optimism.io', usdc: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', explorer: 'https://optimistic.etherscan.io' },
13
+ polygon: { name: 'Polygon', rpc: 'https://polygon-rpc.com', usdc: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', explorer: 'https://polygonscan.com' },
18
14
  };
19
15
 
20
16
  const ERC20_ABI = [
@@ -22,68 +18,50 @@ const ERC20_ABI = [
22
18
  'function decimals() view returns (uint8)',
23
19
  ];
24
20
 
25
- /**
26
- * Show portfolio across all EVM chains
27
- */
28
- export async function showPortfolio(walletName, opts = {}) {
21
+ export async function fetchPortfolioSnapshot(walletName) {
29
22
  const name = walletName || getConfig('activeWallet');
30
23
  if (!name) {
31
- error('No wallet specified. Use: darksol wallet portfolio <name>');
32
- return;
24
+ throw new Error('No wallet specified. Use: darksol wallet portfolio <name>');
33
25
  }
34
26
 
35
27
  const walletData = loadWallet(name);
36
28
  const address = walletData.address;
37
29
 
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
30
  let ethPrice = 0;
49
31
  try {
50
32
  const fetch = (await import('node-fetch')).default;
51
33
  const priceResp = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
52
34
  const priceData = await priceResp.json();
53
35
  ethPrice = priceData.ethereum?.usd || 0;
54
- } catch { ethPrice = 3000; /* fallback estimate */ }
36
+ } catch {
37
+ ethPrice = 3000;
38
+ }
55
39
 
56
- // Scan each chain in parallel
57
- const chainPromises = Object.entries(CHAINS).map(async ([chainId, chain]) => {
40
+ const chains = await Promise.all(Object.entries(CHAINS).map(async ([chainId, chain]) => {
58
41
  try {
59
42
  const rpc = getRPC(chainId) || chain.rpc;
60
43
  const provider = new ethers.JsonRpcProvider(rpc);
61
-
62
- // ETH balance
63
44
  const balance = await provider.getBalance(address);
64
- const ethBal = parseFloat(ethers.formatEther(balance));
45
+ const eth = parseFloat(ethers.formatEther(balance));
65
46
 
66
- // USDC balance
67
- let usdcBal = 0;
47
+ let usdc = 0;
68
48
  if (chain.usdc) {
69
49
  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 { }
50
+ const usdcContract = new ethers.Contract(chain.usdc, ERC20_ABI, provider);
51
+ const raw = await usdcContract.balanceOf(address);
52
+ const decimals = await usdcContract.decimals();
53
+ usdc = parseFloat(ethers.formatUnits(raw, decimals));
54
+ } catch {}
75
55
  }
76
56
 
77
- const ethUSD = ethBal * ethPrice;
78
- const chainTotal = ethUSD + usdcBal;
79
-
57
+ const ethUSD = eth * ethPrice;
80
58
  return {
81
59
  chain: chain.name,
82
60
  chainId,
83
- eth: ethBal,
84
- usdc: usdcBal,
61
+ eth,
62
+ usdc,
85
63
  ethUSD,
86
- total: chainTotal,
64
+ total: ethUSD + usdc,
87
65
  explorer: chain.explorer,
88
66
  };
89
67
  } catch (err) {
@@ -94,58 +72,68 @@ export async function showPortfolio(walletName, opts = {}) {
94
72
  usdc: 0,
95
73
  ethUSD: 0,
96
74
  total: 0,
75
+ explorer: chain.explorer,
97
76
  error: err.message,
98
77
  };
99
78
  }
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
- });
79
+ }));
120
80
 
121
- console.log('');
122
- table(['Chain', 'ETH', 'USDC', 'Total USD'], rows);
81
+ const totalUSD = chains.reduce((sum, item) => sum + item.total, 0);
82
+ return { name, address, chains, totalUSD, ethPrice };
83
+ }
84
+
85
+ export async function showPortfolio(walletName) {
86
+ const name = walletName || getConfig('activeWallet');
87
+ if (!name) {
88
+ error('No wallet specified. Use: darksol wallet portfolio <name>');
89
+ return;
90
+ }
123
91
 
124
- // Summary
125
92
  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) {
93
+ showSection(`PORTFOLIO - ${name}`);
94
+ const spin = spinner('Scanning all chains...').start();
95
+
96
+ try {
97
+ const snapshot = await fetchPortfolioSnapshot(name);
98
+ const { address, chains, totalUSD, ethPrice } = snapshot;
99
+ spin.succeed('Scan complete');
100
+
101
+ console.log(theme.dim(` ${address}`));
135
102
  console.log('');
136
- info('Explorer links:');
137
- withBalance.forEach(r => {
138
- console.log(theme.dim(` ${r.chain}: ${r.explorer}/address/${address}`));
103
+
104
+ const rows = chains.map((item) => {
105
+ const ethStr = item.eth > 0 ? `${item.eth.toFixed(6)} ETH` : theme.dim('0');
106
+ const usdcStr = item.usdc > 0 ? `$${item.usdc.toFixed(2)}` : theme.dim('$0');
107
+ const totalStr = item.total > 0.01 ? theme.gold(`$${item.total.toFixed(2)}`) : theme.dim('$0');
108
+ const status = item.error ? theme.accent('!') : (item.total > 0 ? theme.success('*') : theme.dim('o'));
109
+ return [`${status} ${item.chain}`, ethStr, usdcStr, totalStr];
139
110
  });
140
- }
141
111
 
142
- console.log('');
143
- return { address, chains: chainResults, totalUSD, ethPrice };
112
+ table(['Chain', 'ETH', 'USDC', 'Total USD'], rows);
113
+ console.log('');
114
+ kvDisplay([
115
+ ['Total Value', theme.gold(`$${totalUSD.toFixed(2)}`)],
116
+ ['ETH Price', `$${ethPrice.toFixed(2)}`],
117
+ ['Chains', `${chains.filter((item) => !item.error).length}/${Object.keys(CHAINS).length} connected`],
118
+ ]);
119
+
120
+ const withBalance = chains.filter((item) => item.total > 0.01);
121
+ if (withBalance.length > 0) {
122
+ console.log('');
123
+ info('Explorer links:');
124
+ withBalance.forEach((item) => {
125
+ console.log(theme.dim(` ${item.chain}: ${item.explorer}/address/${address}`));
126
+ });
127
+ }
128
+
129
+ console.log('');
130
+ return { address, chains, totalUSD, ethPrice };
131
+ } catch (err) {
132
+ spin.fail('Scan failed');
133
+ error(err.message);
134
+ }
144
135
  }
145
136
 
146
- /**
147
- * Quick balance check (non-verbose, for status bar)
148
- */
149
137
  export async function quickBalance(walletName) {
150
138
  const name = walletName || getConfig('activeWallet');
151
139
  if (!name) return null;
@@ -902,6 +902,8 @@ export async function handleCommand(cmd, ws) {
902
902
  return await cmdMarket(args, ws);
903
903
  case 'trade':
904
904
  return await cmdTrade(args, ws);
905
+ case 'arb':
906
+ return await cmdArb(args, ws);
905
907
  case 'bridge':
906
908
  return await cmdBridge(args, ws);
907
909
  case 'wallet':
@@ -1351,6 +1353,59 @@ async function cmdTrade(args, ws) {
1351
1353
  return {};
1352
1354
  }
1353
1355
 
1356
+ // ══════════════════════════════════════════════════
1357
+ // ARBITRAGE
1358
+ // ══════════════════════════════════════════════════
1359
+ async function cmdArb(args, ws) {
1360
+ const sub = (args[0] || '').toLowerCase();
1361
+
1362
+ if (sub === 'scan') {
1363
+ ws.sendLine(` ${ANSI.dim}Running arb scan...${ANSI.reset}`);
1364
+ try {
1365
+ const { arbScan } = await import('../trading/arb.js');
1366
+ await arbScan({ chain: args[1] || getConfig('chain') || 'base' });
1367
+ } catch (e) {
1368
+ ws.sendLine(` ${ANSI.red}Scan failed: ${e.message}${ANSI.reset}`);
1369
+ }
1370
+ return {};
1371
+ }
1372
+
1373
+ if (sub === 'stats') {
1374
+ try {
1375
+ const { arbStats } = await import('../trading/arb.js');
1376
+ await arbStats({ days: args[1] || '7' });
1377
+ } catch (e) {
1378
+ ws.sendLine(` ${ANSI.red}Stats failed: ${e.message}${ANSI.reset}`);
1379
+ }
1380
+ return {};
1381
+ }
1382
+
1383
+ if (sub === 'info') {
1384
+ try {
1385
+ const { arbInfo } = await import('../trading/arb.js');
1386
+ await arbInfo();
1387
+ } catch (e) {
1388
+ ws.sendLine(` ${ANSI.red}${e.message}${ANSI.reset}`);
1389
+ }
1390
+ return {};
1391
+ }
1392
+
1393
+ // Default: show arb menu
1394
+ ws.sendLine(`${ANSI.gold} ◆ ARBITRAGE${ANSI.reset}`);
1395
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1396
+ ws.sendLine(` ${ANSI.white}Cross-DEX arbitrage scanner${ANSI.reset}`);
1397
+ ws.sendLine('');
1398
+
1399
+ ws.sendMenu('arb_action', '◆ Arb Actions', [
1400
+ { value: 'arb scan', label: '🔍 Scan', desc: 'One-shot DEX price comparison' },
1401
+ { value: 'arb stats', label: '📊 Stats', desc: 'View arb history & PnL' },
1402
+ { value: 'arb info', label: '📖 Guide', desc: 'How arb works, setup tips, risks' },
1403
+ { value: 'back', label: '← Back', desc: '' },
1404
+ ]);
1405
+
1406
+ return {};
1407
+ }
1408
+
1354
1409
  // ══════════════════════════════════════════════════
1355
1410
  // BRIDGE (LI.FI cross-chain)
1356
1411
  // ══════════════════════════════════════════════════