@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,24 +1,16 @@
1
1
  import fetch from 'node-fetch';
2
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';
3
+ import { error, info } from '../ui/components.js';
4
+ import { showSection } from '../ui/banner.js';
5
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
6
  export async function watchPrice(token, opts = {}) {
14
- const interval = parseInt(opts.interval || '10') * 1000; // seconds → ms
7
+ const interval = parseInt(opts.interval || '10', 10) * 1000;
15
8
  const above = opts.above ? parseFloat(opts.above) : null;
16
9
  const below = opts.below ? parseFloat(opts.below) : null;
17
- const duration = opts.duration ? parseInt(opts.duration) * 60 * 1000 : null; // minutes → ms
10
+ const duration = opts.duration ? parseInt(opts.duration, 10) * 60 * 1000 : null;
18
11
 
19
12
  console.log('');
20
- showSection(`PRICE WATCH ${token.toUpperCase()}`);
21
-
13
+ showSection(`PRICE WATCH - ${token.toUpperCase()}`);
22
14
  if (above) info(`Alert above: $${above}`);
23
15
  if (below) info(`Alert below: $${below}`);
24
16
  info(`Polling every ${interval / 1000}s`);
@@ -33,57 +25,49 @@ export async function watchPrice(token, opts = {}) {
33
25
 
34
26
  const poll = async () => {
35
27
  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) {
28
+ const snapshot = await fetchTokenPrice(token);
29
+ if (!snapshot) {
41
30
  console.log(theme.dim(` [${timestamp()}] No data for ${token}`));
42
31
  return;
43
32
  }
44
33
 
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
34
  let arrow = ' ';
51
35
  if (lastPrice !== null) {
52
- if (price > lastPrice) arrow = theme.success(' ');
53
- else if (price < lastPrice) arrow = theme.accent(' ');
54
- else arrow = theme.dim('= ');
36
+ if (snapshot.price > lastPrice) arrow = theme.success('UP ');
37
+ else if (snapshot.price < lastPrice) arrow = theme.accent('DN ');
38
+ else arrow = theme.dim('= ');
55
39
  }
56
40
 
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`;
41
+ const changeStr = snapshot.change24h >= 0
42
+ ? theme.success(`+${snapshot.change24h.toFixed(2)}%`)
43
+ : theme.accent(`${snapshot.change24h.toFixed(2)}%`);
44
+ const volStr = snapshot.volume24h > 1000000
45
+ ? `$${(snapshot.volume24h / 1000000).toFixed(1)}M`
46
+ : `$${(snapshot.volume24h / 1000).toFixed(0)}K`;
60
47
 
61
- console.log(` ${theme.dim(timestamp())} ${arrow}${theme.gold(priceStr.padEnd(14))} ${changeStr.padEnd(20)} vol: ${theme.dim(volStr)}`);
48
+ console.log(` ${theme.dim(timestamp())} ${arrow}${theme.gold(formatWatchPrice(snapshot.price).padEnd(14))} ${changeStr.padEnd(12)} vol: ${theme.dim(volStr)}`);
62
49
 
63
- // Alert checks
64
- if (above && price >= above) {
50
+ if (above && snapshot.price >= above) {
65
51
  console.log('');
66
- console.log(theme.success(` 🔔 ALERT: ${pair.baseToken.symbol} hit $${price} (above $${above})`));
52
+ console.log(theme.success(` ALERT: ${snapshot.symbol} hit $${snapshot.price} (above $${above})`));
67
53
  console.log('');
68
54
  }
69
55
 
70
- if (below && price <= below) {
56
+ if (below && snapshot.price <= below) {
71
57
  console.log('');
72
- console.log(theme.accent(` 🔔 ALERT: ${pair.baseToken.symbol} dropped to $${price} (below $${below})`));
58
+ console.log(theme.accent(` ALERT: ${snapshot.symbol} dropped to $${snapshot.price} (below $${below})`));
73
59
  console.log('');
74
60
  }
75
61
 
76
- lastPrice = price;
77
- ticks++;
62
+ lastPrice = snapshot.price;
63
+ ticks += 1;
78
64
  } catch (err) {
79
65
  console.log(theme.dim(` [${timestamp()}] Error: ${err.message}`));
80
66
  }
81
67
  };
82
68
 
83
- // Initial fetch
84
69
  await poll();
85
70
 
86
- // Polling loop
87
71
  const timer = setInterval(async () => {
88
72
  if (duration && (Date.now() - startTime) >= duration) {
89
73
  clearInterval(timer);
@@ -91,10 +75,10 @@ export async function watchPrice(token, opts = {}) {
91
75
  info(`Watch ended after ${duration / 60000} minutes (${ticks} ticks)`);
92
76
  return;
93
77
  }
78
+
94
79
  await poll();
95
80
  }, interval);
96
81
 
97
- // Keep alive
98
82
  await new Promise((resolve) => {
99
83
  process.on('SIGINT', () => {
100
84
  clearInterval(timer);
@@ -102,6 +86,7 @@ export async function watchPrice(token, opts = {}) {
102
86
  info(`Watched ${ticks} ticks`);
103
87
  resolve();
104
88
  });
89
+
105
90
  if (duration) {
106
91
  setTimeout(() => {
107
92
  clearInterval(timer);
@@ -111,9 +96,34 @@ export async function watchPrice(token, opts = {}) {
111
96
  });
112
97
  }
113
98
 
114
- /**
115
- * Quick price check (one-shot, multiple tokens)
116
- */
99
+ export async function fetchTokenPrice(token) {
100
+ const resp = await fetch(`https://api.dexscreener.com/latest/dex/search?q=${token}`);
101
+ const data = await resp.json();
102
+ const pair = data.pairs?.[0];
103
+ if (!pair) return null;
104
+
105
+ return {
106
+ query: token,
107
+ symbol: pair.baseToken.symbol,
108
+ price: parseFloat(pair.priceUsd),
109
+ change24h: pair.priceChange?.h24 || 0,
110
+ volume24h: pair.volume?.h24 || 0,
111
+ pair,
112
+ };
113
+ }
114
+
115
+ export async function getPriceSnapshots(tokens = []) {
116
+ const snapshots = await Promise.all(tokens.map(async (token) => {
117
+ try {
118
+ return await fetchTokenPrice(token);
119
+ } catch {
120
+ return { query: token, error: true };
121
+ }
122
+ }));
123
+
124
+ return snapshots.filter(Boolean);
125
+ }
126
+
117
127
  export async function checkPrices(tokens) {
118
128
  if (!tokens || tokens.length === 0) {
119
129
  error('Specify tokens: darksol price ETH AERO VIRTUAL');
@@ -123,31 +133,27 @@ export async function checkPrices(tokens) {
123
133
  console.log('');
124
134
  showSection('PRICE CHECK');
125
135
 
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)}%`);
136
+ const snapshots = await getPriceSnapshots(tokens);
137
+ for (const snapshot of snapshots) {
138
+ if (snapshot.error) {
139
+ console.log(` ${theme.dim(String(snapshot.query).padEnd(10))} ${theme.dim('Error')}`);
140
+ continue;
141
+ }
140
142
 
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')}`);
143
+ if (!snapshot?.symbol) {
144
+ console.log(` ${theme.dim(String(snapshot?.query || '').toUpperCase().padEnd(10))} ${theme.dim('Not found')}`);
145
+ continue;
144
146
  }
147
+
148
+ const changeStr = snapshot.change24h >= 0
149
+ ? theme.success(`+${snapshot.change24h.toFixed(2)}%`)
150
+ : theme.accent(`${snapshot.change24h.toFixed(2)}%`);
151
+ console.log(` ${theme.gold(snapshot.symbol.padEnd(10))} ${formatWatchPrice(snapshot.price).padEnd(14)} ${changeStr}`);
145
152
  }
146
153
 
147
154
  console.log('');
148
155
  }
