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 +18 -2
- package/README_CN.md +17 -2
- package/dist/core/metadata.d.ts +2 -0
- package/dist/grpc/client.d.ts +11 -0
- package/dist/grpc/client.js +165 -16
- package/dist/grpc/geyser_connect.d.ts +1 -2
- package/dist/grpc/geyser_connect.js +20 -10
- package/dist/grpc/rpc_to_grpc.d.ts +4 -1
- package/dist/grpc/transaction_meta.d.ts +5 -1
- package/dist/grpc/yellowstone_transaction_adapter.d.ts +4 -1
- package/package.json +3 -3
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.
|
|
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.
|
|
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`、
|
package/dist/core/metadata.d.ts
CHANGED
|
@@ -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;
|
package/dist/grpc/client.d.ts
CHANGED
|
@@ -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);
|
package/dist/grpc/client.js
CHANGED
|
@@ -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
|
-
//
|
|
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.
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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.
|
|
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": "
|
|
61
|
+
"@triton-one/yellowstone-grpc": "7.0.0",
|
|
62
62
|
"bs58": "^6.0.0"
|
|
63
63
|
}
|
|
64
64
|
}
|