@darksol/terminal 0.3.6 → 0.4.0-beta.2
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 +2 -1
- package/src/cli.js +142 -0
- package/src/config/keys.js +10 -2
- package/src/services/gas.js +116 -0
- package/src/services/mail.js +705 -0
- package/src/services/watch.js +159 -0
- package/src/ui/banner.js +4 -2
- package/src/wallet/history.js +96 -0
- package/src/wallet/portfolio.js +169 -0
|
@@ -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.2') +
|
|
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.2'));
|
|
48
48
|
console.log(theme.dim(' ─────────────────────────────'));
|
|
49
49
|
console.log('');
|
|
50
50
|
}
|
|
@@ -69,3 +69,5 @@ export function showDivider() {
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
|
|
73
|
+
|
|
@@ -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
|
+
}
|