@solard/cli 0.2.20 → 0.2.22

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