@darksol/terminal 0.10.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.
- package/README.md +206 -1
- package/package.json +4 -1
- package/src/agent/autonomous.js +465 -0
- package/src/agent/strategy-evaluator.js +166 -0
- package/src/browser/actions.js +58 -0
- package/src/cli.js +354 -0
- package/src/config/keys.js +28 -2
- package/src/config/store.js +6 -0
- package/src/daemon/index.js +225 -0
- package/src/daemon/manager.js +148 -0
- package/src/daemon/pid.js +80 -0
- package/src/services/browser.js +659 -0
- package/src/services/gas.js +35 -42
- package/src/services/telegram.js +570 -0
- package/src/services/watch.js +67 -61
- package/src/services/whale-monitor.js +388 -0
- package/src/services/whale.js +421 -0
- package/src/ui/dashboard.js +596 -0
- package/src/wallet/history.js +47 -46
- package/src/wallet/portfolio.js +75 -87
- package/src/web/commands.js +135 -1
- package/src/web/server.js +21 -2
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import contrib from 'blessed-contrib';
|
|
3
|
+
import { getConfig, setConfig } from '../config/store.js';
|
|
4
|
+
import { fetchPortfolioSnapshot } from '../wallet/portfolio.js';
|
|
5
|
+
import { fetchHistorySnapshot } from '../wallet/history.js';
|
|
6
|
+
import { fetchGasSnapshot } from '../services/gas.js';
|
|
7
|
+
import { getPriceSnapshots } from '../services/watch.js';
|
|
8
|
+
|
|
9
|
+
export const CHAIN_KEYS = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'];
|
|
10
|
+
export const DEFAULT_TRACKED_TOKENS = ['ETH', 'USDC', 'AERO', 'VIRTUAL'];
|
|
11
|
+
export const DEFAULT_REFRESH_SECONDS = 30;
|
|
12
|
+
|
|
13
|
+
const tuiTheme = {
|
|
14
|
+
screen: '#050505',
|
|
15
|
+
panel: '#111111',
|
|
16
|
+
border: '#FFD700',
|
|
17
|
+
text: '#FFFFFF',
|
|
18
|
+
muted: '#666666',
|
|
19
|
+
accent: '#B8860B',
|
|
20
|
+
success: '#00ff88',
|
|
21
|
+
warning: '#ffaa00',
|
|
22
|
+
error: '#ff4444',
|
|
23
|
+
info: '#4488ff',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function normalizeRefreshSeconds(value) {
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
29
|
+
return DEFAULT_REFRESH_SECONDS;
|
|
30
|
+
}
|
|
31
|
+
return Math.max(5, Math.round(parsed));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createLayoutSpec(compact = false) {
|
|
35
|
+
if (compact) {
|
|
36
|
+
return {
|
|
37
|
+
compact: true,
|
|
38
|
+
panels: ['portfolio', 'prices', 'status'],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
compact: false,
|
|
44
|
+
panels: ['portfolio', 'prices', 'gas', 'transactions', 'whales', 'status'],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatCurrency(value) {
|
|
49
|
+
const amount = Number(value || 0);
|
|
50
|
+
return `$${amount.toFixed(2)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatPortfolioSummary(snapshot) {
|
|
54
|
+
if (!snapshot) {
|
|
55
|
+
return ['Waiting for portfolio data...'];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const activeChains = snapshot.chains.filter((item) => item.total > 0);
|
|
59
|
+
const totalEth = snapshot.chains.reduce((sum, item) => sum + item.eth, 0);
|
|
60
|
+
const totalUsdc = snapshot.chains.reduce((sum, item) => sum + item.usdc, 0);
|
|
61
|
+
const lines = [
|
|
62
|
+
`Wallet: ${snapshot.name || getConfig('activeWallet') || '(none)'}`,
|
|
63
|
+
`Address: ${shorten(snapshot.address, 10)}`,
|
|
64
|
+
`Total Value: ${formatCurrency(snapshot.totalUSD)}`,
|
|
65
|
+
`ETH: ${totalEth.toFixed(4)}`,
|
|
66
|
+
`USDC: ${totalUsdc.toFixed(2)}`,
|
|
67
|
+
`Chains: ${activeChains.length}/${snapshot.chains.length}`,
|
|
68
|
+
'',
|
|
69
|
+
'Chain Breakdown',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
snapshot.chains
|
|
73
|
+
.slice()
|
|
74
|
+
.sort((left, right) => right.total - left.total)
|
|
75
|
+
.forEach((item) => {
|
|
76
|
+
const flag = item.error ? '!' : (item.total > 0 ? '*' : '-');
|
|
77
|
+
lines.push(`${flag} ${item.chain.padEnd(9)} ${formatCurrency(item.total).padStart(10)}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatGasSummary(chainSnapshots) {
|
|
84
|
+
if (!chainSnapshots.length) {
|
|
85
|
+
return ['Waiting for gas data...'];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return chainSnapshots.map((item) => (
|
|
89
|
+
`${item.chain.toUpperCase().padEnd(9)} ${item.gasPrice.toFixed(2).padStart(7)} gwei #${item.blockNumber ?? '?'}`
|
|
90
|
+
));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function formatPriceRows(priceSnapshots) {
|
|
94
|
+
if (!priceSnapshots.length) {
|
|
95
|
+
return [['Token', 'Price', '24h']];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rows = [['Token', 'Price', '24h']];
|
|
99
|
+
priceSnapshots.forEach((item) => {
|
|
100
|
+
const change = item.change24h >= 0 ? `+${item.change24h.toFixed(2)}%` : `${item.change24h.toFixed(2)}%`;
|
|
101
|
+
rows.push([item.symbol || item.query, formatDynamicPrice(item.price), change]);
|
|
102
|
+
});
|
|
103
|
+
return rows;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function formatTransactionRows(historySnapshot) {
|
|
107
|
+
const rows = [['Dir', 'Value', 'Method', 'Time']];
|
|
108
|
+
if (!historySnapshot?.transactions?.length) {
|
|
109
|
+
rows.push(['-', '-', 'No transactions', '-']);
|
|
110
|
+
return rows;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
historySnapshot.transactions.slice(0, 10).forEach((tx) => {
|
|
114
|
+
const isOutgoing = tx.from?.toLowerCase() === historySnapshot.address?.toLowerCase();
|
|
115
|
+
const value = parseFloat(tx.value || '0') / 1e18;
|
|
116
|
+
const method = tx.functionName ? tx.functionName.split('(')[0] : (value > 0 ? 'transfer' : '-');
|
|
117
|
+
const date = new Date(parseInt(tx.timeStamp || '0', 10) * 1000);
|
|
118
|
+
rows.push([
|
|
119
|
+
isOutgoing ? 'OUT' : 'IN',
|
|
120
|
+
value > 0 ? `${value.toFixed(4)} ETH` : '0 ETH',
|
|
121
|
+
method.slice(0, 14),
|
|
122
|
+
Number.isNaN(date.getTime()) ? '-' : date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }),
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return rows;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatStatusBar(state) {
|
|
130
|
+
const wallet = state.walletName || '(no wallet)';
|
|
131
|
+
const chain = state.currentChain || 'base';
|
|
132
|
+
const block = state.blockNumber ? `#${state.blockNumber}` : '#-';
|
|
133
|
+
const refresh = `${Math.max(0, state.secondsUntilRefresh)}s`;
|
|
134
|
+
const whale = state.whaleFeedEnabled ? 'whales:on' : 'whales:off';
|
|
135
|
+
return ` wallet ${wallet} | chain ${chain} | block ${block} | refresh ${refresh} | ${whale} `;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createDashboard(options = {}, deps = {}) {
|
|
139
|
+
const blessedLib = deps.blessed || blessed;
|
|
140
|
+
const contribLib = deps.contrib || contrib;
|
|
141
|
+
const configApi = deps.config || { getConfig, setConfig };
|
|
142
|
+
const timers = deps.timers || globalThis;
|
|
143
|
+
const now = deps.now || (() => Date.now());
|
|
144
|
+
const layout = createLayoutSpec(Boolean(options.compact));
|
|
145
|
+
const trackedTokens = options.tokens?.length ? options.tokens : DEFAULT_TRACKED_TOKENS;
|
|
146
|
+
const refreshSeconds = normalizeRefreshSeconds(options.refresh);
|
|
147
|
+
|
|
148
|
+
const screen = deps.screen || blessedLib.screen({
|
|
149
|
+
smartCSR: true,
|
|
150
|
+
fullUnicode: true,
|
|
151
|
+
dockBorders: true,
|
|
152
|
+
title: 'DARKSOL Dashboard',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (screen.style) {
|
|
156
|
+
screen.style.bg = tuiTheme.screen;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const state = {
|
|
160
|
+
walletName: options.wallet || configApi.getConfig('activeWallet') || '',
|
|
161
|
+
currentChain: options.chain || configApi.getConfig('chain') || 'base',
|
|
162
|
+
refreshSeconds,
|
|
163
|
+
secondsUntilRefresh: refreshSeconds,
|
|
164
|
+
whaleFeedEnabled: true,
|
|
165
|
+
whaleAlerts: [],
|
|
166
|
+
blockNumber: null,
|
|
167
|
+
priceHistory: Object.fromEntries(trackedTokens.map((token) => [token, []])),
|
|
168
|
+
focusIndex: 0,
|
|
169
|
+
refreshCount: 0,
|
|
170
|
+
lastData: null,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const widgets = buildWidgets({ screen, blessedLib, contribLib, compact: layout.compact });
|
|
174
|
+
const focusables = widgets.focusables;
|
|
175
|
+
|
|
176
|
+
let refreshTimer = null;
|
|
177
|
+
let countdownTimer = null;
|
|
178
|
+
let whaleListener = null;
|
|
179
|
+
|
|
180
|
+
async function refreshDashboard() {
|
|
181
|
+
state.walletName = options.wallet || configApi.getConfig('activeWallet') || '';
|
|
182
|
+
state.currentChain = configApi.getConfig('chain') || state.currentChain;
|
|
183
|
+
|
|
184
|
+
const [portfolio, prices, history, gasSnapshots] = await Promise.all([
|
|
185
|
+
loadPortfolioData(state.walletName, deps),
|
|
186
|
+
loadPriceData(trackedTokens, deps),
|
|
187
|
+
loadHistoryData(state.walletName, state.currentChain, deps),
|
|
188
|
+
loadGasData(deps),
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
state.refreshCount += 1;
|
|
192
|
+
state.secondsUntilRefresh = state.refreshSeconds;
|
|
193
|
+
state.blockNumber = gasSnapshots.find((item) => item.chain === state.currentChain)?.blockNumber ?? gasSnapshots[0]?.blockNumber ?? null;
|
|
194
|
+
state.lastData = { portfolio, prices, history, gasSnapshots };
|
|
195
|
+
|
|
196
|
+
updatePortfolioPanel(widgets, portfolio);
|
|
197
|
+
updatePricesPanel(widgets, prices, state.priceHistory);
|
|
198
|
+
if (widgets.gasBar) {
|
|
199
|
+
updateGasPanel(widgets, gasSnapshots);
|
|
200
|
+
}
|
|
201
|
+
if (widgets.transactions) {
|
|
202
|
+
updateTransactionsPanel(widgets, history);
|
|
203
|
+
}
|
|
204
|
+
updateWhalePanel(widgets, state);
|
|
205
|
+
updateStatusPanel(widgets, state);
|
|
206
|
+
|
|
207
|
+
screen.render();
|
|
208
|
+
return state.lastData;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function scheduleRefresh() {
|
|
212
|
+
clearTimers();
|
|
213
|
+
refreshTimer = timers.setInterval(() => {
|
|
214
|
+
refreshDashboard().catch((err) => {
|
|
215
|
+
renderError(widgets, `Refresh failed: ${err.message}`);
|
|
216
|
+
});
|
|
217
|
+
}, state.refreshSeconds * 1000);
|
|
218
|
+
|
|
219
|
+
countdownTimer = timers.setInterval(() => {
|
|
220
|
+
state.secondsUntilRefresh = Math.max(0, state.secondsUntilRefresh - 1);
|
|
221
|
+
updateStatusPanel(widgets, state);
|
|
222
|
+
screen.render();
|
|
223
|
+
}, 1000);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function clearTimers() {
|
|
227
|
+
if (refreshTimer) timers.clearInterval(refreshTimer);
|
|
228
|
+
if (countdownTimer) timers.clearInterval(countdownTimer);
|
|
229
|
+
refreshTimer = null;
|
|
230
|
+
countdownTimer = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function stop() {
|
|
234
|
+
clearTimers();
|
|
235
|
+
detachWhaleFeed();
|
|
236
|
+
if (typeof screen.destroy === 'function') {
|
|
237
|
+
screen.destroy();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function cycleFocus() {
|
|
242
|
+
if (!focusables.length) return;
|
|
243
|
+
state.focusIndex = (state.focusIndex + 1) % focusables.length;
|
|
244
|
+
const widget = focusables[state.focusIndex];
|
|
245
|
+
if (typeof widget.focus === 'function') {
|
|
246
|
+
widget.focus();
|
|
247
|
+
}
|
|
248
|
+
screen.render();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function toggleWhaleFeed() {
|
|
252
|
+
state.whaleFeedEnabled = !state.whaleFeedEnabled;
|
|
253
|
+
updateWhalePanel(widgets, state);
|
|
254
|
+
updateStatusPanel(widgets, state);
|
|
255
|
+
screen.render();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function switchChain(index) {
|
|
259
|
+
const nextChain = CHAIN_KEYS[index];
|
|
260
|
+
if (!nextChain) return;
|
|
261
|
+
state.currentChain = nextChain;
|
|
262
|
+
configApi.setConfig('chain', nextChain);
|
|
263
|
+
refreshDashboard().catch((err) => {
|
|
264
|
+
renderError(widgets, `Chain switch failed: ${err.message}`);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function attachWhaleFeed() {
|
|
269
|
+
const emitter = options.whaleEmitter || deps.whaleEmitter;
|
|
270
|
+
if (!emitter || typeof emitter.on !== 'function') {
|
|
271
|
+
updateWhalePanel(widgets, state);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
whaleListener = (alert) => {
|
|
276
|
+
state.whaleAlerts.unshift(formatWhaleAlert(alert, now()));
|
|
277
|
+
state.whaleAlerts = state.whaleAlerts.slice(0, 10);
|
|
278
|
+
updateWhalePanel(widgets, state);
|
|
279
|
+
screen.render();
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
emitter.on('whale', whaleListener);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function detachWhaleFeed() {
|
|
286
|
+
const emitter = options.whaleEmitter || deps.whaleEmitter;
|
|
287
|
+
if (emitter && whaleListener && typeof emitter.off === 'function') {
|
|
288
|
+
emitter.off('whale', whaleListener);
|
|
289
|
+
} else if (emitter && whaleListener && typeof emitter.removeListener === 'function') {
|
|
290
|
+
emitter.removeListener('whale', whaleListener);
|
|
291
|
+
}
|
|
292
|
+
whaleListener = null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
bindKeys(screen, {
|
|
296
|
+
quit: stop,
|
|
297
|
+
refresh: () => refreshDashboard().catch((err) => renderError(widgets, `Refresh failed: ${err.message}`)),
|
|
298
|
+
cycleFocus,
|
|
299
|
+
toggleWhaleFeed,
|
|
300
|
+
switchChain,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
attachWhaleFeed();
|
|
304
|
+
updateWhalePanel(widgets, state);
|
|
305
|
+
updateStatusPanel(widgets, state);
|
|
306
|
+
scheduleRefresh();
|
|
307
|
+
|
|
308
|
+
const ready = refreshDashboard().catch((err) => {
|
|
309
|
+
renderError(widgets, `Initial load failed: ${err.message}`);
|
|
310
|
+
screen.render();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
screen,
|
|
315
|
+
widgets,
|
|
316
|
+
state,
|
|
317
|
+
refreshDashboard,
|
|
318
|
+
scheduleRefresh,
|
|
319
|
+
stop,
|
|
320
|
+
ready,
|
|
321
|
+
actions: { cycleFocus, toggleWhaleFeed, switchChain },
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function buildWidgets({ screen, blessedLib, contribLib, compact }) {
|
|
326
|
+
const focusables = [];
|
|
327
|
+
const register = (widget) => {
|
|
328
|
+
focusables.push(widget);
|
|
329
|
+
return widget;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const portfolio = register(blessedLib.box(panelConfig({
|
|
333
|
+
parent: screen,
|
|
334
|
+
label: ' Portfolio Summary ',
|
|
335
|
+
top: 0,
|
|
336
|
+
left: 0,
|
|
337
|
+
width: compact ? '50%' : '60%',
|
|
338
|
+
height: compact ? '100%-1' : '60%',
|
|
339
|
+
})));
|
|
340
|
+
|
|
341
|
+
const pricesContainer = blessedLib.box(panelConfig({
|
|
342
|
+
parent: screen,
|
|
343
|
+
label: ' Price Ticker ',
|
|
344
|
+
top: 0,
|
|
345
|
+
left: compact ? '50%' : '60%',
|
|
346
|
+
width: compact ? '50%' : '40%',
|
|
347
|
+
height: compact ? '100%-1' : '40%',
|
|
348
|
+
}));
|
|
349
|
+
|
|
350
|
+
const priceTable = register(contribLib.table({
|
|
351
|
+
parent: pricesContainer,
|
|
352
|
+
top: 0,
|
|
353
|
+
left: 0,
|
|
354
|
+
width: '100%',
|
|
355
|
+
height: compact ? '55%' : '50%',
|
|
356
|
+
keys: true,
|
|
357
|
+
interactive: true,
|
|
358
|
+
fg: tuiTheme.text,
|
|
359
|
+
bg: tuiTheme.panel,
|
|
360
|
+
border: { type: 'line', fg: tuiTheme.border },
|
|
361
|
+
columnSpacing: 2,
|
|
362
|
+
columnWidth: [10, 12, 10],
|
|
363
|
+
label: ' Tokens ',
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
const sparkline = register(contribLib.sparkline({
|
|
367
|
+
parent: pricesContainer,
|
|
368
|
+
top: compact ? '55%' : '50%',
|
|
369
|
+
left: 0,
|
|
370
|
+
width: '100%',
|
|
371
|
+
height: compact ? '45%' : '50%',
|
|
372
|
+
label: ' Micro Charts ',
|
|
373
|
+
style: {
|
|
374
|
+
fg: tuiTheme.border,
|
|
375
|
+
border: { fg: tuiTheme.border },
|
|
376
|
+
},
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
const widgets = {
|
|
380
|
+
portfolio,
|
|
381
|
+
priceTable,
|
|
382
|
+
sparkline,
|
|
383
|
+
status: blessedLib.box({
|
|
384
|
+
parent: screen,
|
|
385
|
+
bottom: 0,
|
|
386
|
+
left: 0,
|
|
387
|
+
width: '100%',
|
|
388
|
+
height: 1,
|
|
389
|
+
tags: false,
|
|
390
|
+
style: {
|
|
391
|
+
fg: tuiTheme.screen,
|
|
392
|
+
bg: tuiTheme.border,
|
|
393
|
+
},
|
|
394
|
+
content: '',
|
|
395
|
+
}),
|
|
396
|
+
focusables,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
if (!compact) {
|
|
400
|
+
widgets.gasBar = register(contribLib.bar({
|
|
401
|
+
parent: screen,
|
|
402
|
+
label: ' Gas Gauge ',
|
|
403
|
+
top: '40%',
|
|
404
|
+
left: '60%',
|
|
405
|
+
width: '40%',
|
|
406
|
+
height: '25%',
|
|
407
|
+
barWidth: 8,
|
|
408
|
+
barSpacing: 3,
|
|
409
|
+
xOffset: 1,
|
|
410
|
+
maxHeight: 100,
|
|
411
|
+
border: { type: 'line', fg: tuiTheme.border },
|
|
412
|
+
fg: tuiTheme.text,
|
|
413
|
+
bg: tuiTheme.panel,
|
|
414
|
+
}));
|
|
415
|
+
|
|
416
|
+
widgets.transactions = register(contribLib.table({
|
|
417
|
+
parent: screen,
|
|
418
|
+
label: ' Recent Transactions ',
|
|
419
|
+
top: '60%',
|
|
420
|
+
left: 0,
|
|
421
|
+
width: '60%',
|
|
422
|
+
height: '39%-1',
|
|
423
|
+
keys: true,
|
|
424
|
+
interactive: true,
|
|
425
|
+
fg: tuiTheme.text,
|
|
426
|
+
bg: tuiTheme.panel,
|
|
427
|
+
border: { type: 'line', fg: tuiTheme.border },
|
|
428
|
+
columnSpacing: 2,
|
|
429
|
+
columnWidth: [8, 14, 16, 10],
|
|
430
|
+
}));
|
|
431
|
+
|
|
432
|
+
widgets.whales = register(blessedLib.box(panelConfig({
|
|
433
|
+
parent: screen,
|
|
434
|
+
label: ' Whale Feed ',
|
|
435
|
+
top: '65%',
|
|
436
|
+
left: '60%',
|
|
437
|
+
width: '40%',
|
|
438
|
+
height: '34%-1',
|
|
439
|
+
scrollable: true,
|
|
440
|
+
alwaysScroll: true,
|
|
441
|
+
keys: true,
|
|
442
|
+
vi: true,
|
|
443
|
+
})));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return widgets;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function panelConfig(overrides = {}) {
|
|
450
|
+
return {
|
|
451
|
+
border: 'line',
|
|
452
|
+
tags: false,
|
|
453
|
+
keys: true,
|
|
454
|
+
vi: true,
|
|
455
|
+
mouse: true,
|
|
456
|
+
scrollable: true,
|
|
457
|
+
alwaysScroll: true,
|
|
458
|
+
style: {
|
|
459
|
+
fg: tuiTheme.text,
|
|
460
|
+
bg: tuiTheme.panel,
|
|
461
|
+
border: { fg: tuiTheme.border },
|
|
462
|
+
label: { fg: tuiTheme.accent },
|
|
463
|
+
focus: { border: { fg: tuiTheme.info } },
|
|
464
|
+
},
|
|
465
|
+
content: '',
|
|
466
|
+
...overrides,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function loadPortfolioData(walletName, deps) {
|
|
471
|
+
const loader = deps.fetchPortfolioSnapshot || fetchPortfolioSnapshot;
|
|
472
|
+
try {
|
|
473
|
+
return await loader(walletName);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
return { name: walletName, address: '', chains: [], totalUSD: 0, ethPrice: 0, error: err.message };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function loadPriceData(tokens, deps) {
|
|
480
|
+
const loader = deps.getPriceSnapshots || getPriceSnapshots;
|
|
481
|
+
const snapshots = await loader(tokens);
|
|
482
|
+
return snapshots.filter((item) => item && !item.error);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function loadHistoryData(walletName, chain, deps) {
|
|
486
|
+
const loader = deps.fetchHistorySnapshot || fetchHistorySnapshot;
|
|
487
|
+
try {
|
|
488
|
+
return await loader(walletName, { chain, limit: 10 });
|
|
489
|
+
} catch (err) {
|
|
490
|
+
return { address: '', chain, transactions: [], error: err.message };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function loadGasData(deps) {
|
|
495
|
+
const loader = deps.fetchGasSnapshot || fetchGasSnapshot;
|
|
496
|
+
const items = await Promise.all(CHAIN_KEYS.map(async (chain) => {
|
|
497
|
+
try {
|
|
498
|
+
return await loader(chain);
|
|
499
|
+
} catch {
|
|
500
|
+
return { chain, gasPrice: 0, blockNumber: null, error: true };
|
|
501
|
+
}
|
|
502
|
+
}));
|
|
503
|
+
return items;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function updatePortfolioPanel(widgets, portfolio) {
|
|
507
|
+
widgets.portfolio?.setContent(formatPortfolioSummary(portfolio).join('\n'));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function updatePricesPanel(widgets, prices, priceHistory) {
|
|
511
|
+
widgets.priceTable?.setData({
|
|
512
|
+
headers: formatPriceRows(prices)[0],
|
|
513
|
+
data: formatPriceRows(prices).slice(1),
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
prices.forEach((item) => {
|
|
517
|
+
const key = item.symbol || item.query;
|
|
518
|
+
priceHistory[key] = [...(priceHistory[key] || []), item.price].slice(-12);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const titles = prices.map((item) => item.symbol || item.query);
|
|
522
|
+
const values = titles.map((title) => priceHistory[title] || []);
|
|
523
|
+
widgets.sparkline?.setData(titles, values);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function updateGasPanel(widgets, gasSnapshots) {
|
|
527
|
+
const titles = gasSnapshots.map((item) => item.chain.slice(0, 4).toUpperCase());
|
|
528
|
+
const data = gasSnapshots.map((item) => Math.max(1, Math.round(item.gasPrice || 0)));
|
|
529
|
+
widgets.gasBar?.setData({ titles, data });
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function updateTransactionsPanel(widgets, history) {
|
|
533
|
+
const rows = formatTransactionRows(history);
|
|
534
|
+
widgets.transactions?.setData({ headers: rows[0], data: rows.slice(1) });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function updateWhalePanel(widgets, state) {
|
|
538
|
+
if (!widgets.whales) return;
|
|
539
|
+
if (!state.whaleFeedEnabled) {
|
|
540
|
+
widgets.whales.setContent('Whale feed paused. Press w to resume.');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (!state.whaleAlerts.length) {
|
|
544
|
+
widgets.whales.setContent('No whale monitor connected.\nPass a whale EventEmitter to stream alerts.');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
widgets.whales.setContent(state.whaleAlerts.join('\n'));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function updateStatusPanel(widgets, state) {
|
|
551
|
+
widgets.status?.setContent(formatStatusBar(state));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function renderError(widgets, message) {
|
|
555
|
+
const content = `Error\n\n${message}`;
|
|
556
|
+
if (widgets.portfolio) {
|
|
557
|
+
widgets.portfolio.setContent(content);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function bindKeys(screen, handlers) {
|
|
562
|
+
screen.key(['q', 'C-c'], handlers.quit);
|
|
563
|
+
screen.key(['r'], handlers.refresh);
|
|
564
|
+
screen.key(['tab'], handlers.cycleFocus);
|
|
565
|
+
screen.key(['w'], handlers.toggleWhaleFeed);
|
|
566
|
+
['1', '2', '3', '4', '5'].forEach((key, index) => {
|
|
567
|
+
screen.key([key], () => handlers.switchChain(index));
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function formatWhaleAlert(alert, timestampValue) {
|
|
572
|
+
if (typeof alert === 'string') {
|
|
573
|
+
return `${toClock(timestampValue)} ${alert}`;
|
|
574
|
+
}
|
|
575
|
+
const chain = alert.chain || 'chain';
|
|
576
|
+
const token = alert.token || alert.symbol || 'token';
|
|
577
|
+
const amount = alert.amount ? `${alert.amount}` : 'size?';
|
|
578
|
+
const side = alert.side || 'move';
|
|
579
|
+
return `${toClock(timestampValue)} ${chain} ${side} ${amount} ${token}`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function formatDynamicPrice(price) {
|
|
583
|
+
if (price >= 1) return `$${price.toFixed(2)}`;
|
|
584
|
+
if (price >= 0.01) return `$${price.toFixed(4)}`;
|
|
585
|
+
return `$${price.toFixed(8)}`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function shorten(value, size = 8) {
|
|
589
|
+
if (!value) return '(unknown)';
|
|
590
|
+
if (value.length <= size + 4) return value;
|
|
591
|
+
return `${value.slice(0, size)}...${value.slice(-4)}`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function toClock(time) {
|
|
595
|
+
return new Date(time).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
596
|
+
}
|