@solard/cli 0.2.21 → 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,18 +1,17 @@
1
1
  import { configure, createMeasure } from "measure-fn";
2
- import WebSocket, { type RawData } from "ws";
3
- import bs58 from "bs58";
4
- import { bondingCurvePda } from "@solard/core";
2
+ import { Connection } from "@solana/web3.js";
5
3
  import {
6
- Connection,
7
- PublicKey,
8
- type ParsedTransactionWithMeta,
9
- } from "@solana/web3.js";
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
- mint: string;
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 value = flag(flags, key);
80
- if (value == null) return fallback;
81
- const parsed = Number(value);
82
- if (!Number.isFinite(parsed)) throw new Error(`Invalid --${key}: ${value}`);
83
- 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;
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, rpc: string): string {
100
- const explicit =
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
- if (explicit) return explicit;
105
- const url = new URL(rpc);
106
- if (url.protocol === "https:") url.protocol = "wss:";
107
- else if (url.protocol === "http:") url.protocol = "ws:";
108
- else if (url.protocol !== "wss:" && url.protocol !== "ws:")
109
- throw new Error(`Cannot derive WebSocket endpoint from ${url.protocol}`);
110
- 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 };
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 safeErrorText(error: unknown, endpoint?: string): string {
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?.data?.amount);
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?.solana?.usd);
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 } = feedUrl(args.flags);
144
+ const { host, port } = feedAddress(args.flags);
992
145
  const rpc = rpcUrl(args.flags);
993
- const ws = websocketUrl(args.flags, rpc);
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 maxFallbackSubscriptions = Math.max(
1004
- 1,
1005
- Math.trunc(numberFlag(args.flags, "max-fallback-subs", 200)),
148
+ const connection = new Connection(
149
+ rpc,
150
+ ws ? { commitment: "confirmed", wsEndpoint: ws } : "confirmed",
1006
151
  );
1007
- const clients = new Map<any, Client>();
1008
- const mints = new Map<string, MintState>();
1009
- const pools = new Map<string, PoolState>();
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 fallbackSubs = new Map<string, { id: number; refs: number }>();
1013
- const fallbackOps = new Map<string, Promise<void>>();
1014
- const pendingFallback = new Map<string, string>();
1015
- const createFetches = new Set<string>();
1016
- const programSubs: number[] = [];
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 solUsdAt = 0;
1025
- let solUsdRetryAt = 0;
1026
- let solUsdFailures = 0;
1027
- let solUsdPromise: Promise<number | null> | null = null;
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 = (ws: any, value: PriceFeedMessage) => {
167
+ const send = (
168
+ socket: { send(value: string): unknown },
169
+ value: PriceFeedMessage,
170
+ ): void => {
1062
171
  try {
1063
- ws.send(JSON.stringify(value));
172
+ socket.send(JSON.stringify(value));
1064
173
  } catch {}
1065
174
  };
1066
175
 
1067
- const broadcast = (value: PriceFeedMessage) => {
1068
- 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 };
1069
179
  if (value.type === "launch") {
1070
- if (client.launches) send(ws, value);
1071
- } else if (value.type === "price") {
1072
- if (client.allPrices || client.mints.has(value.mint)) send(ws, value);
1073
- } 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);
1074
189
  }
1075
190
  };
1076
191
 
