@solard/cli 0.2.21 → 0.2.23
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 +5 -3
- package/src/index.ts +11 -10
- package/src/price-feed-server.architecture.test.ts +21 -0
- package/src/price-feed-server.ts +288 -1446
package/src/price-feed-server.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { configure, createMeasure } from "measure-fn";
|
|
2
|
-
import
|
|
3
|
-
import bs58 from "bs58";
|
|
4
|
-
import { bondingCurvePda } from "@solard/core";
|
|
2
|
+
import { Connection } from "@solana/web3.js";
|
|
5
3
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
|
|
4
|
+
subscribeLaunches,
|
|
5
|
+
subscribeTrades,
|
|
6
|
+
type LaunchEvent,
|
|
7
|
+
type TradeEvent,
|
|
8
|
+
type TradeSubscription,
|
|
9
|
+
} from "@solard/sdk";
|
|
10
10
|
import type {
|
|
11
11
|
PriceFeedCommand,
|
|
12
12
|
PriceFeedLaunch,
|
|
13
13
|
PriceFeedMessage,
|
|
14
14
|
PriceFeedPrice,
|
|
15
|
-
PriceFeedVenue,
|
|
16
15
|
} from "./price-feed-protocol.ts";
|
|
17
16
|
|
|
18
17
|
type Flags = Map<string, string>;
|
|
@@ -23,52 +22,12 @@ type Client = {
|
|
|
23
22
|
allPrices: boolean;
|
|
24
23
|
};
|
|
25
24
|
type MintState = {
|
|
26
|
-
|
|
27
|
-
venue: "pump" | "raydium-launchlab";
|
|
28
|
-
decimals: number;
|
|
29
|
-
supplyUi: number;
|
|
30
|
-
quoteMint: string | null;
|
|
31
|
-
quoteDecimals: number;
|
|
32
|
-
pool: string | null;
|
|
33
|
-
name: string | null;
|
|
34
|
-
symbol: string | null;
|
|
25
|
+
supplyUi: number | null;
|
|
35
26
|
isMayhemMode: boolean | null;
|
|
36
|
-
lastSeenAtMs: number;
|
|
37
|
-
};
|
|
38
|
-
type PoolState = {
|
|
39
|
-
pool: string;
|
|
40
|
-
mint: string;
|
|
41
|
-
baseDecimals: number;
|
|
42
|
-
quoteMint: string;
|
|
43
|
-
quoteDecimals: number;
|
|
44
27
|
};
|
|
45
28
|
|
|
46
|
-
const PUMP_PROGRAM = new PublicKey(
|
|
47
|
-
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
|
|
48
|
-
);
|
|
49
|
-
const PUMP_AMM_PROGRAM = new PublicKey(
|
|
50
|
-
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
|
|
51
|
-
);
|
|
52
|
-
const LAUNCHLAB_PROGRAM = new PublicKey(
|
|
53
|
-
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj",
|
|
54
|
-
);
|
|
55
29
|
const WSOL_MINT = "So11111111111111111111111111111111111111112";
|
|
56
30
|
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
57
|
-
const DEFAULT_PUBKEY = "11111111111111111111111111111111";
|
|
58
|
-
const LAMPORTS_PER_SOL = 1_000_000_000;
|
|
59
|
-
const PUMP_CREATE_EVENT = Buffer.from([27, 114, 169, 77, 222, 235, 99, 118]);
|
|
60
|
-
const PUMP_TRADE_EVENT = Buffer.from([189, 219, 127, 211, 78, 230, 97, 238]);
|
|
61
|
-
const PUMPSWAP_CREATE_EVENT = Buffer.from([
|
|
62
|
-
177, 49, 12, 210, 160, 118, 167, 116,
|
|
63
|
-
]);
|
|
64
|
-
const PUMPSWAP_BUY_EVENT = Buffer.from([103, 244, 82, 31, 44, 245, 119, 119]);
|
|
65
|
-
const PUMPSWAP_SELL_EVENT = Buffer.from([62, 47, 55, 10, 165, 3, 220, 42]);
|
|
66
|
-
const LAUNCHLAB_CREATE_EVENT = Buffer.from([
|
|
67
|
-
151, 215, 226, 9, 118, 161, 115, 174,
|
|
68
|
-
]);
|
|
69
|
-
const LAUNCHLAB_TRADE_EVENT = Buffer.from([
|
|
70
|
-
189, 219, 127, 211, 78, 230, 97, 238,
|
|
71
|
-
]);
|
|
72
31
|
|
|
73
32
|
function flag(flags: Flags, key: string): string | undefined {
|
|
74
33
|
const value = flags.get(key);
|
|
@@ -76,11 +35,11 @@ function flag(flags: Flags, key: string): string | undefined {
|
|
|
76
35
|
}
|
|
77
36
|
|
|
78
37
|
function numberFlag(flags: Flags, key: string, fallback: number): number {
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
const
|
|
82
|
-
if (!Number.isFinite(
|
|
83
|
-
return
|
|
38
|
+
const raw = flag(flags, key);
|
|
39
|
+
if (raw == null) return fallback;
|
|
40
|
+
const value = Number(raw);
|
|
41
|
+
if (!Number.isFinite(value)) throw new Error(`Invalid --${key}: ${raw}`);
|
|
42
|
+
return value;
|
|
84
43
|
}
|
|
85
44
|
|
|
86
45
|
function rpcUrl(flags: Flags): string {
|
|
@@ -89,33 +48,36 @@ function rpcUrl(flags: Flags): string {
|
|
|
89
48
|
process.env.RPC_ENDPOINT?.trim() ??
|
|
90
49
|
process.env.SOLANA_RPC_URL?.trim() ??
|
|
91
50
|
process.env.HELIUS_RPC_URL?.trim();
|
|
92
|
-
if (!value)
|
|
51
|
+
if (!value) {
|
|
93
52
|
throw new Error(
|
|
94
53
|
"feed serve requires RPC_ENDPOINT, SOLANA_RPC_URL, HELIUS_RPC_URL, or --rpc <url>",
|
|
95
54
|
);
|
|
55
|
+
}
|
|
96
56
|
return value;
|
|
97
57
|
}
|
|
98
58
|
|
|
99
|
-
function websocketUrl(flags: Flags
|
|
100
|
-
|
|
59
|
+
function websocketUrl(flags: Flags): string | undefined {
|
|
60
|
+
return (
|
|
101
61
|
flag(flags, "ws") ??
|
|
102
62
|
process.env.SOLANA_WS_URL?.trim() ??
|
|
103
|
-
process.env.HELIUS_WS_URL?.trim()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
63
|
+
process.env.HELIUS_WS_URL?.trim()
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function feedAddress(flags: Flags): { host: string; port: number } {
|
|
68
|
+
const host = flag(flags, "host") ?? "127.0.0.1";
|
|
69
|
+
const port = Math.trunc(numberFlag(flags, "port", 8788));
|
|
70
|
+
if (!(port > 0 && port <= 65_535)) throw new Error("--port must be 1..65535");
|
|
71
|
+
return { host, port };
|
|
111
72
|
}
|
|
112
73
|
|
|
113
74
|
function publicEndpoint(value: string): string {
|
|
114
75
|
try {
|
|
115
76
|
const url = new URL(value);
|
|
116
77
|
for (const key of [...url.searchParams.keys()]) {
|
|
117
|
-
if (/key|token|secret|auth/i.test(key))
|
|
78
|
+
if (/key|token|secret|auth/i.test(key)) {
|
|
118
79
|
url.searchParams.set(key, "<redacted>");
|
|
80
|
+
}
|
|
119
81
|
}
|
|
120
82
|
return url.toString();
|
|
121
83
|
} catch {
|
|
@@ -126,821 +88,11 @@ function publicEndpoint(value: string): string {
|
|
|
126
88
|
}
|
|
127
89
|
}
|
|
128
90
|
|
|
129
|
-
function feedUrl(flags: Flags): { host: string; port: number } {
|
|
130
|
-
const host = flag(flags, "host") ?? "127.0.0.1";
|
|
131
|
-
const port = Math.trunc(numberFlag(flags, "port", 8788));
|
|
132
|
-
if (!(port > 0 && port <= 65535)) throw new Error("--port must be 1..65535");
|
|
133
|
-
return { host, port };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function sleep(ms: number): Promise<void> {
|
|
137
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
91
|
function errorText(error: unknown): string {
|
|
141
92
|
return error instanceof Error ? error.message : String(error);
|
|
142
93
|
}
|
|
143
94
|
|
|
144
|
-
function
|
|
145
|
-
let value = errorText(error);
|
|
146
|
-
if (endpoint) value = value.split(endpoint).join(publicEndpoint(endpoint));
|
|
147
|
-
value = value.replace(
|
|
148
|
-
/([?&](?:api-?key|token|secret|auth)=)[^&\s'"]+/gi,
|
|
149
|
-
"$1<redacted>",
|
|
150
|
-
);
|
|
151
|
-
return value;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
class RpcPacer {
|
|
155
|
-
private tail: Promise<unknown> = Promise.resolve();
|
|
156
|
-
private nextAt = 0;
|
|
157
|
-
private count = 0;
|
|
158
|
-
constructor(readonly rps: number) {}
|
|
159
|
-
get queued(): number {
|
|
160
|
-
return this.count;
|
|
161
|
-
}
|
|
162
|
-
run<T>(fn: () => Promise<T>): Promise<T> {
|
|
163
|
-
this.count += 1;
|
|
164
|
-
const step = Math.ceil(1_000 / this.rps);
|
|
165
|
-
const run = async () => {
|
|
166
|
-
const wait = Math.max(0, this.nextAt - Date.now());
|
|
167
|
-
if (wait) await sleep(wait);
|
|
168
|
-
this.nextAt = Math.max(this.nextAt, Date.now()) + step;
|
|
169
|
-
try {
|
|
170
|
-
return await fn();
|
|
171
|
-
} finally {
|
|
172
|
-
this.count -= 1;
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
const result = this.tail.then(run, run);
|
|
176
|
-
this.tail = result.then(
|
|
177
|
-
() => undefined,
|
|
178
|
-
() => undefined,
|
|
179
|
-
);
|
|
180
|
-
return result;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
type LogsEvent = { signature: string; err: unknown; logs: string[] };
|
|
185
|
-
type LogsContext = { slot: number };
|
|
186
|
-
type LogsCallback = (event: LogsEvent, context: LogsContext) => void;
|
|
187
|
-
type DesiredLogSubscription = {
|
|
188
|
-
pubkey: string;
|
|
189
|
-
callbacks: Map<number, LogsCallback>;
|
|
190
|
-
remoteId: number | null;
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
class StandardLogsMux {
|
|
194
|
-
private socket: WebSocket | null = null;
|
|
195
|
-
private desired = new Map<string, DesiredLogSubscription>();
|
|
196
|
-
private handles = new Map<number, string>();
|
|
197
|
-
private remoteToPubkey = new Map<number, string>();
|
|
198
|
-
private requests = new Map<
|
|
199
|
-
number,
|
|
200
|
-
{ op: "subscribe" | "unsubscribe"; pubkey?: string }
|
|
201
|
-
>();
|
|
202
|
-
private nextHandle = 1;
|
|
203
|
-
private nextRequest = 1;
|
|
204
|
-
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
205
|
-
private reconnectDelayMs = 500;
|
|
206
|
-
private stopped = false;
|
|
207
|
-
private connected = false;
|
|
208
|
-
private connecting = false;
|
|
209
|
-
private initialResolve: (() => void) | null = null;
|
|
210
|
-
private initialReject: ((error: Error) => void) | null = null;
|
|
211
|
-
private initialTimer: ReturnType<typeof setTimeout> | null = null;
|
|
212
|
-
private _reconnects = 0;
|
|
213
|
-
private _lastMessageAtMs = 0;
|
|
214
|
-
|
|
215
|
-
constructor(
|
|
216
|
-
private readonly url: string,
|
|
217
|
-
private readonly report: <T extends Record<string, unknown>>(
|
|
218
|
-
label: string,
|
|
219
|
-
value: T,
|
|
220
|
-
) => T,
|
|
221
|
-
) {}
|
|
222
|
-
|
|
223
|
-
get state(): "connected" | "connecting" | "disconnected" {
|
|
224
|
-
return this.connected
|
|
225
|
-
? "connected"
|
|
226
|
-
: this.connecting
|
|
227
|
-
? "connecting"
|
|
228
|
-
: "disconnected";
|
|
229
|
-
}
|
|
230
|
-
get reconnects(): number {
|
|
231
|
-
return this._reconnects;
|
|
232
|
-
}
|
|
233
|
-
get lastMessageAtMs(): number {
|
|
234
|
-
return this._lastMessageAtMs;
|
|
235
|
-
}
|
|
236
|
-
get subscriptionCount(): number {
|
|
237
|
-
return this.desired.size;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
subscribe(pubkey: PublicKey | string, callback: LogsCallback): number {
|
|
241
|
-
const key = typeof pubkey === "string" ? pubkey : pubkey.toBase58();
|
|
242
|
-
const handle = this.nextHandle++;
|
|
243
|
-
let row = this.desired.get(key);
|
|
244
|
-
if (!row) {
|
|
245
|
-
row = { pubkey: key, callbacks: new Map(), remoteId: null };
|
|
246
|
-
this.desired.set(key, row);
|
|
247
|
-
}
|
|
248
|
-
row.callbacks.set(handle, callback);
|
|
249
|
-
this.handles.set(handle, key);
|
|
250
|
-
if (this.connected && row.remoteId == null) this.sendSubscribe(row);
|
|
251
|
-
return handle;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
unsubscribe(handle: number): void {
|
|
255
|
-
const key = this.handles.get(handle);
|
|
256
|
-
if (!key) return;
|
|
257
|
-
this.handles.delete(handle);
|
|
258
|
-
const row = this.desired.get(key);
|
|
259
|
-
if (!row) return;
|
|
260
|
-
row.callbacks.delete(handle);
|
|
261
|
-
if (row.callbacks.size > 0) return;
|
|
262
|
-
this.desired.delete(key);
|
|
263
|
-
if (row.remoteId != null) {
|
|
264
|
-
this.remoteToPubkey.delete(row.remoteId);
|
|
265
|
-
this.sendRequest("logsUnsubscribe", [row.remoteId], {
|
|
266
|
-
op: "unsubscribe",
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async start(timeoutMs = 15_000): Promise<void> {
|
|
272
|
-
if (this.connected) return;
|
|
273
|
-
if (this.stopped) throw new Error("WebSocket transport is stopped");
|
|
274
|
-
if (!this.initialResolve) {
|
|
275
|
-
const ready = new Promise<void>((resolve, reject) => {
|
|
276
|
-
this.initialResolve = resolve;
|
|
277
|
-
this.initialReject = reject;
|
|
278
|
-
});
|
|
279
|
-
this.initialTimer = setTimeout(() => {
|
|
280
|
-
const reject = this.initialReject;
|
|
281
|
-
this.initialResolve = null;
|
|
282
|
-
this.initialReject = null;
|
|
283
|
-
this.initialTimer = null;
|
|
284
|
-
const error = new Error(
|
|
285
|
-
`WebSocket did not connect within ${timeoutMs}ms`,
|
|
286
|
-
);
|
|
287
|
-
this.stop();
|
|
288
|
-
reject?.(error);
|
|
289
|
-
}, timeoutMs);
|
|
290
|
-
this.connect();
|
|
291
|
-
return await ready;
|
|
292
|
-
}
|
|
293
|
-
return await new Promise<void>((resolve, reject) => {
|
|
294
|
-
const started = Date.now();
|
|
295
|
-
const timer = setInterval(() => {
|
|
296
|
-
if (this.connected) {
|
|
297
|
-
clearInterval(timer);
|
|
298
|
-
resolve();
|
|
299
|
-
} else if (this.stopped) {
|
|
300
|
-
clearInterval(timer);
|
|
301
|
-
reject(new Error("WebSocket transport stopped"));
|
|
302
|
-
} else if (Date.now() - started >= timeoutMs) {
|
|
303
|
-
clearInterval(timer);
|
|
304
|
-
reject(new Error(`WebSocket did not connect within ${timeoutMs}ms`));
|
|
305
|
-
}
|
|
306
|
-
}, 50);
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
stop(): void {
|
|
311
|
-
if (this.stopped && !this.socket) return;
|
|
312
|
-
this.stopped = true;
|
|
313
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
314
|
-
this.reconnectTimer = null;
|
|
315
|
-
if (this.initialTimer) clearTimeout(this.initialTimer);
|
|
316
|
-
this.initialTimer = null;
|
|
317
|
-
const reject = this.initialReject;
|
|
318
|
-
this.initialResolve = null;
|
|
319
|
-
this.initialReject = null;
|
|
320
|
-
const socket = this.socket;
|
|
321
|
-
this.socket = null;
|
|
322
|
-
this.connected = false;
|
|
323
|
-
this.connecting = false;
|
|
324
|
-
reject?.(new Error("WebSocket transport stopped"));
|
|
325
|
-
try {
|
|
326
|
-
socket?.close(1000, "shutdown");
|
|
327
|
-
} catch {}
|
|
328
|
-
try {
|
|
329
|
-
socket?.terminate();
|
|
330
|
-
} catch {}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
private connect(): void {
|
|
334
|
-
if (this.stopped || this.connected || this.connecting) return;
|
|
335
|
-
this.connecting = true;
|
|
336
|
-
let socket: WebSocket;
|
|
337
|
-
try {
|
|
338
|
-
socket = new WebSocket(this.url, {
|
|
339
|
-
handshakeTimeout: 10_000,
|
|
340
|
-
perMessageDeflate: false,
|
|
341
|
-
});
|
|
342
|
-
} catch (error) {
|
|
343
|
-
this.connecting = false;
|
|
344
|
-
this.report("upstream websocket error", {
|
|
345
|
-
endpoint: publicEndpoint(this.url),
|
|
346
|
-
error: safeErrorText(error, this.url),
|
|
347
|
-
});
|
|
348
|
-
this.scheduleReconnect();
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
this.socket = socket;
|
|
352
|
-
|
|
353
|
-
socket.on("open", () => {
|
|
354
|
-
if (this.socket !== socket || this.stopped) return;
|
|
355
|
-
this.connecting = false;
|
|
356
|
-
this.connected = true;
|
|
357
|
-
this.reconnectDelayMs = 500;
|
|
358
|
-
this.remoteToPubkey.clear();
|
|
359
|
-
this.requests.clear();
|
|
360
|
-
for (const row of this.desired.values()) row.remoteId = null;
|
|
361
|
-
for (const row of this.desired.values()) this.sendSubscribe(row);
|
|
362
|
-
this.report("upstream websocket connected", {
|
|
363
|
-
endpoint: publicEndpoint(this.url),
|
|
364
|
-
subscriptions: this.desired.size,
|
|
365
|
-
reconnects: this._reconnects,
|
|
366
|
-
});
|
|
367
|
-
if (this.initialTimer) clearTimeout(this.initialTimer);
|
|
368
|
-
this.initialTimer = null;
|
|
369
|
-
const resolve = this.initialResolve;
|
|
370
|
-
this.initialResolve = null;
|
|
371
|
-
this.initialReject = null;
|
|
372
|
-
resolve?.();
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
socket.on("unexpected-response", (_request, response) => {
|
|
376
|
-
if (this.socket !== socket || this.stopped) return;
|
|
377
|
-
this.report("upstream websocket handshake rejected", {
|
|
378
|
-
endpoint: publicEndpoint(this.url),
|
|
379
|
-
statusCode: response.statusCode ?? null,
|
|
380
|
-
statusMessage: response.statusMessage ?? null,
|
|
381
|
-
retryAfter: response.headers["retry-after"] ?? null,
|
|
382
|
-
});
|
|
383
|
-
try {
|
|
384
|
-
response.resume();
|
|
385
|
-
} catch {}
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
socket.on("message", (raw: RawData) => {
|
|
389
|
-
if (this.socket !== socket) return;
|
|
390
|
-
this._lastMessageAtMs = Date.now();
|
|
391
|
-
let message: any;
|
|
392
|
-
try {
|
|
393
|
-
message = JSON.parse(raw.toString());
|
|
394
|
-
} catch {
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
if (message && typeof message.id === "number") {
|
|
398
|
-
const request = this.requests.get(message.id);
|
|
399
|
-
this.requests.delete(message.id);
|
|
400
|
-
if (!request) return;
|
|
401
|
-
if (message.error) {
|
|
402
|
-
this.report("upstream subscription error", {
|
|
403
|
-
endpoint: publicEndpoint(this.url),
|
|
404
|
-
pubkey: request.pubkey ?? null,
|
|
405
|
-
code: message.error.code ?? null,
|
|
406
|
-
error: String(message.error.message ?? "subscription failed"),
|
|
407
|
-
});
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
if (
|
|
411
|
-
request.op === "subscribe" &&
|
|
412
|
-
request.pubkey &&
|
|
413
|
-
typeof message.result === "number"
|
|
414
|
-
) {
|
|
415
|
-
const row = this.desired.get(request.pubkey);
|
|
416
|
-
if (!row) {
|
|
417
|
-
this.sendRequest("logsUnsubscribe", [message.result], {
|
|
418
|
-
op: "unsubscribe",
|
|
419
|
-
});
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
row.remoteId = message.result;
|
|
423
|
-
this.remoteToPubkey.set(message.result, request.pubkey);
|
|
424
|
-
}
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
if (message?.method !== "logsNotification") return;
|
|
428
|
-
const remote = Number(message?.params?.subscription);
|
|
429
|
-
const key = this.remoteToPubkey.get(remote);
|
|
430
|
-
if (!key) return;
|
|
431
|
-
const row = this.desired.get(key);
|
|
432
|
-
if (!row) return;
|
|
433
|
-
const value = message?.params?.result?.value;
|
|
434
|
-
const slot = Number(message?.params?.result?.context?.slot ?? 0);
|
|
435
|
-
if (
|
|
436
|
-
!value ||
|
|
437
|
-
typeof value.signature !== "string" ||
|
|
438
|
-
!Array.isArray(value.logs)
|
|
439
|
-
)
|
|
440
|
-
return;
|
|
441
|
-
const normalized: LogsEvent = {
|
|
442
|
-
signature: value.signature,
|
|
443
|
-
err: value.err ?? null,
|
|
444
|
-
logs: value.logs.map(String),
|
|
445
|
-
};
|
|
446
|
-
const context: LogsContext = { slot };
|
|
447
|
-
for (const callback of row.callbacks.values()) {
|
|
448
|
-
try {
|
|
449
|
-
callback(normalized, context);
|
|
450
|
-
} catch (error) {
|
|
451
|
-
this.report("upstream callback error", {
|
|
452
|
-
pubkey: key,
|
|
453
|
-
error: safeErrorText(error, this.url),
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
socket.on("error", (error) => {
|
|
460
|
-
if (this.socket !== socket || this.stopped) return;
|
|
461
|
-
this.report("upstream websocket error", {
|
|
462
|
-
endpoint: publicEndpoint(this.url),
|
|
463
|
-
error: safeErrorText(error, this.url),
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
socket.on("close", (code, reason) => {
|
|
468
|
-
if (this.socket !== socket) return;
|
|
469
|
-
this.socket = null;
|
|
470
|
-
const wasConnected = this.connected;
|
|
471
|
-
this.connected = false;
|
|
472
|
-
this.connecting = false;
|
|
473
|
-
this.remoteToPubkey.clear();
|
|
474
|
-
this.requests.clear();
|
|
475
|
-
for (const row of this.desired.values()) row.remoteId = null;
|
|
476
|
-
if (this.stopped) return;
|
|
477
|
-
if (wasConnected) this._reconnects += 1;
|
|
478
|
-
this.report("upstream websocket disconnected", {
|
|
479
|
-
endpoint: publicEndpoint(this.url),
|
|
480
|
-
code,
|
|
481
|
-
reason: reason.length ? reason.toString() : null,
|
|
482
|
-
retryInMs: this.reconnectDelayMs,
|
|
483
|
-
});
|
|
484
|
-
this.scheduleReconnect();
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
private scheduleReconnect(): void {
|
|
489
|
-
if (this.stopped || this.reconnectTimer) return;
|
|
490
|
-
const delay = this.reconnectDelayMs;
|
|
491
|
-
this.reconnectTimer = setTimeout(() => {
|
|
492
|
-
this.reconnectTimer = null;
|
|
493
|
-
this.connect();
|
|
494
|
-
}, delay);
|
|
495
|
-
this.reconnectDelayMs = Math.min(30_000, this.reconnectDelayMs * 2);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
private sendSubscribe(row: DesiredLogSubscription): void {
|
|
499
|
-
this.sendRequest(
|
|
500
|
-
"logsSubscribe",
|
|
501
|
-
[{ mentions: [row.pubkey] }, { commitment: "processed" }],
|
|
502
|
-
{ op: "subscribe", pubkey: row.pubkey },
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
private sendRequest(
|
|
507
|
-
method: string,
|
|
508
|
-
params: unknown[],
|
|
509
|
-
request: { op: "subscribe" | "unsubscribe"; pubkey?: string },
|
|
510
|
-
): void {
|
|
511
|
-
const socket = this.socket;
|
|
512
|
-
if (!this.connected || !socket || socket.readyState !== WebSocket.OPEN)
|
|
513
|
-
return;
|
|
514
|
-
const id = this.nextRequest++;
|
|
515
|
-
this.requests.set(id, request);
|
|
516
|
-
try {
|
|
517
|
-
socket.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
|
|
518
|
-
} catch (error) {
|
|
519
|
-
this.requests.delete(id);
|
|
520
|
-
this.report("upstream websocket send error", {
|
|
521
|
-
endpoint: publicEndpoint(this.url),
|
|
522
|
-
error: safeErrorText(error, this.url),
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
class Cursor {
|
|
529
|
-
private offset: number;
|
|
530
|
-
constructor(
|
|
531
|
-
private readonly data: Buffer,
|
|
532
|
-
offset = 0,
|
|
533
|
-
) {
|
|
534
|
-
this.offset = offset;
|
|
535
|
-
}
|
|
536
|
-
get remaining(): number {
|
|
537
|
-
return this.data.length - this.offset;
|
|
538
|
-
}
|
|
539
|
-
skip(n: number): void {
|
|
540
|
-
if (this.remaining < n) throw new Error("truncated event");
|
|
541
|
-
this.offset += n;
|
|
542
|
-
}
|
|
543
|
-
u8(): number {
|
|
544
|
-
if (this.remaining < 1) throw new Error("truncated u8");
|
|
545
|
-
return this.data[this.offset++]!;
|
|
546
|
-
}
|
|
547
|
-
u16(): number {
|
|
548
|
-
if (this.remaining < 2) throw new Error("truncated u16");
|
|
549
|
-
const value = this.data.readUInt16LE(this.offset);
|
|
550
|
-
this.offset += 2;
|
|
551
|
-
return value;
|
|
552
|
-
}
|
|
553
|
-
u64(): bigint {
|
|
554
|
-
if (this.remaining < 8) throw new Error("truncated u64");
|
|
555
|
-
const value = this.data.readBigUInt64LE(this.offset);
|
|
556
|
-
this.offset += 8;
|
|
557
|
-
return value;
|
|
558
|
-
}
|
|
559
|
-
i64(): bigint {
|
|
560
|
-
if (this.remaining < 8) throw new Error("truncated i64");
|
|
561
|
-
const value = this.data.readBigInt64LE(this.offset);
|
|
562
|
-
this.offset += 8;
|
|
563
|
-
return value;
|
|
564
|
-
}
|
|
565
|
-
bool(): boolean {
|
|
566
|
-
return this.u8() !== 0;
|
|
567
|
-
}
|
|
568
|
-
pubkey(): string {
|
|
569
|
-
if (this.remaining < 32) throw new Error("truncated pubkey");
|
|
570
|
-
const value = bs58.encode(
|
|
571
|
-
this.data.subarray(this.offset, this.offset + 32),
|
|
572
|
-
);
|
|
573
|
-
this.offset += 32;
|
|
574
|
-
return value;
|
|
575
|
-
}
|
|
576
|
-
string(): string {
|
|
577
|
-
if (this.remaining < 4) throw new Error("truncated string length");
|
|
578
|
-
const length = this.data.readUInt32LE(this.offset);
|
|
579
|
-
this.offset += 4;
|
|
580
|
-
if (length > 16_384 || this.remaining < length)
|
|
581
|
-
throw new Error("truncated string");
|
|
582
|
-
const value = this.data
|
|
583
|
-
.subarray(this.offset, this.offset + length)
|
|
584
|
-
.toString("utf8");
|
|
585
|
-
this.offset += length;
|
|
586
|
-
return value;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function dataEvents(logs: readonly string[]): Buffer[] {
|
|
591
|
-
const rows: Buffer[] = [];
|
|
592
|
-
for (const line of logs) {
|
|
593
|
-
const match = /^(?:Program data|Program return: \S+):\s+(.+)$/.exec(
|
|
594
|
-
line.trim(),
|
|
595
|
-
);
|
|
596
|
-
if (!match) continue;
|
|
597
|
-
try {
|
|
598
|
-
rows.push(Buffer.from(match[1]!, "base64"));
|
|
599
|
-
} catch {}
|
|
600
|
-
}
|
|
601
|
-
return rows;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function starts(data: Buffer, discriminator: Buffer): boolean {
|
|
605
|
-
return (
|
|
606
|
-
data.length >= discriminator.length &&
|
|
607
|
-
data.subarray(0, discriminator.length).equals(discriminator)
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function pumpCreate(data: Buffer): PriceFeedLaunch | null {
|
|
612
|
-
if (!starts(data, PUMP_CREATE_EVENT)) return null;
|
|
613
|
-
try {
|
|
614
|
-
const c = new Cursor(data, 8);
|
|
615
|
-
const name = c.string();
|
|
616
|
-
const symbol = c.string();
|
|
617
|
-
c.string();
|
|
618
|
-
const mint = c.pubkey();
|
|
619
|
-
c.pubkey();
|
|
620
|
-
c.pubkey();
|
|
621
|
-
c.pubkey();
|
|
622
|
-
const timestamp = Number(c.i64()) * 1_000;
|
|
623
|
-
c.u64();
|
|
624
|
-
c.u64();
|
|
625
|
-
c.u64();
|
|
626
|
-
const supplyRaw = c.u64();
|
|
627
|
-
c.pubkey();
|
|
628
|
-
const isMayhemMode = c.bool();
|
|
629
|
-
if (c.remaining > 0) c.bool();
|
|
630
|
-
const rawQuoteMint = c.remaining >= 32 ? c.pubkey() : WSOL_MINT;
|
|
631
|
-
const quoteMint =
|
|
632
|
-
rawQuoteMint === DEFAULT_PUBKEY ? WSOL_MINT : rawQuoteMint;
|
|
633
|
-
const decimals = 6;
|
|
634
|
-
return {
|
|
635
|
-
type: "launch",
|
|
636
|
-
atMs: timestamp > 0 ? timestamp : Date.now(),
|
|
637
|
-
signature: null,
|
|
638
|
-
slot: null,
|
|
639
|
-
mint,
|
|
640
|
-
venue: "pump",
|
|
641
|
-
decimals,
|
|
642
|
-
supplyUi: Number(supplyRaw) / 10 ** decimals,
|
|
643
|
-
quoteMint,
|
|
644
|
-
pool: null,
|
|
645
|
-
name: name || null,
|
|
646
|
-
symbol: symbol || null,
|
|
647
|
-
isMayhemMode,
|
|
648
|
-
};
|
|
649
|
-
} catch {
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function pumpTrade(data: Buffer): {
|
|
655
|
-
mint: string;
|
|
656
|
-
atMs: number;
|
|
657
|
-
virtualSol: bigint;
|
|
658
|
-
virtualToken: bigint;
|
|
659
|
-
} | null {
|
|
660
|
-
if (!starts(data, PUMP_TRADE_EVENT)) return null;
|
|
661
|
-
try {
|
|
662
|
-
const c = new Cursor(data, 8);
|
|
663
|
-
const mint = c.pubkey();
|
|
664
|
-
c.u64();
|
|
665
|
-
c.u64();
|
|
666
|
-
c.bool();
|
|
667
|
-
c.pubkey();
|
|
668
|
-
const atMs = Number(c.i64()) * 1_000;
|
|
669
|
-
const virtualSol = c.u64();
|
|
670
|
-
const virtualToken = c.u64();
|
|
671
|
-
if (virtualSol <= 0n || virtualToken <= 0n) return null;
|
|
672
|
-
return {
|
|
673
|
-
mint,
|
|
674
|
-
atMs: atMs > 0 ? atMs : Date.now(),
|
|
675
|
-
virtualSol,
|
|
676
|
-
virtualToken,
|
|
677
|
-
};
|
|
678
|
-
} catch {
|
|
679
|
-
return null;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function pumpSwapCreate(data: Buffer): (PoolState & { atMs: number }) | null {
|
|
684
|
-
if (!starts(data, PUMPSWAP_CREATE_EVENT)) return null;
|
|
685
|
-
try {
|
|
686
|
-
const c = new Cursor(data, 8);
|
|
687
|
-
const atMs = Number(c.i64()) * 1_000;
|
|
688
|
-
c.u16();
|
|
689
|
-
c.pubkey();
|
|
690
|
-
const mint = c.pubkey();
|
|
691
|
-
const quoteMint = c.pubkey();
|
|
692
|
-
const baseDecimals = c.u8();
|
|
693
|
-
const quoteDecimals = c.u8();
|
|
694
|
-
for (let i = 0; i < 7; i += 1) c.u64();
|
|
695
|
-
c.u8();
|
|
696
|
-
const pool = c.pubkey();
|
|
697
|
-
return { pool, mint, baseDecimals, quoteMint, quoteDecimals, atMs };
|
|
698
|
-
} catch {
|
|
699
|
-
return null;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function pumpSwapTrade(data: Buffer): {
|
|
704
|
-
pool: string;
|
|
705
|
-
atMs: number;
|
|
706
|
-
baseRaw: bigint;
|
|
707
|
-
quoteRaw: bigint;
|
|
708
|
-
} | null {
|
|
709
|
-
const buy = starts(data, PUMPSWAP_BUY_EVENT);
|
|
710
|
-
const sell = starts(data, PUMPSWAP_SELL_EVENT);
|
|
711
|
-
if (!buy && !sell) return null;
|
|
712
|
-
try {
|
|
713
|
-
const c = new Cursor(data, 8);
|
|
714
|
-
const atMs = Number(c.i64()) * 1_000;
|
|
715
|
-
c.u64();
|
|
716
|
-
c.u64();
|
|
717
|
-
c.u64();
|
|
718
|
-
c.u64();
|
|
719
|
-
const baseRaw = c.u64();
|
|
720
|
-
const quoteRaw = c.u64();
|
|
721
|
-
c.u64();
|
|
722
|
-
c.u64();
|
|
723
|
-
c.u64();
|
|
724
|
-
c.u64();
|
|
725
|
-
c.u64();
|
|
726
|
-
c.u64();
|
|
727
|
-
c.u64();
|
|
728
|
-
const pool = c.pubkey();
|
|
729
|
-
return { pool, atMs: atMs > 0 ? atMs : Date.now(), baseRaw, quoteRaw };
|
|
730
|
-
} catch {
|
|
731
|
-
return null;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function launchLabPoolCreate(data: Buffer): string | null {
|
|
736
|
-
if (!starts(data, LAUNCHLAB_CREATE_EVENT)) return null;
|
|
737
|
-
try {
|
|
738
|
-
return new Cursor(data, 8).pubkey();
|
|
739
|
-
} catch {
|
|
740
|
-
return null;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function launchLabTrade(data: Buffer): {
|
|
745
|
-
pool: string;
|
|
746
|
-
virtualBase: bigint;
|
|
747
|
-
virtualQuote: bigint;
|
|
748
|
-
} | null {
|
|
749
|
-
if (!starts(data, LAUNCHLAB_TRADE_EVENT)) return null;
|
|
750
|
-
try {
|
|
751
|
-
const c = new Cursor(data, 8);
|
|
752
|
-
const pool = c.pubkey();
|
|
753
|
-
c.u64();
|
|
754
|
-
const virtualBase = c.u64();
|
|
755
|
-
const virtualQuote = c.u64();
|
|
756
|
-
return { pool, virtualBase, virtualQuote };
|
|
757
|
-
} catch {
|
|
758
|
-
return null;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function keyText(value: unknown): string | null {
|
|
763
|
-
if (typeof value === "string") return value;
|
|
764
|
-
if (value && typeof (value as any).toBase58 === "function")
|
|
765
|
-
return (value as any).toBase58();
|
|
766
|
-
if (value && typeof (value as any).pubkey?.toBase58 === "function")
|
|
767
|
-
return (value as any).pubkey.toBase58();
|
|
768
|
-
if (value && typeof (value as any).pubkey === "string")
|
|
769
|
-
return (value as any).pubkey;
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
function instructions(tx: ParsedTransactionWithMeta): any[] {
|
|
774
|
-
const outer = ((tx.transaction.message as any).instructions ?? []) as any[];
|
|
775
|
-
const inner = (tx.meta?.innerInstructions ?? []).flatMap(
|
|
776
|
-
(group: any) => group.instructions ?? [],
|
|
777
|
-
);
|
|
778
|
-
return [...outer, ...inner];
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function initializedMints(tx: ParsedTransactionWithMeta): string[] {
|
|
782
|
-
const out = new Set<string>();
|
|
783
|
-
for (const ix of instructions(tx)) {
|
|
784
|
-
const parsed = (ix as any)?.parsed;
|
|
785
|
-
const type = String(parsed?.type ?? "").toLowerCase();
|
|
786
|
-
if (type !== "initializemint" && type !== "initializemint2") continue;
|
|
787
|
-
const mint =
|
|
788
|
-
keyText(parsed?.info?.mint) ?? String(parsed?.info?.mint ?? "");
|
|
789
|
-
if (mint && mint !== WSOL_MINT && mint !== USDC_MINT) out.add(mint);
|
|
790
|
-
}
|
|
791
|
-
return [...out];
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function tokenBalances(rows: readonly any[] | null | undefined) {
|
|
795
|
-
return (rows ?? []).map((row) => ({
|
|
796
|
-
accountIndex: Number(row.accountIndex),
|
|
797
|
-
mint: String(row.mint ?? ""),
|
|
798
|
-
owner: row.owner ? String(row.owner) : null,
|
|
799
|
-
raw: (() => {
|
|
800
|
-
try {
|
|
801
|
-
return BigInt(String(row?.uiTokenAmount?.amount ?? "0"));
|
|
802
|
-
} catch {
|
|
803
|
-
return 0n;
|
|
804
|
-
}
|
|
805
|
-
})(),
|
|
806
|
-
decimals: Number(row?.uiTokenAmount?.decimals ?? 0),
|
|
807
|
-
}));
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function supplyFromTransaction(
|
|
811
|
-
tx: ParsedTransactionWithMeta,
|
|
812
|
-
mint: string,
|
|
813
|
-
): { decimals: number; supplyUi: number } | null {
|
|
814
|
-
const rows = tokenBalances(tx.meta?.postTokenBalances).filter(
|
|
815
|
-
(row) => row.mint === mint,
|
|
816
|
-
);
|
|
817
|
-
if (!rows.length) return null;
|
|
818
|
-
const decimals = rows[0]!.decimals;
|
|
819
|
-
const byAccount = new Map<number, bigint>();
|
|
820
|
-
for (const row of rows) byAccount.set(row.accountIndex, row.raw);
|
|
821
|
-
const raw = [...byAccount.values()].reduce((sum, value) => sum + value, 0n);
|
|
822
|
-
const supplyUi = Number(raw) / 10 ** decimals;
|
|
823
|
-
return Number.isFinite(supplyUi) && supplyUi > 0
|
|
824
|
-
? { decimals, supplyUi }
|
|
825
|
-
: null;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function signerKeys(tx: ParsedTransactionWithMeta): Set<string> {
|
|
829
|
-
return new Set(
|
|
830
|
-
(((tx.transaction.message as any).accountKeys ?? []) as any[])
|
|
831
|
-
.filter((row) => row && typeof row === "object" && row.signer === true)
|
|
832
|
-
.map((row) => keyText(row))
|
|
833
|
-
.filter((row): row is string => Boolean(row)),
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function accountKeys(tx: ParsedTransactionWithMeta): string[] {
|
|
838
|
-
return (((tx.transaction.message as any).accountKeys ?? []) as any[])
|
|
839
|
-
.map((row) => keyText(row))
|
|
840
|
-
.filter((row): row is string => Boolean(row));
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
function ownerDelta(
|
|
844
|
-
tx: ParsedTransactionWithMeta,
|
|
845
|
-
mint: string,
|
|
846
|
-
owner: string,
|
|
847
|
-
): bigint {
|
|
848
|
-
const pre = tokenBalances(tx.meta?.preTokenBalances);
|
|
849
|
-
const post = tokenBalances(tx.meta?.postTokenBalances);
|
|
850
|
-
const indexes = new Set<number>();
|
|
851
|
-
for (const row of [...pre, ...post])
|
|
852
|
-
if (row.mint === mint && row.owner === owner) indexes.add(row.accountIndex);
|
|
853
|
-
let total = 0n;
|
|
854
|
-
for (const index of indexes) {
|
|
855
|
-
const before =
|
|
856
|
-
pre.find((row) => row.accountIndex === index && row.mint === mint)?.raw ??
|
|
857
|
-
0n;
|
|
858
|
-
const after =
|
|
859
|
-
post.find((row) => row.accountIndex === index && row.mint === mint)
|
|
860
|
-
?.raw ?? 0n;
|
|
861
|
-
total += after - before;
|
|
862
|
-
}
|
|
863
|
-
return total;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function tokenAccountRentDelta(
|
|
867
|
-
tx: ParsedTransactionWithMeta,
|
|
868
|
-
owner: string,
|
|
869
|
-
): bigint {
|
|
870
|
-
if (!tx.meta) return 0n;
|
|
871
|
-
const pre = tokenBalances(tx.meta.preTokenBalances);
|
|
872
|
-
const post = tokenBalances(tx.meta.postTokenBalances);
|
|
873
|
-
const indices = new Set(
|
|
874
|
-
[...pre, ...post]
|
|
875
|
-
.filter((row) => row.owner === owner)
|
|
876
|
-
.map((row) => row.accountIndex),
|
|
877
|
-
);
|
|
878
|
-
let total = 0n;
|
|
879
|
-
for (const index of indices) {
|
|
880
|
-
const before = BigInt(Math.trunc(Number(tx.meta.preBalances[index] ?? 0)));
|
|
881
|
-
const after = BigInt(Math.trunc(Number(tx.meta.postBalances[index] ?? 0)));
|
|
882
|
-
const pairPre = pre.find((row) => row.accountIndex === index);
|
|
883
|
-
const pairPost = post.find((row) => row.accountIndex === index);
|
|
884
|
-
let delta = after - before;
|
|
885
|
-
if ((pairPost?.mint ?? pairPre?.mint) === WSOL_MINT)
|
|
886
|
-
delta -= (pairPost?.raw ?? 0n) - (pairPre?.raw ?? 0n);
|
|
887
|
-
total += delta;
|
|
888
|
-
}
|
|
889
|
-
return total;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function genericPrice(
|
|
893
|
-
tx: ParsedTransactionWithMeta,
|
|
894
|
-
mint: string,
|
|
895
|
-
decimals: number,
|
|
896
|
-
): { atMs: number; priceSol: number | null; priceUsd: number | null } | null {
|
|
897
|
-
if (!tx.meta || tx.meta.err) return null;
|
|
898
|
-
const pre = tokenBalances(tx.meta.preTokenBalances);
|
|
899
|
-
const post = tokenBalances(tx.meta.postTokenBalances);
|
|
900
|
-
const signers = signerKeys(tx);
|
|
901
|
-
const owners = new Set<string>();
|
|
902
|
-
for (const row of [...pre, ...post])
|
|
903
|
-
if (row.mint === mint && row.owner && signers.has(row.owner))
|
|
904
|
-
owners.add(row.owner);
|
|
905
|
-
const keys = accountKeys(tx);
|
|
906
|
-
for (const owner of owners) {
|
|
907
|
-
const tokenDelta = ownerDelta(tx, mint, owner);
|
|
908
|
-
if (tokenDelta === 0n) continue;
|
|
909
|
-
const tokenUi = Math.abs(Number(tokenDelta)) / 10 ** decimals;
|
|
910
|
-
if (!(tokenUi > 0)) continue;
|
|
911
|
-
const ownerIndex = keys.indexOf(owner);
|
|
912
|
-
if (ownerIndex >= 0) {
|
|
913
|
-
const before = BigInt(
|
|
914
|
-
Math.trunc(Number(tx.meta.preBalances[ownerIndex] ?? 0)),
|
|
915
|
-
);
|
|
916
|
-
const after = BigInt(
|
|
917
|
-
Math.trunc(Number(tx.meta.postBalances[ownerIndex] ?? 0)),
|
|
918
|
-
);
|
|
919
|
-
const fee = keys[0] === owner ? BigInt(tx.meta.fee ?? 0) : 0n;
|
|
920
|
-
const rent = tokenAccountRentDelta(tx, owner);
|
|
921
|
-
const wsol = ownerDelta(tx, WSOL_MINT, owner);
|
|
922
|
-
const delta = after - before + fee + rent + wsol;
|
|
923
|
-
const solUi = Math.abs(Number(delta)) / LAMPORTS_PER_SOL;
|
|
924
|
-
if (solUi > 0)
|
|
925
|
-
return {
|
|
926
|
-
atMs: (tx.blockTime ?? Math.floor(Date.now() / 1000)) * 1_000,
|
|
927
|
-
priceSol: solUi / tokenUi,
|
|
928
|
-
priceUsd: null,
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
const usdc = ownerDelta(tx, USDC_MINT, owner);
|
|
932
|
-
const usdcUi = Math.abs(Number(usdc)) / 1_000_000;
|
|
933
|
-
if (usdcUi > 0)
|
|
934
|
-
return {
|
|
935
|
-
atMs: (tx.blockTime ?? Math.floor(Date.now() / 1000)) * 1_000,
|
|
936
|
-
priceSol: null,
|
|
937
|
-
priceUsd: usdcUi / tokenUi,
|
|
938
|
-
};
|
|
939
|
-
}
|
|
940
|
-
return null;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
async function fetchJson(url: string): Promise<any> {
|
|
95
|
+
async function fetchJson(url: string): Promise<unknown> {
|
|
944
96
|
const controller = new AbortController();
|
|
945
97
|
const timer = setTimeout(() => controller.abort(), 5_000);
|
|
946
98
|
try {
|
|
@@ -957,18 +109,19 @@ async function fetchJson(url: string): Promise<any> {
|
|
|
957
109
|
|
|
958
110
|
async function loadSolUsd(): Promise<number> {
|
|
959
111
|
try {
|
|
960
|
-
const raw = await fetchJson(
|
|
112
|
+
const raw = (await fetchJson(
|
|
961
113
|
"https://api.coinbase.com/v2/prices/SOL-USD/spot",
|
|
962
|
-
);
|
|
963
|
-
const value = Number(raw
|
|
114
|
+
)) as { data?: { amount?: unknown } };
|
|
115
|
+
const value = Number(raw.data?.amount);
|
|
964
116
|
if (Number.isFinite(value) && value > 0) return value;
|
|
965
117
|
} catch {}
|
|
966
|
-
const raw = await fetchJson(
|
|
118
|
+
const raw = (await fetchJson(
|
|
967
119
|
"https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
|
|
968
|
-
);
|
|
969
|
-
const value = Number(raw
|
|
970
|
-
if (!Number.isFinite(value) || value <= 0)
|
|
120
|
+
)) as { solana?: { usd?: unknown } };
|
|
121
|
+
const value = Number(raw.solana?.usd);
|
|
122
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
971
123
|
throw new Error("SOL/USD unavailable");
|
|
124
|
+
}
|
|
972
125
|
return value;
|
|
973
126
|
}
|
|
974
127
|
|
|
@@ -988,331 +141,280 @@ export async function runPriceFeedServerCommand(args: {
|
|
|
988
141
|
value: T,
|
|
989
142
|
): T =>
|
|
990
143
|
m.sync({ start: () => label, end: (result: T) => result }, () => value);
|
|
991
|
-
const { host, port } =
|
|
144
|
+
const { host, port } = feedAddress(args.flags);
|
|
992
145
|
const rpc = rpcUrl(args.flags);
|
|
993
|
-
const ws = websocketUrl(args.flags
|
|
994
|
-
const connection = new Connection(rpc, "confirmed");
|
|
995
|
-
const logsMux = new StandardLogsMux(ws, report);
|
|
996
|
-
const hardRps = Math.max(1, Math.trunc(numberFlag(args.flags, "rpc-rps", 5)));
|
|
997
|
-
const readRps = Math.max(
|
|
998
|
-
1,
|
|
999
|
-
Math.min(hardRps, Math.trunc(numberFlag(args.flags, "rpc-read-rps", 2))),
|
|
1000
|
-
);
|
|
1001
|
-
const pacer = new RpcPacer(readRps);
|
|
146
|
+
const ws = websocketUrl(args.flags);
|
|
1002
147
|
const includeMayhem = args.flags.has("include-mayhem");
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
148
|
+
const connection = new Connection(
|
|
149
|
+
rpc,
|
|
150
|
+
ws ? { commitment: "confirmed", wsEndpoint: ws } : "confirmed",
|
|
1006
151
|
);
|
|
1007
|
-
const
|
|
1008
|
-
const
|
|
1009
|
-
const
|
|
1010
|
-
const launchesByMint = new Map<string, PriceFeedLaunch>();
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const clients = new Map<unknown, Client>();
|
|
154
|
+
const launches = new Map<string, PriceFeedLaunch>();
|
|
1011
155
|
const latest = new Map<string, PriceFeedPrice>();
|
|
1012
|
-
const
|
|
1013
|
-
const
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
let launches = 0;
|
|
1018
|
-
let prices = 0;
|
|
1019
|
-
let clientConnections = 0;
|
|
1020
|
-
let coalescedFallback = 0;
|
|
1021
|
-
let rpcErrors = 0;
|
|
1022
|
-
let stopped = false;
|
|
156
|
+
const mintState = new Map<string, MintState>();
|
|
157
|
+
const discoveredMints = new Set<string>();
|
|
158
|
+
const explicitMintRefs = new Map<string, number>();
|
|
159
|
+
let tradeSubscription: TradeSubscription | null = null;
|
|
160
|
+
let allPriceClients = 0;
|
|
1023
161
|
let solUsd: number | null = null;
|
|
1024
|
-
let
|
|
1025
|
-
let
|
|
1026
|
-
let
|
|
1027
|
-
let
|
|
1028
|
-
|
|
1029
|
-
const refreshSolUsd = async () => {
|
|
1030
|
-
const now = Date.now();
|
|
1031
|
-
if (solUsd != null && now - solUsdAt < 60_000) return solUsd;
|
|
1032
|
-
if (now < solUsdRetryAt) return solUsd;
|
|
1033
|
-
if (solUsdPromise) return await solUsdPromise;
|
|
1034
|
-
solUsdPromise = (async () => {
|
|
1035
|
-
try {
|
|
1036
|
-
solUsd = await loadSolUsd();
|
|
1037
|
-
solUsdAt = Date.now();
|
|
1038
|
-
solUsdRetryAt = 0;
|
|
1039
|
-
solUsdFailures = 0;
|
|
1040
|
-
} catch (error) {
|
|
1041
|
-
solUsdFailures += 1;
|
|
1042
|
-
const retry = Math.min(
|
|
1043
|
-
300_000,
|
|
1044
|
-
15_000 * 2 ** Math.min(5, solUsdFailures - 1),
|
|
1045
|
-
);
|
|
1046
|
-
solUsdRetryAt = Date.now() + retry;
|
|
1047
|
-
report("sol/usd unavailable", {
|
|
1048
|
-
error: errorText(error),
|
|
1049
|
-
failures: solUsdFailures,
|
|
1050
|
-
retryInMs: retry,
|
|
1051
|
-
usingCached: solUsd != null,
|
|
1052
|
-
});
|
|
1053
|
-
} finally {
|
|
1054
|
-
solUsdPromise = null;
|
|
1055
|
-
}
|
|
1056
|
-
return solUsd;
|
|
1057
|
-
})();
|
|
1058
|
-
return await solUsdPromise;
|
|
1059
|
-
};
|
|
162
|
+
let launchCount = 0;
|
|
163
|
+
let tradeCount = 0;
|
|
164
|
+
let priceCount = 0;
|
|
165
|
+
let stopped = false;
|
|
1060
166
|
|
|
1061
|
-
const send = (
|
|
167
|
+
const send = (
|
|
168
|
+
socket: { send(value: string): unknown },
|
|
169
|
+
value: PriceFeedMessage,
|
|
170
|
+
): void => {
|
|
1062
171
|
try {
|
|
1063
|
-
|
|
172
|
+
socket.send(JSON.stringify(value));
|
|
1064
173
|
} catch {}
|
|
1065
174
|
};
|
|
1066
175
|
|
|
1067
|
-
const broadcast = (value: PriceFeedMessage) => {
|
|
1068
|
-
for (const [
|
|
176
|
+
const broadcast = (value: PriceFeedMessage): void => {
|
|
177
|
+
for (const [socket, client] of clients) {
|
|
178
|
+
const target = socket as { send(value: string): unknown };
|
|
1069
179
|
if (value.type === "launch") {
|
|
1070
|
-
if (client.launches) send(
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
180
|
+
if (client.launches) send(target, value);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (value.type === "price") {
|
|
184
|
+
if (client.allPrices || client.mints.has(value.mint))
|
|
185
|
+
send(target, value);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
send(target, value);
|
|
1074
189
|
}
|
|
1075
190
|
};
|
|
1076
191
|
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
192
|
+
const shouldWatchMint = (mint: string): boolean =>
|
|
193
|
+
(explicitMintRefs.get(mint) ?? 0) > 0 ||
|
|
194
|
+
(allPriceClients > 0 && discoveredMints.has(mint));
|
|
195
|
+
|
|
196
|
+
const syncMintSubscription = async (mint: string): Promise<void> => {
|
|
197
|
+
if (!tradeSubscription) return;
|
|
198
|
+
const watched = tradeSubscription.hasToken(mint);
|
|
199
|
+
const desired = shouldWatchMint(mint);
|
|
200
|
+
if (desired && !watched) await tradeSubscription.addTokens(mint);
|
|
201
|
+
if (!desired && watched) await tradeSubscription.removeTokens(mint);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const syncDiscoveredSubscriptions = async (): Promise<void> => {
|
|
205
|
+
await Promise.allSettled(
|
|
206
|
+
[...discoveredMints].map((mint) => syncMintSubscription(mint)),
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const onLaunch = async (event: LaunchEvent): Promise<void> => {
|
|
211
|
+
if (event.isMayhemMode === true && !includeMayhem) return;
|
|
212
|
+
const value: PriceFeedLaunch = {
|
|
213
|
+
type: "launch",
|
|
214
|
+
atMs: event.atMs,
|
|
215
|
+
signature: event.signature,
|
|
216
|
+
slot: event.slot,
|
|
217
|
+
mint: event.mint,
|
|
218
|
+
venue: event.venue,
|
|
219
|
+
decimals: event.decimals,
|
|
220
|
+
supplyUi: event.supplyUi,
|
|
221
|
+
quoteMint: event.quoteMint,
|
|
222
|
+
pool: event.pool,
|
|
223
|
+
name: event.name,
|
|
224
|
+
symbol: event.symbol,
|
|
225
|
+
isMayhemMode: event.isMayhemMode,
|
|
226
|
+
};
|
|
227
|
+
mintState.set(event.mint, {
|
|
228
|
+
supplyUi: event.supplyUi,
|
|
229
|
+
isMayhemMode: event.isMayhemMode,
|
|
1093
230
|
});
|
|
1094
|
-
|
|
1095
|
-
|
|
231
|
+
launches.set(event.mint, value);
|
|
232
|
+
discoveredMints.add(event.mint);
|
|
233
|
+
launchCount += 1;
|
|
1096
234
|
broadcast(value);
|
|
235
|
+
await syncMintSubscription(event.mint);
|
|
1097
236
|
};
|
|
1098
237
|
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
},
|
|
1104
|
-
) => {
|
|
1105
|
-
const state = mints.get(input.mint);
|
|
238
|
+
const onTrade = (event: TradeEvent): void => {
|
|
239
|
+
tradeCount += 1;
|
|
240
|
+
if (event.priceQuote == null) return;
|
|
241
|
+
const state = mintState.get(event.mint);
|
|
1106
242
|
if (state?.isMayhemMode === true && !includeMayhem) return;
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
243
|
+
let priceSol: number | null = null;
|
|
244
|
+
let priceUsd: number | null = null;
|
|
245
|
+
if (event.quoteMint === WSOL_MINT) {
|
|
246
|
+
priceSol = event.priceQuote;
|
|
247
|
+
priceUsd = solUsd == null ? null : event.priceQuote * solUsd;
|
|
248
|
+
} else if (event.quoteMint === USDC_MINT) {
|
|
249
|
+
priceUsd = event.priceQuote;
|
|
250
|
+
priceSol = solUsd == null ? null : event.priceQuote / solUsd;
|
|
251
|
+
}
|
|
252
|
+
const value: PriceFeedPrice = {
|
|
253
|
+
type: "price",
|
|
254
|
+
atMs: event.atMs,
|
|
255
|
+
signature: event.signature,
|
|
256
|
+
slot: event.slot,
|
|
257
|
+
mint: event.mint,
|
|
258
|
+
venue: event.venue,
|
|
259
|
+
priceSol,
|
|
260
|
+
priceUsd,
|
|
261
|
+
marketCapUsd:
|
|
262
|
+
priceUsd != null && state?.supplyUi != null
|
|
263
|
+
? priceUsd * state.supplyUi
|
|
264
|
+
: null,
|
|
265
|
+
source: `${event.venue}-trade-event`,
|
|
266
|
+
};
|
|
267
|
+
latest.set(event.mint, value);
|
|
268
|
+
priceCount += 1;
|
|
1121
269
|
broadcast(value);
|
|
1122
270
|
};
|
|
1123
271
|
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
connection.getAccountInfo(bondingCurvePda(key), "confirmed"),
|
|
1133
|
-
);
|
|
1134
|
-
const supplyUi = Number(supply.value.uiAmountString ?? "0");
|
|
1135
|
-
const state: MintState = {
|
|
1136
|
-
mint,
|
|
1137
|
-
venue: "pump",
|
|
1138
|
-
decimals: supply.value.decimals,
|
|
1139
|
-
supplyUi,
|
|
1140
|
-
quoteMint: WSOL_MINT,
|
|
1141
|
-
quoteDecimals: 9,
|
|
1142
|
-
pool: null,
|
|
1143
|
-
name: null,
|
|
1144
|
-
symbol: null,
|
|
1145
|
-
isMayhemMode: null,
|
|
1146
|
-
lastSeenAtMs: Date.now(),
|
|
1147
|
-
};
|
|
1148
|
-
mints.set(mint, state);
|
|
1149
|
-
if (
|
|
1150
|
-
curve &&
|
|
1151
|
-
curve.owner.equals(PUMP_PROGRAM) &&
|
|
1152
|
-
curve.data.length >= 49
|
|
1153
|
-
) {
|
|
1154
|
-
const data = Buffer.from(curve.data);
|
|
1155
|
-
const base = data.readBigUInt64LE(8);
|
|
1156
|
-
const quote = data.readBigUInt64LE(16);
|
|
1157
|
-
if (base > 0n && quote > 0n) {
|
|
1158
|
-
const priceSol =
|
|
1159
|
-
Number(quote) /
|
|
1160
|
-
LAMPORTS_PER_SOL /
|
|
1161
|
-
(Number(base) / 10 ** state.decimals);
|
|
1162
|
-
await emitPrice({
|
|
1163
|
-
type: "price",
|
|
1164
|
-
atMs: Date.now(),
|
|
1165
|
-
signature: null,
|
|
1166
|
-
slot: null,
|
|
1167
|
-
mint,
|
|
1168
|
-
venue: "pump",
|
|
1169
|
-
priceSol,
|
|
1170
|
-
source: "bootstrap-curve",
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
} catch (error) {
|
|
1175
|
-
report("mint bootstrap error", { mint, error: errorText(error) });
|
|
1176
|
-
}
|
|
272
|
+
const updateExplicitMintRef = async (
|
|
273
|
+
mint: string,
|
|
274
|
+
delta: 1 | -1,
|
|
275
|
+
): Promise<void> => {
|
|
276
|
+
const next = Math.max(0, (explicitMintRefs.get(mint) ?? 0) + delta);
|
|
277
|
+
if (next === 0) explicitMintRefs.delete(mint);
|
|
278
|
+
else explicitMintRefs.set(mint, next);
|
|
279
|
+
await syncMintSubscription(mint);
|
|
1177
280
|
};
|
|
1178
281
|
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
if (refs === 0) {
|
|
1185
|
-
if (existing) {
|
|
1186
|
-
fallbackSubs.delete(mint);
|
|
1187
|
-
logsMux.unsubscribe(existing.id);
|
|
1188
|
-
}
|
|
1189
|
-
return;
|
|
1190
|
-
}
|
|
1191
|
-
if (existing) {
|
|
1192
|
-
existing.refs = refs;
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
if (fallbackSubs.size >= maxFallbackSubscriptions) {
|
|
1196
|
-
report("fallback subscription budget reached", {
|
|
1197
|
-
mint,
|
|
1198
|
-
active: fallbackSubs.size,
|
|
1199
|
-
limit: maxFallbackSubscriptions,
|
|
1200
|
-
});
|
|
1201
|
-
return;
|
|
1202
|
-
}
|
|
1203
|
-
await bootstrapMint(mint);
|
|
1204
|
-
const id = logsMux.subscribe(new PublicKey(mint), (event) => {
|
|
1205
|
-
if (event.err) return;
|
|
1206
|
-
if (
|
|
1207
|
-
pendingFallback.has(mint) &&
|
|
1208
|
-
pendingFallback.get(mint) !== event.signature
|
|
1209
|
-
)
|
|
1210
|
-
coalescedFallback += 1;
|
|
1211
|
-
pendingFallback.set(mint, event.signature);
|
|
1212
|
-
});
|
|
1213
|
-
fallbackSubs.set(mint, { id, refs });
|
|
282
|
+
const enableAllPrices = async (client: Client): Promise<void> => {
|
|
283
|
+
if (client.allPrices) return;
|
|
284
|
+
client.allPrices = true;
|
|
285
|
+
allPriceClients += 1;
|
|
286
|
+
if (allPriceClients === 1) await syncDiscoveredSubscriptions();
|
|
1214
287
|
};
|
|
1215
288
|
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
report("fallback subscription error", {
|
|
1222
|
-
mint,
|
|
1223
|
-
error: errorText(error),
|
|
1224
|
-
}),
|
|
1225
|
-
)
|
|
1226
|
-
.then(() => undefined);
|
|
1227
|
-
fallbackOps.set(mint, next);
|
|
1228
|
-
void next.finally(() => {
|
|
1229
|
-
if (fallbackOps.get(mint) === next) fallbackOps.delete(mint);
|
|
1230
|
-
});
|
|
289
|
+
const disableAllPrices = async (client: Client): Promise<void> => {
|
|
290
|
+
if (!client.allPrices) return;
|
|
291
|
+
client.allPrices = false;
|
|
292
|
+
allPriceClients = Math.max(0, allPriceClients - 1);
|
|
293
|
+
if (allPriceClients === 0) await syncDiscoveredSubscriptions();
|
|
1231
294
|
};
|
|
1232
295
|
|
|
1233
|
-
const
|
|
1234
|
-
|
|
296
|
+
const applyCommand = async (
|
|
297
|
+
socket: unknown,
|
|
298
|
+
command: PriceFeedCommand,
|
|
299
|
+
): Promise<void> => {
|
|
300
|
+
const client = clients.get(socket);
|
|
1235
301
|
if (!client) return;
|
|
302
|
+
const target = socket as { send(value: string): unknown };
|
|
1236
303
|
if (command.op === "ping") {
|
|
1237
|
-
send(
|
|
304
|
+
send(target, { type: "status", atMs: Date.now(), event: "pong" });
|
|
1238
305
|
return;
|
|
1239
306
|
}
|
|
1240
|
-
const
|
|
307
|
+
const mints: string[] = [...new Set((command.mints ?? []).filter(Boolean))];
|
|
1241
308
|
if (command.op === "subscribe") {
|
|
1242
309
|
if (command.launches === true && !client.launches) {
|
|
1243
310
|
client.launches = true;
|
|
1244
|
-
for (const
|
|
311
|
+
for (const value of launches.values()) send(target, value);
|
|
1245
312
|
}
|
|
1246
313
|
if (command.allPrices === true && !client.allPrices) {
|
|
1247
|
-
client
|
|
1248
|
-
for (const value of latest.values()) send(
|
|
314
|
+
await enableAllPrices(client);
|
|
315
|
+
for (const value of latest.values()) send(target, value);
|
|
1249
316
|
}
|
|
1250
|
-
for (const mint of
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
317
|
+
for (const mint of mints) {
|
|
318
|
+
if (!client.mints.has(mint)) {
|
|
319
|
+
client.mints.add(mint);
|
|
320
|
+
await updateExplicitMintRef(mint, 1);
|
|
321
|
+
}
|
|
322
|
+
const value = latest.get(mint);
|
|
323
|
+
if (value) send(target, value);
|
|
1255
324
|
}
|
|
1256
325
|
return;
|
|
1257
326
|
}
|
|
1258
|
-
for (const mint of
|
|
1259
|
-
client.mints.delete(mint);
|
|
1260
|
-
|
|
327
|
+
for (const mint of mints) {
|
|
328
|
+
if (!client.mints.delete(mint)) continue;
|
|
329
|
+
await updateExplicitMintRef(mint, -1);
|
|
1261
330
|
}
|
|
1262
331
|
};
|
|
1263
332
|
|
|
333
|
+
const releaseClient = async (socket: unknown): Promise<void> => {
|
|
334
|
+
const client = clients.get(socket);
|
|
335
|
+
clients.delete(socket);
|
|
336
|
+
if (!client) return;
|
|
337
|
+
await disableAllPrices(client);
|
|
338
|
+
await Promise.allSettled(
|
|
339
|
+
[...client.mints].map((mint) => updateExplicitMintRef(mint, -1)),
|
|
340
|
+
);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
tradeSubscription = await subscribeTrades({
|
|
344
|
+
connection,
|
|
345
|
+
tokens: [],
|
|
346
|
+
commitment: "confirmed",
|
|
347
|
+
metadata: false,
|
|
348
|
+
signal: controller.signal,
|
|
349
|
+
onTrade,
|
|
350
|
+
onStatus(event, data) {
|
|
351
|
+
report(`trades ${event}`, data ?? {});
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
const launchSubscription = await subscribeLaunches({
|
|
355
|
+
connection,
|
|
356
|
+
commitment: "confirmed",
|
|
357
|
+
metadata: false,
|
|
358
|
+
signal: controller.signal,
|
|
359
|
+
onLaunch,
|
|
360
|
+
onStatus(event, data) {
|
|
361
|
+
report(`launches ${event}`, data ?? {});
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
|
|
1264
365
|
const server = Bun.serve({
|
|
1265
366
|
hostname: host,
|
|
1266
367
|
port,
|
|
1267
368
|
fetch(request, server) {
|
|
1268
369
|
const url = new URL(request.url);
|
|
1269
370
|
if (url.pathname === "/ws") {
|
|
1270
|
-
const
|
|
1271
|
-
return
|
|
371
|
+
const upgraded = server.upgrade(request);
|
|
372
|
+
return upgraded
|
|
373
|
+
? undefined
|
|
374
|
+
: new Response("upgrade failed", { status: 400 });
|
|
1272
375
|
}
|
|
1273
376
|
if (url.pathname === "/health") {
|
|
1274
377
|
return Response.json({
|
|
1275
378
|
ok: true,
|
|
1276
379
|
clients: clients.size,
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
? Date.now() - logsMux.lastMessageAtMs
|
|
1282
|
-
: null,
|
|
1283
|
-
programSubscriptions: programSubs.length,
|
|
1284
|
-
fallbackSubscriptions: fallbackSubs.size,
|
|
1285
|
-
knownMints: mints.size,
|
|
1286
|
-
cachedLaunches: launchesByMint.size,
|
|
380
|
+
launchCount,
|
|
381
|
+
tradeCount,
|
|
382
|
+
priceCount,
|
|
383
|
+
cachedLaunches: launches.size,
|
|
1287
384
|
latestPrices: latest.size,
|
|
1288
|
-
|
|
385
|
+
discoveredMints: discoveredMints.size,
|
|
386
|
+
explicitMints: explicitMintRefs.size,
|
|
387
|
+
allPriceClients,
|
|
388
|
+
watchedTokens: tradeSubscription?.listTokens().length ?? 0,
|
|
389
|
+
solUsd,
|
|
1289
390
|
});
|
|
1290
391
|
}
|
|
1291
|
-
return new Response("Solard price feed\n"
|
|
392
|
+
return new Response("Solard price feed\n");
|
|
1292
393
|
},
|
|
1293
394
|
websocket: {
|
|
1294
|
-
open(
|
|
1295
|
-
clients.set(
|
|
395
|
+
open(socket) {
|
|
396
|
+
clients.set(socket, {
|
|
1296
397
|
mints: new Set(),
|
|
1297
398
|
launches: false,
|
|
1298
399
|
allPrices: false,
|
|
1299
400
|
});
|
|
1300
|
-
|
|
1301
|
-
send(ws, {
|
|
1302
|
-
type: "status",
|
|
1303
|
-
atMs: Date.now(),
|
|
1304
|
-
event: "ready",
|
|
1305
|
-
data: { programSubscriptions: programSubs.length },
|
|
1306
|
-
});
|
|
401
|
+
send(socket, { type: "status", atMs: Date.now(), event: "ready" });
|
|
1307
402
|
},
|
|
1308
|
-
message(
|
|
403
|
+
message(socket, message) {
|
|
1309
404
|
try {
|
|
1310
|
-
|
|
1311
|
-
|
|
405
|
+
void applyCommand(
|
|
406
|
+
socket,
|
|
1312
407
|
JSON.parse(String(message)) as PriceFeedCommand,
|
|
1313
|
-
)
|
|
408
|
+
).catch((error) => {
|
|
409
|
+
send(socket, {
|
|
410
|
+
type: "status",
|
|
411
|
+
atMs: Date.now(),
|
|
412
|
+
event: "client-error",
|
|
413
|
+
data: { error: errorText(error) },
|
|
414
|
+
});
|
|
415
|
+
});
|
|
1314
416
|
} catch (error) {
|
|
1315
|
-
send(
|
|
417
|
+
send(socket, {
|
|
1316
418
|
type: "status",
|
|
1317
419
|
atMs: Date.now(),
|
|
1318
420
|
event: "client-error",
|
|
@@ -1320,334 +422,74 @@ export async function runPriceFeedServerCommand(args: {
|
|
|
1320
422
|
});
|
|
1321
423
|
}
|
|
1322
424
|
},
|
|
1323
|
-
close(
|
|
1324
|
-
|
|
1325
|
-
clients.delete(ws);
|
|
1326
|
-
if (client) for (const mint of client.mints) queueFallbackRefresh(mint);
|
|
425
|
+
close(socket) {
|
|
426
|
+
void releaseClient(socket);
|
|
1327
427
|
},
|
|
1328
428
|
},
|
|
1329
429
|
});
|
|
1330
430
|
|
|
1331
|
-
const
|
|
1332
|
-
signature: string,
|
|
1333
|
-
pool: string,
|
|
1334
|
-
slot: number | null,
|
|
1335
|
-
) => {
|
|
1336
|
-
if (createFetches.has(signature)) return;
|
|
1337
|
-
createFetches.add(signature);
|
|
431
|
+
const refreshSolUsd = async (): Promise<void> => {
|
|
1338
432
|
try {
|
|
1339
|
-
|
|
1340
|
-
connection.getParsedTransaction(signature, {
|
|
1341
|
-
commitment: "confirmed",
|
|
1342
|
-
maxSupportedTransactionVersion: 1,
|
|
1343
|
-
} as any),
|
|
1344
|
-
);
|
|
1345
|
-
if (!tx) return;
|
|
1346
|
-
const candidates = initializedMints(tx as ParsedTransactionWithMeta);
|
|
1347
|
-
const mint = candidates[0];
|
|
1348
|
-
if (!mint) return;
|
|
1349
|
-
let supply = supplyFromTransaction(tx as ParsedTransactionWithMeta, mint);
|
|
1350
|
-
if (!supply) {
|
|
1351
|
-
const loaded = await pacer.run(() =>
|
|
1352
|
-
connection.getTokenSupply(new PublicKey(mint), "confirmed"),
|
|
1353
|
-
);
|
|
1354
|
-
const supplyUi = Number(loaded.value.uiAmountString ?? "0");
|
|
1355
|
-
supply = { decimals: loaded.value.decimals, supplyUi };
|
|
1356
|
-
}
|
|
1357
|
-
const balances = tokenBalances(
|
|
1358
|
-
(tx as ParsedTransactionWithMeta).meta?.postTokenBalances,
|
|
1359
|
-
);
|
|
1360
|
-
const quoteMint = balances.some((row) => row.mint === WSOL_MINT)
|
|
1361
|
-
? WSOL_MINT
|
|
1362
|
-
: balances.some((row) => row.mint === USDC_MINT)
|
|
1363
|
-
? USDC_MINT
|
|
1364
|
-
: WSOL_MINT;
|
|
1365
|
-
pools.set(pool, {
|
|
1366
|
-
pool,
|
|
1367
|
-
mint,
|
|
1368
|
-
baseDecimals: supply.decimals,
|
|
1369
|
-
quoteMint,
|
|
1370
|
-
quoteDecimals: quoteMint === USDC_MINT ? 6 : 9,
|
|
1371
|
-
});
|
|
1372
|
-
emitLaunch({
|
|
1373
|
-
type: "launch",
|
|
1374
|
-
atMs:
|
|
1375
|
-
((tx as ParsedTransactionWithMeta).blockTime ??
|
|
1376
|
-
Math.floor(Date.now() / 1000)) * 1_000,
|
|
1377
|
-
signature,
|
|
1378
|
-
slot: slot ?? (tx as ParsedTransactionWithMeta).slot,
|
|
1379
|
-
mint,
|
|
1380
|
-
venue: "raydium-launchlab",
|
|
1381
|
-
decimals: supply.decimals,
|
|
1382
|
-
supplyUi: supply.supplyUi,
|
|
1383
|
-
quoteMint,
|
|
1384
|
-
pool,
|
|
1385
|
-
name: null,
|
|
1386
|
-
symbol: null,
|
|
1387
|
-
isMayhemMode: false,
|
|
1388
|
-
});
|
|
433
|
+
solUsd = await loadSolUsd();
|
|
1389
434
|
} catch (error) {
|
|
1390
|
-
|
|
1391
|
-
report("launchlab create enrichment error", {
|
|
1392
|
-
signature,
|
|
1393
|
-
pool,
|
|
435
|
+
report("sol/usd unavailable", {
|
|
1394
436
|
error: errorText(error),
|
|
437
|
+
cached: solUsd,
|
|
1395
438
|
});
|
|
1396
|
-
} finally {
|
|
1397
|
-
createFetches.delete(signature);
|
|
1398
439
|
}
|
|
1399
440
|
};
|
|
1400
441
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
if (!(priceSol > 0 && Number.isFinite(priceSol))) continue;
|
|
1420
|
-
void emitPrice({
|
|
1421
|
-
type: "price",
|
|
1422
|
-
atMs: trade.atMs,
|
|
1423
|
-
signature: event.signature,
|
|
1424
|
-
slot: context.slot,
|
|
1425
|
-
mint: trade.mint,
|
|
1426
|
-
venue: "pump",
|
|
1427
|
-
priceSol,
|
|
1428
|
-
source: "pump-trade-event",
|
|
1429
|
-
});
|
|
1430
|
-
}
|
|
1431
|
-
});
|
|
1432
|
-
programSubs.push(pumpSub);
|
|
1433
|
-
|
|
1434
|
-
const pumpSwapSub = logsMux.subscribe(PUMP_AMM_PROGRAM, (event, context) => {
|
|
1435
|
-
if (event.err) return;
|
|
1436
|
-
for (const data of dataEvents(event.logs)) {
|
|
1437
|
-
const created = pumpSwapCreate(data);
|
|
1438
|
-
if (created) {
|
|
1439
|
-
pools.set(created.pool, created);
|
|
1440
|
-
const state = mints.get(created.mint);
|
|
1441
|
-
if (state) {
|
|
1442
|
-
state.pool = created.pool;
|
|
1443
|
-
state.quoteMint = created.quoteMint;
|
|
1444
|
-
state.quoteDecimals = created.quoteDecimals;
|
|
1445
|
-
}
|
|
1446
|
-
continue;
|
|
1447
|
-
}
|
|
1448
|
-
const trade = pumpSwapTrade(data);
|
|
1449
|
-
if (!trade) continue;
|
|
1450
|
-
const pool = pools.get(trade.pool);
|
|
1451
|
-
if (!pool || trade.baseRaw <= 0n || trade.quoteRaw <= 0n) continue;
|
|
1452
|
-
const priceQuote =
|
|
1453
|
-
Number(trade.quoteRaw) /
|
|
1454
|
-
10 ** pool.quoteDecimals /
|
|
1455
|
-
(Number(trade.baseRaw) / 10 ** pool.baseDecimals);
|
|
1456
|
-
if (!(priceQuote > 0 && Number.isFinite(priceQuote))) continue;
|
|
1457
|
-
void emitPrice({
|
|
1458
|
-
type: "price",
|
|
1459
|
-
atMs: trade.atMs,
|
|
1460
|
-
signature: event.signature,
|
|
1461
|
-
slot: context.slot,
|
|
1462
|
-
mint: pool.mint,
|
|
1463
|
-
venue: "pumpswap",
|
|
1464
|
-
priceSol: pool.quoteMint === WSOL_MINT ? priceQuote : null,
|
|
1465
|
-
priceUsd: pool.quoteMint === USDC_MINT ? priceQuote : null,
|
|
1466
|
-
source: "pumpswap-event",
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
});
|
|
1470
|
-
programSubs.push(pumpSwapSub);
|
|
1471
|
-
|
|
1472
|
-
const launchLabSub = logsMux.subscribe(
|
|
1473
|
-
LAUNCHLAB_PROGRAM,
|
|
1474
|
-
(event, context) => {
|
|
1475
|
-
if (event.err) return;
|
|
1476
|
-
for (const data of dataEvents(event.logs)) {
|
|
1477
|
-
const createdPool = launchLabPoolCreate(data);
|
|
1478
|
-
if (createdPool) {
|
|
1479
|
-
void createFromLaunchLab(event.signature, createdPool, context.slot);
|
|
1480
|
-
continue;
|
|
1481
|
-
}
|
|
1482
|
-
const trade = launchLabTrade(data);
|
|
1483
|
-
if (!trade) continue;
|
|
1484
|
-
const pool = pools.get(trade.pool);
|
|
1485
|
-
if (!pool || trade.virtualBase <= 0n || trade.virtualQuote <= 0n)
|
|
1486
|
-
continue;
|
|
1487
|
-
const priceQuote =
|
|
1488
|
-
Number(trade.virtualQuote) /
|
|
1489
|
-
10 ** pool.quoteDecimals /
|
|
1490
|
-
(Number(trade.virtualBase) / 10 ** pool.baseDecimals);
|
|
1491
|
-
if (!(priceQuote > 0 && Number.isFinite(priceQuote))) continue;
|
|
1492
|
-
void emitPrice({
|
|
1493
|
-
type: "price",
|
|
1494
|
-
atMs: Date.now(),
|
|
1495
|
-
signature: event.signature,
|
|
1496
|
-
slot: context.slot,
|
|
1497
|
-
mint: pool.mint,
|
|
1498
|
-
venue: "raydium-launchlab",
|
|
1499
|
-
priceSol: pool.quoteMint === WSOL_MINT ? priceQuote : null,
|
|
1500
|
-
priceUsd: pool.quoteMint === USDC_MINT ? priceQuote : null,
|
|
1501
|
-
source: "launchlab-trade-event",
|
|
1502
|
-
});
|
|
1503
|
-
}
|
|
1504
|
-
},
|
|
442
|
+
await refreshSolUsd();
|
|
443
|
+
const solTimer = setInterval(() => void refreshSolUsd(), 60_000);
|
|
444
|
+
const heartbeat = setInterval(
|
|
445
|
+
() =>
|
|
446
|
+
report("heartbeat", {
|
|
447
|
+
clients: clients.size,
|
|
448
|
+
launchCount,
|
|
449
|
+
tradeCount,
|
|
450
|
+
priceCount,
|
|
451
|
+
cachedLaunches: launches.size,
|
|
452
|
+
latestPrices: latest.size,
|
|
453
|
+
discoveredMints: discoveredMints.size,
|
|
454
|
+
explicitMints: explicitMintRefs.size,
|
|
455
|
+
allPriceClients,
|
|
456
|
+
watchedTokens: tradeSubscription?.listTokens().length ?? 0,
|
|
457
|
+
solUsd,
|
|
458
|
+
}),
|
|
459
|
+
Math.max(1_000, Math.trunc(numberFlag(args.flags, "heartbeat-ms", 15_000))),
|
|
1505
460
|
);
|
|
1506
|
-
programSubs.push(launchLabSub);
|
|
1507
461
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
}
|
|
1516
|
-
const [mint, signature] = entry;
|
|
1517
|
-
pendingFallback.delete(mint);
|
|
1518
|
-
const state = mints.get(mint);
|
|
1519
|
-
if (!state) continue;
|
|
1520
|
-
try {
|
|
1521
|
-
const tx = await pacer.run(() =>
|
|
1522
|
-
connection.getParsedTransaction(signature, {
|
|
1523
|
-
commitment: "confirmed",
|
|
1524
|
-
maxSupportedTransactionVersion: 1,
|
|
1525
|
-
} as any),
|
|
1526
|
-
);
|
|
1527
|
-
if (!tx) continue;
|
|
1528
|
-
const parsed = genericPrice(
|
|
1529
|
-
tx as ParsedTransactionWithMeta,
|
|
1530
|
-
mint,
|
|
1531
|
-
state.decimals,
|
|
1532
|
-
);
|
|
1533
|
-
if (!parsed) continue;
|
|
1534
|
-
await emitPrice({
|
|
1535
|
-
type: "price",
|
|
1536
|
-
atMs: parsed.atMs,
|
|
1537
|
-
signature,
|
|
1538
|
-
slot: (tx as ParsedTransactionWithMeta).slot,
|
|
1539
|
-
mint,
|
|
1540
|
-
venue: "rpc-fallback",
|
|
1541
|
-
priceSol: parsed.priceSol,
|
|
1542
|
-
priceUsd: parsed.priceUsd,
|
|
1543
|
-
source: "explicit-mint-fallback",
|
|
1544
|
-
});
|
|
1545
|
-
} catch (error) {
|
|
1546
|
-
rpcErrors += 1;
|
|
1547
|
-
report("fallback enrichment error", {
|
|
1548
|
-
mint,
|
|
1549
|
-
signature,
|
|
1550
|
-
error: errorText(error),
|
|
1551
|
-
});
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
};
|
|
1555
|
-
|
|
1556
|
-
let heartbeat: ReturnType<typeof setInterval> | null = null;
|
|
1557
|
-
let solTimer: ReturnType<typeof setInterval> | null = null;
|
|
1558
|
-
let pruneTimer: ReturnType<typeof setInterval> | null = null;
|
|
462
|
+
report("ready", {
|
|
463
|
+
url: `ws://${host}:${server.port}/ws`,
|
|
464
|
+
health: `http://${host}:${server.port}/health`,
|
|
465
|
+
rpc: publicEndpoint(rpc),
|
|
466
|
+
ws: ws ? publicEndpoint(ws) : "connection-default",
|
|
467
|
+
includeMayhem,
|
|
468
|
+
});
|
|
1559
469
|
|
|
1560
|
-
const stop = () => {
|
|
470
|
+
const stop = (): void => {
|
|
1561
471
|
if (stopped) return;
|
|
1562
472
|
stopped = true;
|
|
1563
|
-
|
|
1564
|
-
try {
|
|
1565
|
-
server.stop(true);
|
|
1566
|
-
} catch {}
|
|
473
|
+
controller.abort();
|
|
1567
474
|
};
|
|
1568
475
|
process.once("SIGINT", stop);
|
|
1569
476
|
process.once("SIGTERM", stop);
|
|
1570
477
|
|
|
1571
478
|
try {
|
|
1572
|
-
|
|
1573
|
-
await logsMux.start();
|
|
1574
|
-
} catch (error) {
|
|
1575
|
-
if (!stopped) throw error;
|
|
1576
|
-
return;
|
|
1577
|
-
}
|
|
1578
|
-
if (stopped) return;
|
|
1579
|
-
await refreshSolUsd();
|
|
1580
|
-
report("ready", {
|
|
1581
|
-
url: `ws://${host}:${port}/ws`,
|
|
1582
|
-
health: `http://${host}:${port}/health`,
|
|
1583
|
-
upstreamEndpoint: publicEndpoint(ws),
|
|
1584
|
-
upstreamState: logsMux.state,
|
|
1585
|
-
upstreamProgramSubscriptions: programSubs.length,
|
|
1586
|
-
maxFallbackSubscriptions,
|
|
1587
|
-
rpcReadRps: readRps,
|
|
1588
|
-
rpcHardRps: hardRps,
|
|
1589
|
-
includeMayhem,
|
|
1590
|
-
});
|
|
1591
|
-
|
|
1592
|
-
heartbeat = setInterval(
|
|
1593
|
-
() => {
|
|
1594
|
-
report("heartbeat", {
|
|
1595
|
-
clients: clients.size,
|
|
1596
|
-
clientConnections,
|
|
1597
|
-
upstreamState: logsMux.state,
|
|
1598
|
-
upstreamSubscriptions: logsMux.subscriptionCount,
|
|
1599
|
-
upstreamProgramSubscriptions: programSubs.length,
|
|
1600
|
-
upstreamReconnects: logsMux.reconnects,
|
|
1601
|
-
upstreamLastMessageAgeMs: logsMux.lastMessageAtMs
|
|
1602
|
-
? Date.now() - logsMux.lastMessageAtMs
|
|
1603
|
-
: null,
|
|
1604
|
-
fallbackSubscriptions: fallbackSubs.size,
|
|
1605
|
-
knownMints: mints.size,
|
|
1606
|
-
cachedLaunches: launchesByMint.size,
|
|
1607
|
-
pools: pools.size,
|
|
1608
|
-
latestPrices: latest.size,
|
|
1609
|
-
launches,
|
|
1610
|
-
prices,
|
|
1611
|
-
pendingFallbackMints: pendingFallback.size,
|
|
1612
|
-
coalescedFallbackUpdates: coalescedFallback,
|
|
1613
|
-
rpcQueue: pacer.queued,
|
|
1614
|
-
rpcErrors,
|
|
1615
|
-
solUsd,
|
|
1616
|
-
solUsdAgeMs: solUsdAt ? Date.now() - solUsdAt : null,
|
|
1617
|
-
});
|
|
1618
|
-
},
|
|
1619
|
-
Math.max(
|
|
1620
|
-
1_000,
|
|
1621
|
-
Math.trunc(numberFlag(args.flags, "heartbeat-ms", 15_000)),
|
|
1622
|
-
),
|
|
1623
|
-
);
|
|
1624
|
-
solTimer = setInterval(() => void refreshSolUsd(), 30_000);
|
|
1625
|
-
pruneTimer = setInterval(() => {
|
|
1626
|
-
const cutoff = Date.now() - 6 * 60 * 60_000;
|
|
1627
|
-
if (mints.size > 50_000) {
|
|
1628
|
-
for (const [mint, state] of mints) {
|
|
1629
|
-
if (state.lastSeenAtMs < cutoff && !fallbackSubs.has(mint)) {
|
|
1630
|
-
mints.delete(mint);
|
|
1631
|
-
latest.delete(mint);
|
|
1632
|
-
launchesByMint.delete(mint);
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
}, 60_000);
|
|
1637
|
-
|
|
1638
|
-
await fallbackLoop();
|
|
479
|
+
await Promise.all([launchSubscription.closed, tradeSubscription.closed]);
|
|
1639
480
|
} finally {
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
if (pruneTimer) clearInterval(pruneTimer);
|
|
481
|
+
clearInterval(solTimer);
|
|
482
|
+
clearInterval(heartbeat);
|
|
1643
483
|
process.removeListener("SIGINT", stop);
|
|
1644
484
|
process.removeListener("SIGTERM", stop);
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
485
|
+
await launchSubscription.close();
|
|
486
|
+
await tradeSubscription.close();
|
|
487
|
+
server.stop(true);
|
|
488
|
+
report("stopped", {
|
|
489
|
+
launchCount,
|
|
490
|
+
tradeCount,
|
|
491
|
+
priceCount,
|
|
492
|
+
clients: clients.size,
|
|
493
|
+
});
|
|
1652
494
|
}
|
|
1653
495
|
}
|