@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
package/src/services/watch.js
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import fetch from 'node-fetch';
|
|
2
2
|
import { theme } from '../ui/theme.js';
|
|
3
|
-
import {
|
|
4
|
-
import { showSection
|
|
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;
|
|
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;
|
|
10
|
+
const duration = opts.duration ? parseInt(opts.duration, 10) * 60 * 1000 : null;
|
|
18
11
|
|
|
19
12
|
console.log('');
|
|
20
|
-
showSection(`PRICE WATCH
|
|
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
|
|
37
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
48
|
+
console.log(` ${theme.dim(timestamp())} ${arrow}${theme.gold(formatWatchPrice(snapshot.price).padEnd(14))} ${changeStr.padEnd(12)} vol: ${theme.dim(volStr)}`);
|
|
62
49
|
|
|
63
|
-
|
|
64
|
-
if (above && price >= above) {
|
|
50
|
+
if (above && snapshot.price >= above) {
|
|
65
51
|
console.log('');
|
|
66
|
-
console.log(theme.success(`
|
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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 };
|