1077
- const emitLaunch = (value: PriceFeedLaunch) => {
1078
- if (value.isMayhemMode === true && !includeMayhem) return;
1079
- const existing = mints.get(value.mint);
1080
- mints.set(value.mint, {
1081
- ...(existing ?? {}),
1082
- mint: value.mint,
1083
- venue: value.venue,
1084
- decimals: value.decimals,
1085
- supplyUi: value.supplyUi,
1086
- quoteMint: value.quoteMint,
1087
- quoteDecimals: value.quoteMint === USDC_MINT ? 6 : 9,
1088
- pool: value.pool ?? existing?.pool ?? null,
1089
- name: value.name ?? existing?.name ?? null,
1090
- symbol: value.symbol ?? existing?.symbol ?? null,
1091
- isMayhemMode: value.isMayhemMode,
1092
- 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,
1093
230
  });
1094
- launchesByMint.set(value.mint, value);
1095
- launches += 1;
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 emitPrice = async (
1100
- input: Omit<PriceFeedPrice, "priceUsd" | "marketCapUsd"> & {
1101
- priceUsd?: number | null;
1102
- marketCapUsd?: number | null;
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
- const usd =
1108
- input.priceUsd ??
1109
- (input.priceSol != null
1110
- ? (await refreshSolUsd()) != null
1111
- ? input.priceSol * solUsd!
1112
- : null
1113
- : null);
1114
- const marketCapUsd =
1115
- input.marketCapUsd ??
1116
- (usd != null && state?.supplyUi ? usd * state.supplyUi : null);
1117
- const value: PriceFeedPrice = { ...input, priceUsd: usd, marketCapUsd };
1118
- latest.set(value.mint, value);
1119
- if (state) state.lastSeenAtMs = Math.max(state.lastSeenAtMs, value.atMs);
1120
- 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;
1121
269
  broadcast(value);
1122
270
  };
1123
271
 
1124
- const bootstrapMint = async (mint: string) => {
1125
- if (mints.has(mint)) return;
1126
- try {
1127
- const key = new PublicKey(mint);
1128
- const supply = await pacer.run(() =>
1129
- connection.getTokenSupply(key, "confirmed"),
1130
- );
1131
- const curve = await pacer.run(() =>
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 refreshFallbackRef = async (mint: string) => {
1180
- let refs = 0;
1181
- for (const client of clients.values())
1182
- if (client.mints.has(mint)) refs += 1;
1183
- const existing = fallbackSubs.get(mint);
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 queueFallbackRefresh = (mint: string) => {
1217
- const previous = fallbackOps.get(mint) ?? Promise.resolve();
1218
- const next = previous
1219
- .then(() => refreshFallbackRef(mint))
1220
- .catch((error) =>
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 applyClientCommand = (ws: any, command: PriceFeedCommand) => {
1234
- const client = clients.get(ws);
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(ws, { type: "status", atMs: Date.now(), event: "pong" });
304
+ send(target, { type: "status", atMs: Date.now(), event: "pong" });
1238
305
  return;
1239
306
  }
1240
- const rows = (command.mints ?? []).filter(Boolean);
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 launch of launchesByMint.values()) send(ws, launch);
311
+ for (const value of launches.values()) send(target, value);
1245
312
  }
1246
313
  if (command.allPrices === true && !client.allPrices) {
1247
- client.allPrices = true;
1248
- for (const value of latest.values()) send(ws, value);
314
+ await enableAllPrices(client);
315
+ for (const value of latest.values()) send(target, value);
1249
316
  }
1250
- for (const mint of rows) client.mints.add(mint);
1251
- for (const mint of rows) {
1252
- const cached = latest.get(mint);
1253
- if (cached) send(ws, cached);
1254
- 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);
1255
324
  }
1256
325
  return;
1257
326
  }
1258
- for (const mint of rows) {
1259
- client.mints.delete(mint);
1260
- queueFallbackRefresh(mint);
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 ok = server.upgrade(request);
1271
- 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 });
1272
375
  }
1273
376
  if (url.pathname === "/health") {
1274
377
  return Response.json({
1275
378
  ok: true,
1276
379
  clients: clients.size,
1277
- upstreamState: logsMux.state,
1278
- upstreamSubscriptions: logsMux.subscriptionCount,
1279
- upstreamReconnects: logsMux.reconnects,
1280
- upstreamLastMessageAgeMs: logsMux.lastMessageAtMs
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
- rpcQueue: pacer.queued,
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", { status: 200 });
392
+ return new Response("Solard price feed\n");
1292
393
  },
1293
394
  websocket: {
1294
- open(ws) {
1295
- clients.set(ws, {
395
+ open(socket) {
396
+ clients.set(socket, {
1296
397
  mints: new Set(),
1297
398
  launches: false,
1298
399
  allPrices: false,
1299
400
  });
1300
- clientConnections += 1;
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(ws, message) {
403
+ message(socket, message) {
1309
404
  try {
1310
- applyClientCommand(
1311
- ws,
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(ws, {
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(ws) {
1324
- const client = clients.get(ws);
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 createFromLaunchLab = async (
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
- const tx = await pacer.run(() =>
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
- rpcErrors += 1;
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
- const pumpSub = logsMux.subscribe(PUMP_PROGRAM, (event, context) => {
1402
- if (event.err) return;
1403
- for (const data of dataEvents(event.logs)) {
1404
- const created = pumpCreate(data);
1405
- if (created) {
1406
- created.signature = event.signature;
1407
- created.slot = context.slot;
1408
- emitLaunch(created);
1409
- continue;
1410
- }
1411
- const trade = pumpTrade(data);
1412
- if (!trade) continue;
1413
- const state = mints.get(trade.mint);
1414
- if (!state || state.isMayhemMode === true) continue;
1415
- if (state.quoteMint && state.quoteMint !== WSOL_MINT) continue;
1416
- const virtualTokens = Number(trade.virtualToken) / 10 ** state.decimals;
1417
- const virtualSol = Number(trade.virtualSol) / LAMPORTS_PER_SOL;
1418
- const priceSol = virtualTokens > 0 ? virtualSol / virtualTokens : 0;
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
- const fallbackLoop = async () => {
1509
- while (!stopped) {
1510
- const entry = pendingFallback.entries().next().value as
1511
- [string, string] | undefined;
1512
- if (!entry) {
1513
- await sleep(25);
1514
- continue;
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
- logsMux.stop();
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
- try {
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
- if (heartbeat) clearInterval(heartbeat);
1641
- if (solTimer) clearInterval(solTimer);
1642
- if (pruneTimer) clearInterval(pruneTimer);
481
+ clearInterval(solTimer);
482
+ clearInterval(heartbeat);
1643
483
  process.removeListener("SIGINT", stop);
1644
484
  process.removeListener("SIGTERM", stop);
1645
- for (const id of programSubs) logsMux.unsubscribe(id);
1646
- for (const row of fallbackSubs.values()) logsMux.unsubscribe(row.id);
1647
- logsMux.stop();
1648
- try {
1649
- server.stop(true);
1650
- } catch {}
1651
- report("stopped", { clients: clients.size, launches, prices, rpcErrors });
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
  }