sol-parser-sdk 0.5.12 → 0.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,12 +38,20 @@
38
38
  | Parser inputs | Yellowstone gRPC, ShredStream, RPC transactions, encoded transactions, protocol account data |
39
39
  | DEX protocols | PumpFun, PumpSwap, Pump Fees, Raydium LaunchLab, Raydium CPMM, Raydium CLMM, Raydium AMM V4, Meteora DAMM v2, Meteora DLMM, Meteora DBC, Orca Whirlpool |
40
40
  | Use cases | Real-time DEX event parsing, token launch monitoring, copy trading, sniper bots, account filling, JSON event pipelines |
41
- | Runtime | Node.js 18+, TypeScript, npm/yarn/pnpm projects |
41
+ | Runtime | Node.js 20.18+, TypeScript, npm/yarn/pnpm projects |
42
42
 
43
43
  ---
44
44
 
45
45
  ## Release notes
46
46
 
47
+ ### v0.5.14
48
+
49
+ - Upgrades the Yellowstone client to `@triton-one/yellowstone-grpc` 7.0.0 with its native Rust N-API transport.
50
+ - Adds `source_to_grpc_latency_us` to distinguish provider or transport backlog from exact local processing latency.
51
+ - Fixes Yellowstone `createdAt` timestamps represented as JavaScript `Date` objects.
52
+ - Updates native connection, keepalive, flow-control, stream lifecycle, and unary RPC compatibility.
53
+ - Requires Node.js 20.18 or newer.
54
+
47
55
  ### v0.5.8
48
56
 
49
57
  - Syncs parser parity with the Rust SDK event model and program routing.
@@ -75,7 +83,7 @@
75
83
  **From npm**
76
84
 
77
85
  ```bash
78
- npm install sol-parser-sdk@0.5.12
86
+ npm install sol-parser-sdk@0.5.14
79
87
  ```
80
88
 
81
89
  **From source** (folder may be named `sol-parser-sdk-ts` in a monorepo)
@@ -176,6 +184,14 @@ queue with `sub.len()` / `sub.eventDropped()` and the pre-parser queue with
176
184
  `sub.ingressLen()` / `sub.ingressDropped()`. `sub.dropped()` is the combined count. Every overflow
177
185
  is counted and periodically reported through `sub.errors`.
178
186
 
187
+ Automatic reconnect resumes from the last successfully parsed slot by default and deduplicates
188
+ replayed transaction/account updates. Monitor transport continuity with `sub.isStreamConnected()`,
189
+ `sub.streamDisconnects()`, `sub.reconnects()`, `sub.replayedUpdates()`, and
190
+ `sub.continuityBreaks()`. A zero local drop count does not describe updates lost before the gRPC
191
+ `data` callback. If a provider rejects Yellowstone `fromSlot`, the SDK reports the failure through
192
+ `sub.errors`, increments `continuityBreaks()`, and reconnects live. Set `replayOnReconnect: false`
193
+ only when duplicate-free live delivery is more important than recovering a disconnect window.
194
+
179
195
  For precise local-only measurements, set `config.enable_metrics = true`. Event metadata then includes
180
196
  `local_queue_latency_us`, `parse_duration_us`, and `local_processing_latency_us`. These values use
181
197
  Node's monotonic high-resolution clock. They measure only local queueing and parsing; the hot path
package/README_CN.md CHANGED
@@ -38,12 +38,20 @@
38
38
  | 解析输入 | Yellowstone gRPC、ShredStream、RPC 交易、编码交易、协议账户数据 |
39
39
  | DEX 协议 | PumpFun、PumpSwap、Pump Fees、Raydium LaunchLab、Raydium CPMM、Raydium CLMM、Raydium AMM V4、Meteora DAMM v2、Meteora DLMM、Meteora DBC、Orca Whirlpool |
40
40
  | 使用场景 | 实时 DEX 事件解析、代币发射监控、跟单交易、狙击机器人、账户填充、JSON 事件管道 |
41
- | 运行环境 | Node.js 18+、TypeScript、npm/yarn/pnpm 项目 |
41
+ | 运行环境 | Node.js 20.18+、TypeScript、npm/yarn/pnpm 项目 |
42
42
 
43
43
  ---
44
44
 
45
45
  ## 发布说明
46
46
 
47
+ ### v0.5.14
48
+
49
+ - 将 Yellowstone 客户端升级到 `@triton-one/yellowstone-grpc` 7.0.0,使用其原生 Rust N-API 传输层。
50
+ - 新增 `source_to_grpc_latency_us`,用于区分服务商或传输层积压与精确的本地处理延迟。
51
+ - 修复 Yellowstone `createdAt` 使用 JavaScript `Date` 对象表示时的时间戳解析。
52
+ - 更新原生连接、keepalive、流量控制、流生命周期和 unary RPC 兼容处理。
53
+ - 要求 Node.js 20.18 或更高版本。
54
+
47
55
  ### v0.5.8