149
156
 
150
- // Helpers
151
157
  function timestamp() {
152
158
  return new Date().toLocaleTimeString('en-US', { hour12: false });
153
159
  }
@@ -0,0 +1,388 @@
1
+ import blessed from 'blessed';
2
+ import contrib from 'blessed-contrib';
3
+ import { ethers } from 'ethers';
4
+ import { getRPC } from '../config/store.js';
5
+ import { hasService, registerService } from '../daemon/manager.js';
6
+ import {
7
+ DEFAULT_POLL_INTERVAL_MS,
8
+ fetchExplorerTransactions,
9
+ getTokenMeta,
10
+ getTrackedWallet,
11
+ getWhaleFeed,
12
+ listTracked,
13
+ registerWhaleEvent,
14
+ runMirrorTrade,
15
+ updateTrackedWallet,
16
+ whaleEvents,
17
+ __setWhaleDeps,
18
+ } from './whale.js';
19
+
20
+ const ERC20_TRANSFER_IFACE = new ethers.Interface([
21
+ 'event Transfer(address indexed from, address indexed to, uint256 value)',
22
+ ]);
23
+
24
+ const V2_IFACE = new ethers.Interface([
25
+ 'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
26
+ 'function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline)',
27
+ 'function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
28
+ 'function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
29
+ ]);
30
+
31
+ const V3_IFACE = new ethers.Interface([
32
+ 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96))',
33
+ 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96))',
34
+ 'function exactInput((bytes path, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum))',
35
+ 'function exactInput((bytes path, address recipient, uint256 amountIn, uint256 amountOutMinimum))',
36
+ 'function multicall(uint256 deadline, bytes[] data)',
37
+ 'function multicall(bytes[] data)',
38
+ ]);
39
+
40
+ const monitorState = {
41
+ timer: null,
42
+ running: false,
43
+ lastPollAt: null,
44
+ processed: new Set(),
45
+ };
46
+
47
+ const monitorDeps = {
48
+ providerFactory: (chain) => new ethers.JsonRpcProvider(getRPC(chain)),
49
+ processTransaction: null,
50
+ };
51
+
52
+ function getProviderFactory() {
53
+ return monitorDeps.providerFactory;
54
+ }
55
+
56
+ export function parseV3Path(path) {
57
+ const hex = path.startsWith('0x') ? path.slice(2) : path;
58
+ const tokens = [];
59
+ let index = 0;
60
+
61
+ while (index + 40 <= hex.length) {
62
+ tokens.push(ethers.getAddress('0x' + hex.slice(index, index + 40)));
63
+ index += 40;
64
+ if (index + 6 <= hex.length) index += 6;
65
+ }
66
+
67
+ return tokens;
68
+ }
69
+
70
+ export function decodeSwapInput(data) {
71
+ if (!data || data === '0x') return null;
72
+
73
+ try {
74
+ const parsed = V2_IFACE.parseTransaction({ data });
75
+ const { name, args } = parsed;
76
+ return {
77
+ protocol: 'uniswap-v2',
78
+ method: name,
79
+ tokenIn: name === 'swapExactETHForTokens' ? ethers.ZeroAddress : args.path[0],
80
+ tokenOut: args.path[args.path.length - 1],
81
+ amountIn: name === 'swapExactETHForTokens' ? null : args.amountIn,
82
+ amountOutMinimum: args.amountOutMin,
83
+ path: args.path,
84
+ };
85
+ } catch {}
86
+
87
+ try {
88
+ const parsed = V3_IFACE.parseTransaction({ data });
89
+ const { name, args } = parsed;
90
+
91
+ if (name === 'multicall') {
92
+ const innerCalls = Array.isArray(args.data) ? args.data : args[1] || args[0] || [];
93
+ for (const inner of innerCalls) {
94
+ const decoded = decodeSwapInput(inner);
95
+ if (decoded) return decoded;
96
+ }
97
+ return null;
98
+ }
99
+
100
+ if (name === 'exactInputSingle') {
101
+ const params = args.params || args[0];
102
+ return {
103
+ protocol: 'uniswap-v3',
104
+ method: name,
105
+ tokenIn: params.tokenIn,
106
+ tokenOut: params.tokenOut,
107
+ amountIn: params.amountIn,
108
+ amountOutMinimum: params.amountOutMinimum,
109
+ };
110
+ }
111
+
112
+ if (name === 'exactInput') {
113
+ const params = args.params || args[0];
114
+ const path = parseV3Path(params.path);
115
+ return {
116
+ protocol: 'uniswap-v3',
117
+ method: name,
118
+ tokenIn: path[0],
119
+ tokenOut: path[path.length - 1],
120
+ amountIn: params.amountIn,
121
+ amountOutMinimum: params.amountOutMinimum,
122
+ path,
123
+ };
124
+ }
125
+ } catch {}
126
+
127
+ return null;
128
+ }
129
+
130
+ async function enrichSwap(chain, decoded, tx, provider) {
131
+ const tokenInMeta = await getTokenMeta(decoded.tokenIn, chain, provider);
132
+ const tokenOutMeta = await getTokenMeta(decoded.tokenOut, chain, provider);
133
+ const nativeAmount = tx.value && tx.value > 0n ? tx.value : null;
134
+
135
+ return {
136
+ ...decoded,
137
+ txHash: tx.hash,
138
+ amountIn: decoded.amountIn || nativeAmount || 0n,
139
+ tokenInMeta,
140
+ tokenOutMeta,
141
+ };
142
+ }
143
+
144
+ async function processTransfers(wallet, receipt) {
145
+ const walletLower = wallet.address.toLowerCase();
146
+ const tracked = getTrackedWallet(wallet.address);
147
+
148
+ for (const log of receipt.logs || []) {
149
+ let parsed;
150
+ try {
151
+ parsed = ERC20_TRANSFER_IFACE.parseLog(log);
152
+ } catch {
153
+ continue;
154
+ }
155
+
156
+ const from = parsed.args.from.toLowerCase();
157
+ const to = parsed.args.to.toLowerCase();
158
+ if (from !== walletLower && to !== walletLower) continue;
159
+
160
+ const tokenMeta = await getTokenMeta(log.address, wallet.chain);
161
+ const amount = ethers.formatUnits(parsed.args.value, tokenMeta.decimals);
162
+
163
+ registerWhaleEvent('whale:transfer', {
164
+ address: wallet.address,
165
+ label: wallet.label,
166
+ chain: wallet.chain,
167
+ txHash: receipt.hash,
168
+ token: tokenMeta.symbol,
169
+ tokenAddress: tokenMeta.address,
170
+ direction: to === walletLower ? 'in' : 'out',
171
+ amount,
172
+ });
173
+
174
+ if (to === walletLower && tracked && !tracked.knownTokens.includes(tokenMeta.address.toLowerCase())) {
175
+ updateTrackedWallet(wallet.address, (current) => ({
176
+ ...current,
177
+ knownTokens: [...current.knownTokens, tokenMeta.address.toLowerCase()],
178
+ updatedAt: new Date().toISOString(),
179
+ }));
180
+ registerWhaleEvent('whale:newtoken', {
181
+ address: wallet.address,
182
+ label: wallet.label,
183
+ chain: wallet.chain,
184
+ txHash: receipt.hash,
185
+ token: tokenMeta.symbol,
186
+ tokenAddress: tokenMeta.address,
187
+ });
188
+ }
189
+ }
190
+ }
191
+
192
+ export async function processWhaleTransaction(wallet, txSummary, providerOverride) {
193
+ const provider = providerOverride || getProviderFactory()(wallet.chain);
194
+ const tx = await provider.getTransaction(txSummary.hash);
195
+ const receipt = await provider.getTransactionReceipt(txSummary.hash);
196
+ if (!tx || !receipt) return null;
197
+
198
+ const decodedSwap = decodeSwapInput(tx.data);
199
+ if (decodedSwap) {
200
+ const swap = await enrichSwap(wallet.chain, decodedSwap, tx, provider);
201
+ registerWhaleEvent('whale:swap', {
202
+ address: wallet.address,
203
+ label: wallet.label,
204
+ chain: wallet.chain,
205
+ txHash: tx.hash,
206
+ tokenIn: swap.tokenInMeta.symbol,
207
+ tokenOut: swap.tokenOutMeta.symbol,
208
+ amountIn: ethers.formatUnits(swap.amountIn, swap.tokenInMeta.decimals),
209
+ protocol: swap.protocol,
210
+ });
211
+ await runMirrorTrade(wallet, swap);
212
+ }
213
+
214
+ await processTransfers(wallet, receipt);
215
+
216
+ updateTrackedWallet(wallet.address, (current) => ({
217
+ ...current,
218
+ lastSeenHash: txSummary.hash,
219
+ lastActivity: new Date(parseInt(txSummary.timeStamp || '0', 10) * 1000).toISOString(),
220
+ lastCheckedAt: new Date().toISOString(),
221
+ updatedAt: new Date().toISOString(),
222
+ }));
223
+
224
+ return { tx, receipt, decodedSwap };
225
+ }
226
+
227
+ monitorDeps.processTransaction = processWhaleTransaction;
228
+
229
+ export async function pollTrackedWallets(fetchActivity = fetchExplorerTransactions) {
230
+ const wallets = listTracked({ silent: true });
231
+
232
+ for (const wallet of wallets) {
233
+ const txs = await fetchActivity(wallet.address, { chain: wallet.chain, limit: 5 });
234
+ if (!txs.length) continue;
235
+
236
+ const unseen = [];
237
+ for (const tx of txs) {
238
+ if (wallet.lastSeenHash && tx.hash === wallet.lastSeenHash) break;
239
+ if (!monitorState.processed.has(tx.hash)) unseen.push(tx);
240
+ }
241
+
242
+ unseen.reverse();
243
+ for (const tx of unseen) {
244
+ await monitorDeps.processTransaction(wallet, tx);
245
+ monitorState.processed.add(tx.hash);
246
+ if (monitorState.processed.size > 1000) {
247
+ monitorState.processed = new Set(Array.from(monitorState.processed).slice(-500));
248
+ }
249
+ }
250
+
251
+ updateTrackedWallet(wallet.address, (current) => ({
252
+ ...current,
253
+ lastCheckedAt: new Date().toISOString(),
254
+ }));
255
+ }
256
+
257
+ monitorState.lastPollAt = new Date().toISOString();
258
+ }
259
+
260
+ export async function startWhaleMonitor(options = {}) {
261
+ if (monitorState.running) return;
262
+ const intervalMs = Number(options.intervalMs || DEFAULT_POLL_INTERVAL_MS);
263
+ monitorState.running = true;
264
+ await pollTrackedWallets();
265
+ monitorState.timer = setInterval(() => {
266
+ pollTrackedWallets().catch(() => {});
267
+ }, intervalMs);
268
+ }
269
+
270
+ export async function stopWhaleMonitor() {
271
+ if (monitorState.timer) {
272
+ clearInterval(monitorState.timer);
273
+ monitorState.timer = null;
274
+ }
275
+ monitorState.running = false;
276
+ }
277
+
278
+ export function getWhaleMonitorStatus() {
279
+ return {
280
+ running: monitorState.running,
281
+ lastPollAt: monitorState.lastPollAt,
282
+ tracked: listTracked({ silent: true }).length,
283
+ };
284
+ }
285
+
286
+ function formatFeedEvent(event) {
287
+ const stamp = new Date(event.createdAt || Date.now()).toLocaleTimeString('en-US', {
288
+ hour12: false,
289
+ hour: '2-digit',
290
+ minute: '2-digit',
291
+ second: '2-digit',
292
+ });
293
+
294
+ if (event.type === 'whale:swap') {
295
+ return `[${stamp}] SWAP ${event.label || event.address} ${event.amountIn} ${event.tokenIn} -> ${event.tokenOut} (${event.chain})`;
296
+ }
297
+ if (event.type === 'whale:mirror-executed') {
298
+ return `[${stamp}] MIRROR ${event.dryRun ? 'DRY' : 'LIVE'} ${event.amount} ${event.tokenIn} -> ${event.tokenOut}`;
299
+ }
300
+ if (event.type === 'whale:newtoken') {
301
+ return `[${stamp}] NEW TOKEN ${event.label || event.address} received ${event.token}`;
302
+ }
303
+ return `[${stamp}] TRANSFER ${event.label || event.address} ${event.direction} ${event.amount} ${event.token}`;
304
+ }
305
+
306
+ export async function startWhaleFeed() {
307
+ const screen = blessed.screen({ smartCSR: true, title: 'DARKSOL Whale Radar' });
308
+ const grid = new contrib.grid({ rows: 12, cols: 12, screen });
309
+ const status = grid.set(0, 0, 3, 12, blessed.box, {
310
+ label: ' Whale Radar ',
311
+ tags: true,
312
+ border: 'line',
313
+ style: { border: { fg: 'yellow' } },
314
+ });
315
+ const log = grid.set(3, 0, 9, 12, contrib.log, {
316
+ fg: 'white',
317
+ selectedFg: 'green',
318
+ label: ' Live Feed ',
319
+ });
320
+
321
+ const renderStatus = () => {
322
+ const monitor = getWhaleMonitorStatus();
323
+ status.setContent(
324
+ `Tracked: ${monitor.tracked}\n` +
325
+ `Monitor: ${monitor.running ? '{green-fg}running{/green-fg}' : '{red-fg}stopped{/red-fg}'}\n` +
326
+ `Last Poll: ${monitor.lastPollAt || 'never'}`,
327
+ );
328
+ screen.render();
329
+ };
330
+
331
+ getWhaleFeed(20).forEach((event) => log.log(formatFeedEvent(event)));
332
+ renderStatus();
333
+
334
+ const onEvent = (event) => {
335
+ log.log(formatFeedEvent(event));
336
+ renderStatus();
337
+ };
338
+
339
+ const handlers = ['whale:swap', 'whale:transfer', 'whale:newtoken', 'whale:mirror-executed'];
340
+ handlers.forEach((name) => whaleEvents.on(name, onEvent));
341
+
342
+ screen.key(['escape', 'q', 'C-c'], async () => {
343
+ handlers.forEach((name) => whaleEvents.off(name, onEvent));
344
+ screen.destroy();
345
+ await stopWhaleMonitor();
346
+ process.exit(0);
347
+ });
348
+
349
+ await startWhaleMonitor();
350
+ screen.render();
351
+ }
352
+
353
+ export const whaleMonitorServiceHandler = {
354
+ start: startWhaleMonitor,
355
+ stop: stopWhaleMonitor,
356
+ status: getWhaleMonitorStatus,
357
+ };
358
+
359
+ export function registerWhaleMonitorService() {
360
+ if (!hasService('whale-monitor')) {
361
+ registerService('whale-monitor', whaleMonitorServiceHandler);
362
+ }
363
+ }
364
+
365
+ export function __setWhaleMonitorDeps(overrides = {}) {
366
+ if (overrides.providerFactory) {
367
+ monitorDeps.providerFactory = overrides.providerFactory;
368
+ __setWhaleDeps({ providerFactory: overrides.providerFactory });
369
+ }
370
+ if (overrides.processTransaction) {
371
+ monitorDeps.processTransaction = overrides.processTransaction;
372
+ }
373
+ }
374
+
375
+ export function __resetWhaleMonitor() {
376
+ if (monitorState.timer) clearInterval(monitorState.timer);
377
+ monitorState.timer = null;
378
+ monitorState.running = false;
379
+ monitorState.lastPollAt = null;
380
+ monitorState.processed = new Set();
381
+ monitorDeps.providerFactory = (chain) => new ethers.JsonRpcProvider(getRPC(chain));
382
+ monitorDeps.processTransaction = processWhaleTransaction;
383
+ __setWhaleDeps({ providerFactory: monitorDeps.providerFactory });
384
+ }
385
+
386
+ registerWhaleMonitorService();
387
+
388
+ export { ERC20_TRANSFER_IFACE, formatFeedEvent };