@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 +1 -1
- package/src/cli.js +53 -0
- package/src/services/gas.js +116 -0
- package/src/services/watch.js +159 -0
- package/src/ui/banner.js +3 -2
- package/src/wallet/history.js +96 -0
- package/src/wallet/portfolio.js +169 -0
package/package.json
CHANGED
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.
|
|
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.
|
|
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
|
+
}
|