48
56
 
49
57
  - 与 Rust SDK 的事件模型和 program 路由保持解析对齐。
@@ -75,7 +83,7 @@
75
83
  **npm**
76
84
 
77
85
  ```bash
78
- npm install sol-parser-sdk@0.5.12
86
+ npm install sol-parser-sdk@0.5.14
79
87
  ```
80
88
 
81
89
  **源码**(monorepo 里目录可能是 `sol-parser-sdk-ts`)
@@ -174,6 +182,13 @@ for await (const event of sub) {
174
182
  公开事件队列,通过 `sub.ingressLen()` / `sub.ingressDropped()` 监控解析前入口队列;
175
183
  `sub.dropped()` 返回两者之和。每次溢出都会计数,并定期通过 `sub.errors` 报告。
176
184
 
185
+ 自动重连默认从最后一个成功解析的 slot 继续订阅,并对回放的 transaction/account update
186
+ 去重。可通过 `sub.isStreamConnected()`、`sub.streamDisconnects()`、`sub.reconnects()`、
187
+ `sub.replayedUpdates()` 和 `sub.continuityBreaks()` 监控传输连续性。本地 drop 为零并不能说明
188
+ 进入 gRPC `data` 回调之前没有丢数据。如果 provider 拒绝 Yellowstone `fromSlot`,SDK 会通过
189
+ `sub.errors` 明确报告,增加 `continuityBreaks()`,再降级为 live 重连。只有在“绝不接收回放重复”
190
+ 比“补偿断线窗口”更重要时,才应设置 `replayOnReconnect: false`。
191
+
177
192
  如需精确测量纯本地延迟,请设置 `config.enable_metrics = true`。事件 metadata 将包含
178
193
  `local_queue_latency_us`、`parse_duration_us` 和 `local_processing_latency_us`,这些字段使用
179
194
  Node 单调高精度时钟,只测量本地排队与解析。热路径不会调用 `getSlot`、`getTransaction`、
@@ -12,6 +12,8 @@ export interface EventMetadata {
12
12
  parse_duration_us?: number;
13
13
  /** Start of the local gRPC data callback through parser completion. No RPC calls are included. */
14
14
  local_processing_latency_us?: number;
15
+ /** Yellowstone update creation through the local gRPC data callback; includes provider and transport delay. */
16
+ source_to_grpc_latency_us?: number;
15
17
  recent_blockhash?: string;
16
18
  }
17
19
  export declare function makeMetadata(signature: string, slot: number, txIndex: number, blockTimeUs: number | undefined, grpcRecvUs: number, recentBlockhash?: string): EventMetadata;
@@ -18,6 +18,12 @@ export interface DexEventSubscription extends AsyncIterable<DexEvent> {
18
18
  eventDropped(): number;
19
19
  ingressLen(): number;
20
20
  ingressDropped(): number;
21
+ isStreamConnected(): boolean;
22
+ streamDisconnects(): number;
23
+ reconnects(): number;
24
+ replayedUpdates(): number;
25
+ /** Reconnects that could not resume from the last parsed slot. */
26
+ continuityBreaks(): number;
21
27
  }
22
28
  export interface SubscribeDexEventsOptions {
23
29
  autoReconnect?: boolean;
@@ -30,12 +36,17 @@ export interface SubscribeDexEventsOptions {
30
36
  ingressBufferSize?: number;
31
37
  /** Maximum raw updates parsed before yielding back to Node's event loop. Defaults to 8. */
32
38
  parserBatchSize?: number;
39
+ /** Resume from the last parsed slot after reconnect. Defaults to true. */
40
+ replayOnReconnect?: boolean;
41
+ /** Parsed slots retained for replay deduplication. Defaults to 128. */
42
+ replayDedupeSlots?: number;
33
43
  }
34
44
  /** Yellowstone gRPC 客户端包装器 */
35
45
  export declare class YellowstoneGrpc {
36
46
  private client;
37
47
  private config;
38
48
  private connected;
49
+ private connectPromise?;
39
50
  private subscribers;
40
51
  private dexSubscribers;
41
52
  constructor(endpoint: string, xToken: string, config?: ClientConfig);
@@ -42,6 +42,7 @@ exports.YellowstoneGrpc = void 0;
42
42
  */
43
43
  const yellowstone_grpc_1 = __importStar(require("@triton-one/yellowstone-grpc"));
44
44
  const bs58_1 = __importDefault(require("bs58"));
45
+ const node_buffer_1 = require("node:buffer");
45
46
  const dex_event_js_1 = require("../core/dex_event.js");
46
47
  const metadata_js_1 = require("../core/metadata.js");
47
48
  const mod_js_1 = require("../accounts/mod.js");
@@ -54,10 +55,10 @@ const types_js_1 = require("./types.js");
54
55
  function cancelSubscribeStream(stream) {
55
56
  if (!stream)
56
57
  return;
57
- // grpc-js emits CANCELLED through `error`; guard the short setup window before
58
+ // Stream destruction can emit an error during the short setup window before
58
59
  // the normal lifecycle listeners have been installed.
59
60
  stream.once("error", () => { });
60
- stream.cancel();
61
+ stream.destroy();
61
62
  }
62
63
  function positiveIntegerOption(value, fallback, name) {
63
64
  const normalized = value ?? fallback;
@@ -66,11 +67,41 @@ function positiveIntegerOption(value, fallback, name) {
66
67
  }
67
68
  return normalized;
68
69
  }
70
+ function replayUnsupported(error) {
71
+ const grpcError = error;
72
+ if (grpcError.code === 3 || grpcError.code === 11 || grpcError.code === 12)
73
+ return true;
74
+ return /from.?slot.*(?:unsupported|disabled|unavailable|invalid)/i.test(`${grpcError.message ?? ""} ${grpcError.details ?? ""}`);
75
+ }
76
+ function updateSlot(update) {
77
+ const value = update.transaction?.slot ?? update.account?.slot;
78
+ if (value === undefined || value === null)
79
+ return undefined;
80
+ try {
81
+ return BigInt(value);
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ function bytesKey(value) {
88
+ return node_buffer_1.Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("base64");
89
+ }
90
+ function updateIdentity(update) {
91
+ const transaction = update.transaction?.transaction;
92
+ if (transaction?.signature)
93
+ return `t:${bytesKey(transaction.signature)}`;
94
+ const account = update.account?.account;
95
+ if (!account?.pubkey)
96
+ return undefined;
97
+ return `a:${bytesKey(account.pubkey)}:${String(update.account?.slot ?? 0)}:${String(account.writeVersion ?? 0)}`;
98
+ }
69
99
  /** Yellowstone gRPC 客户端包装器 */
70
100
  class YellowstoneGrpc {
71
101
  client;
72
102
  config;
73
103
  connected = false;
104
+ connectPromise;
74
105
  subscribers = new Map();
75
106
  dexSubscribers = new Map();
76
107
  constructor(endpoint, xToken, config = (0, types_js_1.defaultClientConfig)()) {
@@ -87,10 +118,20 @@ class YellowstoneGrpc {
87
118
  }
88
119
  /** 连接到 gRPC 服务器 */
89
120
  async connect() {
90
- if (this.connected) {
121
+ if (this.connected)
91
122
  return;
123
+ if (!this.connectPromise) {
124
+ this.connectPromise = this.client.connect().then(() => {
125
+ this.connected = true;
126
+ });
127
+ }
128
+ try {
129
+ await this.connectPromise;
130
+ }
131
+ catch (error) {
132
+ this.connectPromise = undefined;
133
+ throw error;
92
134
  }
93
- this.connected = true;
94
135
  }
95
136
  /** 断开连接 */
96
137
  async disconnect() {
@@ -103,6 +144,7 @@ class YellowstoneGrpc {
103
144
  }
104
145
  this.dexSubscribers.clear();
105
146
  this.connected = false;
147
+ this.connectPromise = undefined;
106
148
  }
107
149
  /** 检查是否已连接 */
108
150
  isConnected() {
@@ -253,6 +295,10 @@ class YellowstoneGrpc {
253
295
  return Number.NaN;
254
296
  }
255
297
  static timestampToMicros(value) {
298
+ if (value instanceof Date) {
299
+ const milliseconds = value.getTime();
300
+ return Number.isFinite(milliseconds) ? Math.trunc(milliseconds * 1_000) : undefined;
301
+ }
256
302
  if (!value || typeof value !== "object")
257
303
  return undefined;
258
304
  const ts = value;
@@ -321,6 +367,7 @@ class YellowstoneGrpc {
321
367
  }
322
368
  /** 订阅交易 */
323
369
  async subscribeTransactions(filter, callbacks) {
370
+ await this.connect();
324
371
  const id = `sub_${Date.now()}_${Math.random().toString(36).slice(2)}`;
325
372
  let isCancelled = false;
326
373
  const streamHolder = { current: null };
@@ -440,12 +487,24 @@ class YellowstoneGrpc {
440
487
  const events = new async_event_queue_js_1.AsyncEventQueue(Math.max(1, this.config.buffer_size), options?.queueOverflowStrategy);
441
488
  const ingressBufferSize = positiveIntegerOption(options?.ingressBufferSize, Math.min(this.config.buffer_size, 1024), "ingressBufferSize");
442
489
  const parserBatchSize = positiveIntegerOption(options?.parserBatchSize, 8, "parserBatchSize");
490
+ const replayDedupeSlots = positiveIntegerOption(options?.replayDedupeSlots, 128, "replayDedupeSlots");
491
+ const replayOnReconnect = options?.replayOnReconnect !== false;
443
492
  const ingress = new async_event_queue_js_1.AsyncEventQueue(ingressBufferSize, options?.queueOverflowStrategy);
444
493
  const errors = new async_event_queue_js_1.AsyncEventQueue(Math.max(1, this.config.buffer_size));
445
494
  const order = new order_buffer_js_1.OrderDispatcher(this.config);
446
495
  const eventDropped = events.dropped.bind(events);
447
496
  let nextEventDropWarningAt = 0;
448
497
  let nextIngressDropWarningAt = 0;
498
+ let streamConnected = false;
499
+ let streamDisconnectCount = 0;
500
+ let reconnectCount = 0;
501
+ let replayedUpdateCount = 0;
502
+ let continuityBreakCount = 0;
503
+ let lastParsedSlot;
504
+ let highestRememberedSlot = 0n;
505
+ let seenHead = 0;
506
+ const seenUpdates = new Map();
507
+ const seenOrder = [];
449
508
  const reportError = (error) => errors.push(error);
450
509
  const emitEvent = (event) => {
451
510
  if (events.push(event))
@@ -474,7 +533,7 @@ class YellowstoneGrpc {
474
533
  for (const resolve of ingressIdleWaiters.splice(0))
475
534
  resolve();
476
535
  };
477
- const annotateLocalMetrics = (events, grpcRecvNs, parseStartedNs, parseFinishedNs) => {
536
+ const annotateLocalMetrics = (events, grpcRecvNs, parseStartedNs, parseFinishedNs, sourceToGrpcLatencyUs) => {
478
537
  if (grpcRecvNs === undefined || parseStartedNs === undefined || parseFinishedNs === undefined) {
479
538
  return;
480
539
  }
@@ -488,9 +547,42 @@ class YellowstoneGrpc {
488
547
  metadata.local_queue_latency_us = queueUs;
489
548
  metadata.parse_duration_us = parseUs;
490
549
  metadata.local_processing_latency_us = processingUs;
550
+ if (sourceToGrpcLatencyUs !== undefined) {
551
+ metadata.source_to_grpc_latency_us = sourceToGrpcLatencyUs;
552
+ }
491
553
  }
492
554
  };
493
- const processUpdate = ({ update, grpcRecvUs, grpcRecvNs, blockTimeUs }) => {
555
+ const rememberParsedUpdate = (identity, slot) => {
556
+ if (slot !== undefined && (lastParsedSlot === undefined || slot > lastParsedSlot)) {
557
+ lastParsedSlot = slot;
558
+ }
559
+ if (!replayOnReconnect || identity === undefined || slot === undefined)
560
+ return;
561
+ seenUpdates.set(identity, slot);
562
+ seenOrder.push({ identity, slot });
563
+ if (slot > highestRememberedSlot)
564
+ highestRememberedSlot = slot;
565
+ const retainFrom = highestRememberedSlot > BigInt(replayDedupeSlots)
566
+ ? highestRememberedSlot - BigInt(replayDedupeSlots)
567
+ : 0n;
568
+ while (seenHead < seenOrder.length && seenOrder[seenHead].slot < retainFrom) {
569
+ const stale = seenOrder[seenHead++];
570
+ if (seenUpdates.get(stale.identity) === stale.slot)
571
+ seenUpdates.delete(stale.identity);
572
+ }
573
+ if (seenHead >= 4096 && seenHead * 2 >= seenOrder.length) {
574
+ seenOrder.splice(0, seenHead);
575
+ seenHead = 0;
576
+ }
577
+ };
578
+ const processUpdate = ({ update, grpcRecvUs, grpcRecvNs, blockTimeUs, sourceToGrpcLatencyUs, }) => {
579
+ const identity = updateIdentity(update);
580
+ const slotForReplay = updateSlot(update);
581
+ if (replayOnReconnect && identity !== undefined && seenUpdates.has(identity)) {
582
+ replayedUpdateCount++;
583
+ rememberParsedUpdate(undefined, slotForReplay);
584
+ return;
585
+ }
494
586
  if (order.needsTimer)
495
587
  order.flushDue(emitEvent);
496
588
  const txUpdate = update.transaction;
@@ -513,18 +605,20 @@ class YellowstoneGrpc {
513
605
  eventTypeFilter,
514
606
  });
515
607
  const parseFinishedNs = grpcRecvNs === undefined ? undefined : process.hrtime.bigint();
516
- annotateLocalMetrics(txEvents, grpcRecvNs, parseStartedNs, parseFinishedNs);
608
+ annotateLocalMetrics(txEvents, grpcRecvNs, parseStartedNs, parseFinishedNs, sourceToGrpcLatencyUs);
517
609
  order.pushTransactionEvents(txEvents, fallbackSlot, fallbackTxIndex, emitEvent);
518
610
  }
519
611
  if (update.account) {
520
612
  const parseStartedNs = grpcRecvNs === undefined ? undefined : process.hrtime.bigint();
521
613
  const ev = this.parseAccountEventRaw(update.account, grpcRecvUs, eventTypeFilter, blockTimeUs);
522
614
  const parseFinishedNs = grpcRecvNs === undefined ? undefined : process.hrtime.bigint();
523
- if (ev)
524
- annotateLocalMetrics([ev], grpcRecvNs, parseStartedNs, parseFinishedNs);
615
+ if (ev) {
616
+ annotateLocalMetrics([ev], grpcRecvNs, parseStartedNs, parseFinishedNs, sourceToGrpcLatencyUs);
617
+ }
525
618
  if (ev)
526
619
  emitEvent(ev);
527
620
  }
621
+ rememberParsedUpdate(identity, slotForReplay);
528
622
  };
529
623
  const scheduleIngressDrain = () => {
530
624
  if (isCancelled || ingressDrainScheduled || ingress.len() === 0)
@@ -618,25 +712,39 @@ class YellowstoneGrpc {
618
712
  const autoReconnect = options?.autoReconnect !== false;
619
713
  const maxBackoffMs = 60_000;
620
714
  let backoffMs = this.config.retry_delay_ms;
715
+ let replayAllowed = replayOnReconnect;
716
+ let attempt = 0;
621
717
  try {
622
718
  while (!isCancelled) {
719
+ let attemptedReplay = false;
720
+ let receivedData = false;
623
721
  try {
722
+ attempt++;
723
+ if (attempt > 1)
724
+ reconnectCount++;
624
725
  const stream = await this.client.subscribe();
625
726
  if (isCancelled) {
626
727
  cancelSubscribeStream(stream);
627
728
  break;
628
729
  }
629
730
  streamHolder.current = stream;
731
+ const request = (0, subscribe_builder_js_1.buildSubscribeRequest)(currentTransactionFilters, currentAccountFilters);
732
+ if (attempt > 1 && replayAllowed && lastParsedSlot !== undefined) {
733
+ request.fromSlot = lastParsedSlot.toString();
734
+ attemptedReplay = true;
735
+ }
630
736
  await new Promise((resolve, reject) => {
631
- stream.write((0, subscribe_builder_js_1.buildSubscribeRequest)(currentTransactionFilters, currentAccountFilters), (err) => {
737
+ stream.write(request, (err) => {
632
738
  if (err)
633
739
  reject(err);
634
740
  else
635
741
  resolve();
636
742
  });
637
743
  });
744
+ streamConnected = true;
638
745
  await new Promise((resolve, reject) => {
639
746
  const cleanup = () => {
747
+ streamConnected = false;
640
748
  stream.removeAllListeners();
641
749
  if (streamHolder.current === stream) {
642
750
  streamHolder.current = null;
@@ -645,6 +753,7 @@ class YellowstoneGrpc {
645
753
  stream.on("data", (update) => {
646
754
  if (isCancelled)
647
755
  return;
756
+ receivedData = true;
648
757
  const grpcRecvNs = this.config.enable_metrics
649
758
  ? process.hrtime.bigint()
650
759
  : undefined;
@@ -658,11 +767,13 @@ class YellowstoneGrpc {
658
767
  });
659
768
  return;
660
769
  }
770
+ const createdAtUs = YellowstoneGrpc.timestampToMicros(update.createdAt ?? update.created_at);
661
771
  const accepted = ingress.push({
662
772
  update,
663
773
  grpcRecvUs,
664
774
  grpcRecvNs,
665
- blockTimeUs: YellowstoneGrpc.timestampToMicros(update.createdAt ?? update.created_at),
775
+ blockTimeUs: createdAtUs,
776
+ sourceToGrpcLatencyUs: createdAtUs === undefined ? undefined : Math.max(0, grpcRecvUs - createdAtUs),
666
777
  });
667
778
  if (!accepted) {
668
779
  const now = Date.now();
@@ -676,11 +787,15 @@ class YellowstoneGrpc {
676
787
  scheduleIngressDrain();
677
788
  });
678
789
  stream.on("error", (err) => {
790
+ streamDisconnectCount++;
679
791
  cleanup();
680
792
  reject(err);
681
793
  });
682
794
  stream.on("end", () => {
795
+ streamDisconnectCount++;
683
796
  cleanup();
797
+ if (!isCancelled)
798
+ reportError(new Error("Yellowstone stream ended; reconnecting"));
684
799
  resolve();
685
800
  });
686
801
  });
@@ -690,15 +805,38 @@ class YellowstoneGrpc {
690
805
  break;
691
806
  cancelSubscribeStream(streamHolder.current);
692
807
  streamHolder.current = null;
808
+ streamConnected = false;
693
809
  reportError(err instanceof Error ? err : new Error(String(err)));
694
- if (!autoReconnect)
810
+ await waitForIngressIdle();
811
+ if (!autoReconnect) {
812
+ if (lastParsedSlot !== undefined)
813
+ continuityBreakCount++;
695
814
  break;
815
+ }
816
+ let continuityBreakRecorded = false;
817
+ if (attemptedReplay && !receivedData && replayUnsupported(err)) {
818
+ replayAllowed = false;
819
+ continuityBreakCount++;
820
+ continuityBreakRecorded = true;
821
+ reportError(new Error("Yellowstone provider rejected fromSlot replay; reconnecting live and continuity is not guaranteed"));
822
+ }
823
+ if (!replayAllowed && lastParsedSlot !== undefined && !continuityBreakRecorded) {
824
+ continuityBreakCount++;
825
+ }
696
826
  await waitBeforeReconnect(backoffMs);
697
827
  backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
698
828
  continue;
699
829
  }
700
- if (isCancelled || !autoReconnect)
830
+ await waitForIngressIdle();
831
+ if (isCancelled)
701
832
  break;
833
+ if (!autoReconnect) {
834
+ if (lastParsedSlot !== undefined)
835
+ continuityBreakCount++;
836
+ break;
837
+ }
838
+ if (!replayAllowed && lastParsedSlot !== undefined)
839
+ continuityBreakCount++;
702
840
  await waitBeforeReconnect(backoffMs);
703
841
  backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
704
842
  }
@@ -706,6 +844,7 @@ class YellowstoneGrpc {
706
844
  finally {
707
845
  cancelSubscribeStream(streamHolder.current);
708
846
  streamHolder.current = null;
847
+ streamConnected = false;
709
848
  if (flushTimer)
710
849
  clearInterval(flushTimer);
711
850
  if (!isCancelled) {
@@ -725,6 +864,11 @@ class YellowstoneGrpc {
725
864
  eventDropped,
726
865
  ingressLen: () => ingress.len(),
727
866
  ingressDropped: () => ingress.dropped(),
867
+ isStreamConnected: () => streamConnected,
868
+ streamDisconnects: () => streamDisconnectCount,
869
+ reconnects: () => reconnectCount,
870
+ replayedUpdates: () => replayedUpdateCount,
871
+ continuityBreaks: () => continuityBreakCount,
728
872
  cancel: () => {
729
873
  cancel();
730
874
  this.dexSubscribers.delete(id);
@@ -757,6 +901,7 @@ class YellowstoneGrpc {
757
901
  }
758
902
  /** 获取最新区块哈希 */
759
903
  async getLatestBlockhash(commitment) {
904
+ await this.connect();
760
905
  const resp = await this.client.getLatestBlockhash(commitment);
761
906
  return {
762
907
  slot: Number(resp.slot),
@@ -766,21 +911,25 @@ class YellowstoneGrpc {
766
911
  }
767
912
  /** 获取区块高度 */
768
913
  async getBlockHeight(commitment) {
914
+ await this.connect();
769
915
  const resp = await this.client.getBlockHeight(commitment);
770
- return Number(resp);
916
+ return Number(resp.blockHeight);
771
917
  }
772
918
  /** 获取当前 Slot */
773
919
  async getSlot(commitment) {
920
+ await this.connect();
774
921
  const resp = await this.client.getSlot(commitment);
775
- return Number(resp);
922
+ return Number(resp.slot);
776
923
  }
777
924
  /** 获取服务器版本 */
778
925
  async getVersion() {
926
+ await this.connect();
779
927
  const resp = await this.client.getVersion();
780
- return String(resp);
928
+ return resp.version;
781
929
  }
782
930
  /** 验证区块哈希是否有效 */
783
931
  async isBlockhashValid(blockhash, commitment) {
932
+ await this.connect();
784
933
  const resp = await this.client.isBlockhashValid(blockhash, commitment);
785
934
  return {
786
935
  slot: Number(resp.slot),
@@ -4,8 +4,7 @@
4
4
  * `@triton-one/yellowstone-grpc` 的 `Client` 通过构造函数第三参传入 `ChannelOptions`;
5
5
  * `connectTimeoutMs` 无与 tonic 完全一一对应的项,仅作文档与默认值对齐(可配合环境网络调优)。
6
6
  */
7
- import type { ChannelOptions } from "@grpc/grpc-js";
8
- import Client from "@triton-one/yellowstone-grpc";
7
+ import Client, { type ChannelOptions } from "@triton-one/yellowstone-grpc";
9
8
  /** 与 Rust `GeyserConnectConfig` 字段对应(毫秒 / 字节) */
10
9
  export interface GeyserConnectConfig {
11
10
  /** 对应 Rust `connect_timeout`(毫秒) */
@@ -6,6 +6,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.defaultGeyserConnectConfig = defaultGeyserConnectConfig;
7
7
  exports.geyserGrpcChannelOptions = geyserGrpcChannelOptions;
8
8
  exports.connectYellowstoneGeyser = connectYellowstoneGeyser;
9
+ /**
10
+ * 与 Rust `sol-parser-sdk/src/grpc/geyser_connect.rs` 对齐:Yellowstone Geyser gRPC 连接选项。
11
+ *
12
+ * `@triton-one/yellowstone-grpc` 的 `Client` 通过构造函数第三参传入 `ChannelOptions`;
13
+ * `connectTimeoutMs` 无与 tonic 完全一一对应的项,仅作文档与默认值对齐(可配合环境网络调优)。
14
+ */
9
15
  const yellowstone_grpc_1 = __importDefault(require("@triton-one/yellowstone-grpc"));
10
16
  /** 公共字段与 Rust `GeyserConnectConfig::default` 一致,并包含 Node gRPC 通道默认值。 */
11
17
  function defaultGeyserConnectConfig() {
@@ -26,15 +32,17 @@ function geyserGrpcChannelOptions(config = defaultGeyserConnectConfig()) {
26
32
  const interval = config.keepAliveIntervalMs ?? 30_000;
27
33
  const timeout = config.keepAliveTimeoutMs ?? 5000;
28
34
  return {
29
- "grpc.max_receive_message_length": max,
30
- "grpc.max_send_message_length": max,
31
- "grpc.keepalive_time_ms": interval,
32
- "grpc.keepalive_timeout_ms": timeout,
33
- /** 无活跃 RPC 时仍发 keepalive,避免长时间仅订阅时被中间设备掐断 */
34
- "grpc.keepalive_permit_without_calls": 1,
35
- "grpc.initial_reconnect_backoff_ms": config.initialReconnectBackoffMs,
36
- "grpc.max_reconnect_backoff_ms": config.maxReconnectBackoffMs,
37
- "grpc-node.flow_control_window": config.flowControlWindowBytes,
35
+ grpcConnectTimeout: config.connectTimeoutMs,
36
+ grpcMaxDecodingMessageSize: max,
37
+ grpcMaxEncodingMessageSize: max,
38
+ grpcHttp2KeepAliveInterval: interval,
39
+ grpcKeepAliveTimeout: timeout,
40
+ grpcTcpKeepalive: interval,
41
+ grpcInitialConnectionWindowSize: config.flowControlWindowBytes,
42
+ grpcInitialStreamWindowSize: config.flowControlWindowBytes,
43
+ grpcHttp2AdaptiveWindow: true,
44
+ grpcKeepAliveWhileIdle: true,
45
+ grpcTcpNodelay: true,
38
46
  };
39
47
  }
40
48
  /**
@@ -42,5 +50,7 @@ function geyserGrpcChannelOptions(config = defaultGeyserConnectConfig()) {
42
50
  * Rust 为 async;此处构造同步完成,返回 `Promise` 以保持 `await connectYellowstoneGeyser(...)` 写法。
43
51
  */
44
52
  async function connectYellowstoneGeyser(endpoint, config = defaultGeyserConnectConfig()) {
45
- return new yellowstone_grpc_1.default(endpoint, config.xToken, geyserGrpcChannelOptions(config));
53
+ const client = new yellowstone_grpc_1.default(endpoint, config.xToken, geyserGrpcChannelOptions(config));
54
+ await client.connect();
55
+ return client;
46
56
  }
@@ -1,6 +1,8 @@
1
- import type { Transaction as YTransaction, TransactionStatusMeta } from "@triton-one/yellowstone-grpc/dist/grpc/solana-storage.js";
1
+ import type { SubscribeUpdateTransactionInfo } from "@triton-one/yellowstone-grpc";
2
2
  import { type VersionedTransactionResponse } from "@solana/web3.js";
3
3
  import type { ParseError } from "../core/error.js";
4
+ type YTransaction = NonNullable<SubscribeUpdateTransactionInfo["transaction"]>;
5
+ type TransactionStatusMeta = NonNullable<SubscribeUpdateTransactionInfo["meta"]>;
4
6
  export type ConvertRpcToGrpcOk = {
5
7
  ok: true;
6
8
  meta: TransactionStatusMeta;
@@ -16,3 +18,4 @@ export type ConvertRpcToGrpcErr = {
16
18
  export declare function convertRpcToGrpc(tx: VersionedTransactionResponse): ConvertRpcToGrpcOk | ConvertRpcToGrpcErr;
17
19
  /** Rust 蛇形命名 */
18
20
  export declare const convert_rpc_to_grpc: typeof convertRpcToGrpc;
21
+ export {};
@@ -1,4 +1,7 @@
1
- import type { TokenBalance, Transaction, TransactionStatusMeta } from "@triton-one/yellowstone-grpc/dist/grpc/solana-storage.js";
1
+ import type { SubscribeUpdateTransactionInfo } from "@triton-one/yellowstone-grpc";
2
+ type Transaction = NonNullable<SubscribeUpdateTransactionInfo["transaction"]>;
3
+ type TransactionStatusMeta = NonNullable<SubscribeUpdateTransactionInfo["meta"]>;
4
+ type TokenBalance = TransactionStatusMeta["preTokenBalances"][number];
2
5
  /** 32 字节公钥 → Base58(无效长度返回 `null`) */
3
6
  export declare function pubkeyBytesToBs58(bytes: Uint8Array): string | null;
4
7
  /**
@@ -27,3 +30,4 @@ export declare function splTokenCounterpartyByOwner(meta: TransactionStatusMeta,
27
30
  export declare function collectWatchTransferCounterpartyPairs(tx: Transaction, meta: TransactionStatusMeta, watchedBs58: string[], minNativeOutflowLamports: bigint, splMinWatchDecreaseRaw: bigint): [string, string][] | null;
28
31
  /** Yellowstone 交易签名原始字节(64)→ Base58 签名字符串;长度非 64 返回 `null` */
29
32
  export declare function tryYellowstoneSignature(sig: Uint8Array): string | null;
33
+ export {};
@@ -1,4 +1,7 @@
1
1
  import { type VersionedTransactionResponse } from "@solana/web3.js";
2
- import type { Transaction as YellowstoneTransaction, TransactionStatusMeta as YellowstoneMeta } from "@triton-one/yellowstone-grpc/dist/grpc/solana-storage.js";
2
+ import type { SubscribeUpdateTransactionInfo } from "@triton-one/yellowstone-grpc";
3
+ type YellowstoneTransaction = NonNullable<SubscribeUpdateTransactionInfo["transaction"]>;
4
+ type YellowstoneMeta = NonNullable<SubscribeUpdateTransactionInfo["meta"]>;
3
5
  /** Convert Yellowstone protobuf objects directly, without a WASM/Base58 serialization round trip. */
4
6
  export declare function yellowstoneTransactionToWeb3(transaction: YellowstoneTransaction, meta: YellowstoneMeta, slot: number, blockTime: number | null): VersionedTransactionResponse;
7
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sol-parser-sdk",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "description": "High-performance Solana DEX event parser for Node.js/TypeScript (Yellowstone gRPC, log parsing)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "homepage": "https://github.com/0xfnzero/sol-parser-sdk-nodejs#readme",
22
22
  "engines": {
23
- "node": ">=18"
23
+ "node": ">=20.18.0"
24
24
  },
25
25
  "scripts": {
26
26
  "build": "tsc && mkdir -p dist/shredstream && cp src/shredstream/shredstream.proto dist/shredstream/",
@@ -58,7 +58,7 @@
58
58
  "@grpc/grpc-js": "^1.12.4",
59
59
  "@grpc/proto-loader": "^0.7.13",
60
60
  "@solana/web3.js": "^1.95.4",
61
- "@triton-one/yellowstone-grpc": "^1.0.0",
61
+ "@triton-one/yellowstone-grpc": "7.0.0",
62
62
  "bs58": "^6.0.0"
63
63
  }
64
64
  }