sol-parser-sdk 0.5.11 → 0.5.13
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 +60 -7
- package/README_CN.md +53 -5
- package/dist/core/metadata.d.ts +6 -0
- package/dist/core/rpc_invoke_map.js +8 -1
- package/dist/grpc/async_event_queue.d.ts +3 -0
- package/dist/grpc/async_event_queue.js +15 -5
- package/dist/grpc/client.d.ts +21 -3
- package/dist/grpc/client.js +270 -36
- package/dist/grpc/program_ids.d.ts +8 -0
- package/dist/grpc/program_ids.js +21 -0
- package/dist/grpc/yellowstone_parse.js +5 -16
- package/dist/grpc/yellowstone_transaction_adapter.d.ts +4 -0
- package/dist/grpc/yellowstone_transaction_adapter.js +101 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -3
- package/dist/rpc_transaction.js +18 -2
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
**From npm**
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
npm install sol-parser-sdk@0.5.
|
|
78
|
+
npm install sol-parser-sdk@0.5.13
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
**From source** (folder may be named `sol-parser-sdk-ts` in a monorepo)
|
|
@@ -150,9 +150,10 @@ console.log("subscribed", sub.id);
|
|
|
150
150
|
### Long-running low-latency DEX subscriptions
|
|
151
151
|
|
|
152
152
|
The public API remains queue-based, matching the Rust SDK's `subscribe_dex_events` shape. The Node
|
|
153
|
-
subscription is an async iterable instead of Rust's `ArrayQueue
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
subscription is an async iterable instead of Rust's `ArrayQueue`. Raw Yellowstone protobuf objects
|
|
154
|
+
are mapped directly into the parser without the former protobuf/WASM/JSON/Base58 transaction
|
|
155
|
+
round-trip. Ring-buffer dequeue, bounded pre-parser scheduling, HTTP/2 receive-window tuning, and
|
|
156
|
+
reconnect behavior are handled inside the SDK:
|
|
156
157
|
|
|
157
158
|
```typescript
|
|
158
159
|
import { YellowstoneGrpc, lowLatencyClientConfig } from "sol-parser-sdk";
|
|
@@ -170,8 +171,59 @@ for await (const event of sub) {
|
|
|
170
171
|
```
|
|
171
172
|
|
|
172
173
|
If using `for await`, the default overflow behavior remains `drop-newest` for compatibility. A
|
|
173
|
-
tip-oriented consumer can choose `queueOverflowStrategy: "drop-oldest"
|
|
174
|
-
`sub.
|
|
174
|
+
tip-oriented consumer can choose `queueOverflowStrategy: "drop-oldest"`. Monitor the public event
|
|
175
|
+
queue with `sub.len()` / `sub.eventDropped()` and the pre-parser queue with
|
|
176
|
+
`sub.ingressLen()` / `sub.ingressDropped()`. `sub.dropped()` is the combined count. Every overflow
|
|
177
|
+
is counted and periodically reported through `sub.errors`.
|
|
178
|
+
|
|
179
|
+
Automatic reconnect resumes from the last successfully parsed slot by default and deduplicates
|
|
180
|
+
replayed transaction/account updates. Monitor transport continuity with `sub.isStreamConnected()`,
|
|
181
|
+
`sub.streamDisconnects()`, `sub.reconnects()`, `sub.replayedUpdates()`, and
|
|
182
|
+
`sub.continuityBreaks()`. A zero local drop count does not describe updates lost before the gRPC
|
|
183
|
+
`data` callback. If a provider rejects Yellowstone `fromSlot`, the SDK reports the failure through
|
|
184
|
+
`sub.errors`, increments `continuityBreaks()`, and reconnects live. Set `replayOnReconnect: false`
|
|
185
|
+
only when duplicate-free live delivery is more important than recovering a disconnect window.
|
|
186
|
+
|
|
187
|
+
For precise local-only measurements, set `config.enable_metrics = true`. Event metadata then includes
|
|
188
|
+
`local_queue_latency_us`, `parse_duration_us`, and `local_processing_latency_us`. These values use
|
|
189
|
+
Node's monotonic high-resolution clock. They measure only local queueing and parsing; the hot path
|
|
190
|
+
does not call `getSlot`, `getTransaction`, JSON-RPC, or any unary RPC method.
|
|
191
|
+
|
|
192
|
+
`local_processing_latency_us` is the end-to-end local metric: it starts at entry to Yellowstone's
|
|
193
|
+
gRPC `data` callback and ends immediately after complete event parsing and field filling.
|
|
194
|
+
|
|
195
|
+
Full-volume PumpFun and PumpSwap can use the same Rust-aligned program filter. Yellowstone applies
|
|
196
|
+
`account_include` as ANY, so this receives every transaction containing either supported program:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const allPumpTransactions = transactionFilterForProtocols(["PumpFun", "PumpSwap"]);
|
|
200
|
+
const sub = await client.subscribeDexEvents([allPumpTransactions], [], eventTypeFilter);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`eventTypeFilter` avoids unnecessary event decoding, but it does not reduce traffic sent by
|
|
204
|
+
Yellowstone. When an application intentionally needs only selected mints, pools, or users, the
|
|
205
|
+
optional scoped filter requires the protocol program and any one tracked account:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const pumpSwap = transactionFilterForProtocolAccounts("PumpSwap", knownMintsAndPools);
|
|
209
|
+
const sub = await client.subscribeDexEvents(
|
|
210
|
+
[transactionFilterForProtocols(["PumpFun"]), pumpSwap],
|
|
211
|
+
[],
|
|
212
|
+
eventTypeFilter
|
|
213
|
+
);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The combined example uses full-volume PumpSwap by default. Set `PUMPSWAP_FILTER_MODE=scoped` only when
|
|
217
|
+
scoped monitoring is desired; in that mode `updateSubscription()` refreshes the active account
|
|
218
|
+
filters. `PumpSwapCreatePool` maps a known base or quote mint to its pool, and
|
|
219
|
+
`PumpSwapLiquidityAdded` can then be compared with the developer associated with that mint.
|
|
220
|
+
|
|
221
|
+
Two `YellowstoneGrpc` instances in one Node process still share one JavaScript event loop. The direct
|
|
222
|
+
protobuf path supports full PumpFun + PumpSwap in one subscription; use a separate OS process or
|
|
223
|
+
Worker Thread only when application callbacks, database work, or additional protocols saturate that
|
|
224
|
+
event loop. A separate gRPC connection alone does not provide CPU isolation. Send database work to a
|
|
225
|
+
bounded handoff queue and batch asynchronous SQLite WAL writes; do not await one write per event in
|
|
226
|
+
the subscription loop.
|
|
175
227
|
|
|
176
228
|
No bounded in-memory stream can guarantee both zero loss and bounded latency when the consumer's
|
|
177
229
|
average processing rate is below the filtered input rate. Keep the loop body CPU-light, avoid
|
|
@@ -205,6 +257,7 @@ From the **package root** after `npm install`. Examples use `npx tsx` and load `
|
|
|
205
257
|
| Debug: print `metaRaw` / log structure | `npx tsx scripts/debug-grpc-ts.ts` | [debug-grpc-ts.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/scripts/debug-grpc-ts.ts) |
|
|
206
258
|
| **PumpFun** | | |
|
|
207
259
|
| CREATE + dev BUY/SELL, long-running low latency | `npx tsx examples/devtrades_low_latency.ts` | [devtrades_low_latency.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/devtrades_low_latency.ts) |
|
|
260
|
+
| PumpFun + full-volume PumpSwap dev trades/liquidity | `npm run example:grpc:devtrades:pumpswap` | [devtrades_pumpfun_pumpswap_low_latency.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/devtrades_pumpfun_pumpswap_low_latency.ts) |
|
|
208
261
|
| Pretty-print full JSON `DexEvent` over gRPC | `npx tsx examples/pumpfun_grpc_json.ts` | [pumpfun_grpc_json.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpfun_grpc_json.ts) |
|
|
209
262
|
| PumpFun events + metrics | `npx tsx examples/pumpfun_with_metrics.ts` | [pumpfun_with_metrics.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpfun_with_metrics.ts) |
|
|
210
263
|
| PumpFun trade filter | `npx tsx examples/pumpfun_trade_filter.ts` | [pumpfun_trade_filter.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpfun_trade_filter.ts) |
|
|
@@ -212,7 +265,7 @@ From the **package root** after `npm install`. Examples use `npx tsx` and load `
|
|
|
212
265
|
| **PumpSwap** | | |
|
|
213
266
|
| Pretty-print full JSON `DexEvent` over gRPC | `npx tsx examples/pumpswap_grpc_json.ts` | [pumpswap_grpc_json.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpswap_grpc_json.ts) |
|
|
214
267
|
| PumpSwap events + metrics | `npx tsx examples/pumpswap_with_metrics.ts` | [pumpswap_with_metrics.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpswap_with_metrics.ts) |
|
|
215
|
-
| PumpSwap ultra-low latency | `
|
|
268
|
+
| PumpSwap swaps, pool creation, and liquidity events (ultra-low latency) | `npm run example:grpc:pumpswap` | [pumpswap_low_latency.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpswap_low_latency.ts) |
|
|
216
269
|
| **Meteora DAMM** | | |
|
|
217
270
|
| Meteora DAMM V2 events | `npx tsx examples/meteora_damm_grpc.ts` | [meteora_damm_grpc.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/meteora_damm_grpc.ts) |
|
|
218
271
|
| **ShredStream** (HTTP, not Yellowstone gRPC; see **step 5** above) | | |
|
package/README_CN.md
CHANGED
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
**npm**
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
npm install sol-parser-sdk@0.5.
|
|
78
|
+
npm install sol-parser-sdk@0.5.13
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
**源码**(monorepo 里目录可能是 `sol-parser-sdk-ts`)
|
|
@@ -150,7 +150,8 @@ console.log("subscribed", sub.id);
|
|
|
150
150
|
### 长时间运行的低延迟 DEX 订阅
|
|
151
151
|
|
|
152
152
|
公开 API 保持队列式调用,与 Rust SDK 的 `subscribe_dex_events` 形态一致。Node 使用异步
|
|
153
|
-
迭代器代替 Rust 的 `ArrayQueue
|
|
153
|
+
迭代器代替 Rust 的 `ArrayQueue`。Yellowstone 原始 protobuf 对象会直接映射给解析器,不再经过
|
|
154
|
+
旧的 protobuf/WASM/JSON/Base58 整笔交易往返;环形队列出队、有界解析前调度、HTTP/2 接收窗口
|
|
154
155
|
和重连优化全部由 SDK 内部完成:
|
|
155
156
|
|
|
156
157
|
```typescript
|
|
@@ -169,8 +170,54 @@ for await (const event of sub) {
|
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
若使用 `for await`,默认溢出策略仍是 `drop-newest`,以保持兼容。更重视贴近链尖的消费者
|
|
172
|
-
可以设置 `queueOverflowStrategy: "drop-oldest"
|
|
173
|
-
|
|
173
|
+
可以设置 `queueOverflowStrategy: "drop-oldest"`。通过 `sub.len()` / `sub.eventDropped()` 监控
|
|
174
|
+
公开事件队列,通过 `sub.ingressLen()` / `sub.ingressDropped()` 监控解析前入口队列;
|
|
175
|
+
`sub.dropped()` 返回两者之和。每次溢出都会计数,并定期通过 `sub.errors` 报告。
|
|
176
|
+
|
|
177
|
+
自动重连默认从最后一个成功解析的 slot 继续订阅,并对回放的 transaction/account update
|
|
178
|
+
去重。可通过 `sub.isStreamConnected()`、`sub.streamDisconnects()`、`sub.reconnects()`、
|
|
179
|
+
`sub.replayedUpdates()` 和 `sub.continuityBreaks()` 监控传输连续性。本地 drop 为零并不能说明
|
|
180
|
+
进入 gRPC `data` 回调之前没有丢数据。如果 provider 拒绝 Yellowstone `fromSlot`,SDK 会通过
|
|
181
|
+
`sub.errors` 明确报告,增加 `continuityBreaks()`,再降级为 live 重连。只有在“绝不接收回放重复”
|
|
182
|
+
比“补偿断线窗口”更重要时,才应设置 `replayOnReconnect: false`。
|
|
183
|
+
|
|
184
|
+
如需精确测量纯本地延迟,请设置 `config.enable_metrics = true`。事件 metadata 将包含
|
|
185
|
+
`local_queue_latency_us`、`parse_duration_us` 和 `local_processing_latency_us`,这些字段使用
|
|
186
|
+
Node 单调高精度时钟,只测量本地排队与解析。热路径不会调用 `getSlot`、`getTransaction`、
|
|
187
|
+
JSON-RPC 或任何 unary RPC 方法。
|
|
188
|
+
|
|
189
|
+
`local_processing_latency_us` 是本地端到端指标:从进入 Yellowstone gRPC `data` 回调开始,
|
|
190
|
+
到完整事件解析和字段填充结束为止。
|
|
191
|
+
|
|
192
|
+
PumpFun 与 PumpSwap 全量交易可以直接使用和 Rust 对齐的 Program ID 过滤器。Yellowstone 的
|
|
193
|
+
`account_include` 是 ANY 语义,因此下面会接收包含任一协议程序的全部交易:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const allPumpTransactions = transactionFilterForProtocols(["PumpFun", "PumpSwap"]);
|
|
197
|
+
const sub = await client.subscribeDexEvents([allPumpTransactions], [], eventTypeFilter);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`eventTypeFilter` 可以减少无关事件解码,但不会减少 Yellowstone 发来的交易。只有应用明确只想
|
|
201
|
+
监控部分 mint、pool 或 user 时,才需要下面的可选限定过滤器;它要求交易同时包含协议程序,
|
|
202
|
+
并包含任意一个已跟踪账户:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const pumpSwap = transactionFilterForProtocolAccounts("PumpSwap", knownMintsAndPools);
|
|
206
|
+
const sub = await client.subscribeDexEvents(
|
|
207
|
+
[transactionFilterForProtocols(["PumpFun"]), pumpSwap],
|
|
208
|
+
[],
|
|
209
|
+
eventTypeFilter
|
|
210
|
+
);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
组合示例默认订阅全量 PumpSwap。只有设置 `PUMPSWAP_FILTER_MODE=scoped` 时才按账户限定;
|
|
214
|
+
该模式会通过 `updateSubscription()` 刷新活跃账户过滤器。`PumpSwapCreatePool` 可把已知 base
|
|
215
|
+
mint 或 quote mint 映射到 pool,再将 `PumpSwapLiquidityAdded.user` 与该 mint 的开发者比较。
|
|
216
|
+
|
|
217
|
+
同一 Node 进程里的两个 `YellowstoneGrpc` 实例仍共享一个 JavaScript 事件循环。直接 protobuf
|
|
218
|
+
路径支持在一条订阅中接收全量 PumpFun + PumpSwap;只有业务回调、数据库写入或更多协议使事件
|
|
219
|
+
循环饱和时,才需要独立进程或 Worker Thread。仅拆成两条 gRPC 连接不能隔离 CPU。数据库写入
|
|
220
|
+
应进入有界交接队列并异步批量写 SQLite WAL,不要在订阅循环中逐事件等待写入。
|
|
174
221
|
|
|
175
222
|
当消费者平均处理速度低于过滤后的入流速度时,任何有界内存流都无法同时保证零丢失和
|
|
176
223
|
有界延迟。消费循环应保持轻量,避免逐事件同步日志,在服务端尽量缩小过滤范围,并把
|
|
@@ -203,6 +250,7 @@ npx tsx examples/shredstream_example.ts -- --url=http://127.0.0.1:10800
|
|
|
203
250
|
| 调试:打印 `metaRaw` / 日志结构 | `npx tsx scripts/debug-grpc-ts.ts` | [debug-grpc-ts.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/scripts/debug-grpc-ts.ts) |
|
|
204
251
|
| **PumpFun** | | |
|
|
205
252
|
| CREATE + dev BUY/SELL 长时间低延迟订阅 | `npx tsx examples/devtrades_low_latency.ts` | [devtrades_low_latency.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/devtrades_low_latency.ts) |
|
|
253
|
+
| PumpFun + 全量 PumpSwap 开发者交易/加池 | `npm run example:grpc:devtrades:pumpswap` | [devtrades_pumpfun_pumpswap_low_latency.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/devtrades_pumpfun_pumpswap_low_latency.ts) |
|
|
206
254
|
| gRPC 输出完整 JSON `DexEvent` | `npx tsx examples/pumpfun_grpc_json.ts` | [pumpfun_grpc_json.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpfun_grpc_json.ts) |
|
|
207
255
|
| PumpFun 事件 + 性能指标 | `npx tsx examples/pumpfun_with_metrics.ts` | [pumpfun_with_metrics.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpfun_with_metrics.ts) |
|
|
208
256
|
| PumpFun 交易类型过滤 | `npx tsx examples/pumpfun_trade_filter.ts` | [pumpfun_trade_filter.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpfun_trade_filter.ts) |
|
|
@@ -210,7 +258,7 @@ npx tsx examples/shredstream_example.ts -- --url=http://127.0.0.1:10800
|
|
|
210
258
|
| **PumpSwap** | | |
|
|
211
259
|
| gRPC 输出完整 JSON `DexEvent` | `npx tsx examples/pumpswap_grpc_json.ts` | [pumpswap_grpc_json.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpswap_grpc_json.ts) |
|
|
212
260
|
| PumpSwap 事件 + 性能指标 | `npx tsx examples/pumpswap_with_metrics.ts` | [pumpswap_with_metrics.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpswap_with_metrics.ts) |
|
|
213
|
-
| PumpSwap
|
|
261
|
+
| PumpSwap 交易、创建池及流动性事件(超低延迟) | `npm run example:grpc:pumpswap` | [pumpswap_low_latency.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/pumpswap_low_latency.ts) |
|
|
214
262
|
| **Meteora DAMM** | | |
|
|
215
263
|
| Meteora DAMM V2 事件 | `npx tsx examples/meteora_damm_grpc.ts` | [meteora_damm_grpc.ts](https://github.com/0xfnzero/sol-parser-sdk-nodejs/blob/main/examples/meteora_damm_grpc.ts) |
|
|
216
264
|
| **ShredStream**(HTTP,**非** Yellowstone gRPC;端点见上文步骤 5) | | |
|
package/dist/core/metadata.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export interface EventMetadata {
|
|
|
6
6
|
tx_index: number;
|
|
7
7
|
block_time_us: number;
|
|
8
8
|
grpc_recv_us: number;
|
|
9
|
+
/** Time waiting in the local pre-parser queue, measured with a monotonic clock. */
|
|
10
|
+
local_queue_latency_us?: number;
|
|
11
|
+
/** Pure local adapter + parser duration. No network or RPC calls are included. */
|
|
12
|
+
parse_duration_us?: number;
|
|
13
|
+
/** Start of the local gRPC data callback through parser completion. No RPC calls are included. */
|
|
14
|
+
local_processing_latency_us?: number;
|
|
9
15
|
recent_blockhash?: string;
|
|
10
16
|
}
|
|
11
17
|
export declare function makeMetadata(signature: string, slot: number, txIndex: number, blockTimeUs: number | undefined, grpcRecvUs: number, recentBlockhash?: string): EventMetadata;
|
|
@@ -19,6 +19,7 @@ exports.getInstructionDataBytes = getInstructionDataBytes;
|
|
|
19
19
|
*/
|
|
20
20
|
const bs58_1 = __importDefault(require("bs58"));
|
|
21
21
|
const web3_js_1 = require("@solana/web3.js");
|
|
22
|
+
const accountKeyBase58Cache = new WeakMap();
|
|
22
23
|
/**
|
|
23
24
|
* 将账户表项转为 Base58(与 `PublicKey.toBase58()` 一致)。
|
|
24
25
|
* 兼容:已是地址字符串、标准 `PublicKey`、以及多份 `@solana/web3.js` 副本导致 `instanceof` 失效的情况。
|
|
@@ -37,7 +38,13 @@ function accountKeyToBase58(account) {
|
|
|
37
38
|
const maybeFn = account.toBase58;
|
|
38
39
|
if (typeof maybeFn === "function") {
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
+
const object = account;
|
|
42
|
+
const cached = accountKeyBase58Cache.get(object);
|
|
43
|
+
if (cached !== undefined)
|
|
44
|
+
return cached;
|
|
45
|
+
const encoded = maybeFn.call(account);
|
|
46
|
+
accountKeyBase58Cache.set(object, encoded);
|
|
47
|
+
return encoded;
|
|
41
48
|
}
|
|
42
49
|
catch {
|
|
43
50
|
/* fall through */
|
|
@@ -13,6 +13,9 @@ export declare class AsyncEventQueue<T> implements AsyncIterable<T>, AsyncIterat
|
|
|
13
13
|
push(item: T): boolean;
|
|
14
14
|
len(): number;
|
|
15
15
|
dropped(): number;
|
|
16
|
+
/** Synchronous O(1) dequeue for internal scheduled consumers. */
|
|
17
|
+
shift(): T | undefined;
|
|
18
|
+
clear(): void;
|
|
16
19
|
close(): void;
|
|
17
20
|
next(): Promise<IteratorResult<T>>;
|
|
18
21
|
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
@@ -45,6 +45,20 @@ class AsyncEventQueue {
|
|
|
45
45
|
dropped() {
|
|
46
46
|
return this.droppedCount;
|
|
47
47
|
}
|
|
48
|
+
/** Synchronous O(1) dequeue for internal scheduled consumers. */
|
|
49
|
+
shift() {
|
|
50
|
+
if (this.size === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
const item = this.items[this.head];
|
|
53
|
+
this.items[this.head] = undefined;
|
|
54
|
+
this.head = (this.head + 1) % this.maxSize;
|
|
55
|
+
this.size--;
|
|
56
|
+
return item;
|
|
57
|
+
}
|
|
58
|
+
clear() {
|
|
59
|
+
while (this.size > 0)
|
|
60
|
+
this.shift();
|
|
61
|
+
}
|
|
48
62
|
close() {
|
|
49
63
|
if (this.closed)
|
|
50
64
|
return;
|
|
@@ -55,11 +69,7 @@ class AsyncEventQueue {
|
|
|
55
69
|
}
|
|
56
70
|
next() {
|
|
57
71
|
if (this.size > 0) {
|
|
58
|
-
|
|
59
|
-
this.items[this.head] = undefined;
|
|
60
|
-
this.head = (this.head + 1) % this.maxSize;
|
|
61
|
-
this.size--;
|
|
62
|
-
return Promise.resolve({ value: item, done: false });
|
|
72
|
+
return Promise.resolve({ value: this.shift(), done: false });
|
|
63
73
|
}
|
|
64
74
|
if (this.closed) {
|
|
65
75
|
return Promise.resolve({ value: undefined, done: true });
|
package/dist/grpc/client.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Yellowstone gRPC 客户端实现 - 基于 @triton-one/yellowstone-grpc
|
|
3
3
|
*/
|
|
4
4
|
import { CommitmentLevel } from "@triton-one/yellowstone-grpc";
|
|
5
|
-
import type
|
|
5
|
+
import { type DexEvent } from "../core/dex_event.js";
|
|
6
6
|
import { type EventQueueOverflowStrategy } from "./async_event_queue.js";
|
|
7
7
|
import { type AccountFilter, type ClientConfig, type EventTypeFilter, type TransactionFilter, type SubscribeCallbacks } from "./types.js";
|
|
8
8
|
export { SubscribeCallbacks };
|
|
@@ -13,15 +13,33 @@ export interface DexEventSubscription extends AsyncIterable<DexEvent> {
|
|
|
13
13
|
cancel(): void;
|
|
14
14
|
next(): Promise<IteratorResult<DexEvent>>;
|
|
15
15
|
len(): number;
|
|
16
|
+
/** Total drops across ingress and the public event queue. */
|
|
16
17
|
dropped(): number;
|
|
18
|
+
eventDropped(): number;
|
|
19
|
+
ingressLen(): number;
|
|
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;
|
|
17
27
|
}
|
|
18
28
|
export interface SubscribeDexEventsOptions {
|
|
19
29
|
autoReconnect?: boolean;
|
|
20
30
|
/**
|
|
21
|
-
* Queue behavior when
|
|
22
|
-
* preserves
|
|
31
|
+
* Queue behavior when parsing or the async-iterator consumer cannot keep up.
|
|
32
|
+
* The default preserves existing behavior; `drop-oldest` favors chain-tip latency.
|
|
23
33
|
*/
|
|
24
34
|
queueOverflowStrategy?: EventQueueOverflowStrategy;
|
|
35
|
+
/** Raw Yellowstone updates retained before CPU-heavy parsing. Defaults to min(buffer_size, 1024). */
|
|
36
|
+
ingressBufferSize?: number;
|
|
37
|
+
/** Maximum raw updates parsed before yielding back to Node's event loop. Defaults to 8. */
|
|
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;
|
|
25
43
|
}
|
|
26
44
|
/** Yellowstone gRPC 客户端包装器 */
|
|
27
45
|
export declare class YellowstoneGrpc {
|
package/dist/grpc/client.js
CHANGED
|
@@ -42,6 +42,8 @@ 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");
|
|
46
|
+
const dex_event_js_1 = require("../core/dex_event.js");
|
|
45
47
|
const metadata_js_1 = require("../core/metadata.js");
|
|
46
48
|
const mod_js_1 = require("../accounts/mod.js");
|
|
47
49
|
const geyser_connect_js_1 = require("./geyser_connect.js");
|
|
@@ -58,6 +60,42 @@ function cancelSubscribeStream(stream) {
|
|
|
58
60
|
stream.once("error", () => { });
|
|
59
61
|
stream.cancel();
|
|
60
62
|
}
|
|
63
|
+
function positiveIntegerOption(value, fallback, name) {
|
|
64
|
+
const normalized = value ?? fallback;
|
|
65
|
+
if (!Number.isInteger(normalized) || normalized < 1) {
|
|
66
|
+
throw new RangeError(`${name} must be a positive integer`);
|
|
67
|
+
}
|
|
68
|
+
return normalized;
|
|
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
|
+
}
|
|
61
99
|
/** Yellowstone gRPC 客户端包装器 */
|
|
62
100
|
class YellowstoneGrpc {
|
|
63
101
|
client;
|
|
@@ -430,18 +468,35 @@ class YellowstoneGrpc {
|
|
|
430
468
|
await this.connect();
|
|
431
469
|
const id = `dex_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
432
470
|
const events = new async_event_queue_js_1.AsyncEventQueue(Math.max(1, this.config.buffer_size), options?.queueOverflowStrategy);
|
|
471
|
+
const ingressBufferSize = positiveIntegerOption(options?.ingressBufferSize, Math.min(this.config.buffer_size, 1024), "ingressBufferSize");
|
|
472
|
+
const parserBatchSize = positiveIntegerOption(options?.parserBatchSize, 8, "parserBatchSize");
|
|
473
|
+
const replayDedupeSlots = positiveIntegerOption(options?.replayDedupeSlots, 128, "replayDedupeSlots");
|
|
474
|
+
const replayOnReconnect = options?.replayOnReconnect !== false;
|
|
475
|
+
const ingress = new async_event_queue_js_1.AsyncEventQueue(ingressBufferSize, options?.queueOverflowStrategy);
|
|
433
476
|
const errors = new async_event_queue_js_1.AsyncEventQueue(Math.max(1, this.config.buffer_size));
|
|
434
477
|
const order = new order_buffer_js_1.OrderDispatcher(this.config);
|
|
435
|
-
|
|
478
|
+
const eventDropped = events.dropped.bind(events);
|
|
479
|
+
let nextEventDropWarningAt = 0;
|
|
480
|
+
let nextIngressDropWarningAt = 0;
|
|
481
|
+
let streamConnected = false;
|
|
482
|
+
let streamDisconnectCount = 0;
|
|
483
|
+
let reconnectCount = 0;
|
|
484
|
+
let replayedUpdateCount = 0;
|
|
485
|
+
let continuityBreakCount = 0;
|
|
486
|
+
let lastParsedSlot;
|
|
487
|
+
let highestRememberedSlot = 0n;
|
|
488
|
+
let seenHead = 0;
|
|
489
|
+
const seenUpdates = new Map();
|
|
490
|
+
const seenOrder = [];
|
|
436
491
|
const reportError = (error) => errors.push(error);
|
|
437
492
|
const emitEvent = (event) => {
|
|
438
493
|
if (events.push(event))
|
|
439
494
|
return;
|
|
440
495
|
const now = Date.now();
|
|
441
|
-
if (now <
|
|
496
|
+
if (now < nextEventDropWarningAt)
|
|
442
497
|
return;
|
|
443
|
-
|
|
444
|
-
reportError(new Error(`Dex event queue full; dropped=${
|
|
498
|
+
nextEventDropWarningAt = now + 10_000;
|
|
499
|
+
reportError(new Error(`Dex event queue full; dropped=${eventDropped()} buffer_size=${this.config.buffer_size} ` +
|
|
445
500
|
`strategy=${options?.queueOverflowStrategy ?? "drop-newest"}`));
|
|
446
501
|
};
|
|
447
502
|
let flushTimer;
|
|
@@ -452,6 +507,133 @@ class YellowstoneGrpc {
|
|
|
452
507
|
flushTimer = setInterval(() => order.flushDue(emitEvent), intervalMs);
|
|
453
508
|
}
|
|
454
509
|
let isCancelled = false;
|
|
510
|
+
let ingressDrainScheduled = false;
|
|
511
|
+
let ingressDrainHandle;
|
|
512
|
+
const ingressIdleWaiters = [];
|
|
513
|
+
const resolveIngressIdle = () => {
|
|
514
|
+
if (ingress.len() > 0 || ingressDrainScheduled)
|
|
515
|
+
return;
|
|
516
|
+
for (const resolve of ingressIdleWaiters.splice(0))
|
|
517
|
+
resolve();
|
|
518
|
+
};
|
|
519
|
+
const annotateLocalMetrics = (events, grpcRecvNs, parseStartedNs, parseFinishedNs) => {
|
|
520
|
+
if (grpcRecvNs === undefined || parseStartedNs === undefined || parseFinishedNs === undefined) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const queueUs = Number(parseStartedNs - grpcRecvNs) / 1_000;
|
|
524
|
+
const parseUs = Number(parseFinishedNs - parseStartedNs) / 1_000;
|
|
525
|
+
const processingUs = Number(parseFinishedNs - grpcRecvNs) / 1_000;
|
|
526
|
+
for (const event of events) {
|
|
527
|
+
const metadata = (0, dex_event_js_1.metadataForDexEvent)(event);
|
|
528
|
+
if (!metadata)
|
|
529
|
+
continue;
|
|
530
|
+
metadata.local_queue_latency_us = queueUs;
|
|
531
|
+
metadata.parse_duration_us = parseUs;
|
|
532
|
+
metadata.local_processing_latency_us = processingUs;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
const rememberParsedUpdate = (identity, slot) => {
|
|
536
|
+
if (slot !== undefined && (lastParsedSlot === undefined || slot > lastParsedSlot)) {
|
|
537
|
+
lastParsedSlot = slot;
|
|
538
|
+
}
|
|
539
|
+
if (!replayOnReconnect || identity === undefined || slot === undefined)
|
|
540
|
+
return;
|
|
541
|
+
seenUpdates.set(identity, slot);
|
|
542
|
+
seenOrder.push({ identity, slot });
|
|
543
|
+
if (slot > highestRememberedSlot)
|
|
544
|
+
highestRememberedSlot = slot;
|
|
545
|
+
const retainFrom = highestRememberedSlot > BigInt(replayDedupeSlots)
|
|
546
|
+
? highestRememberedSlot - BigInt(replayDedupeSlots)
|
|
547
|
+
: 0n;
|
|
548
|
+
while (seenHead < seenOrder.length && seenOrder[seenHead].slot < retainFrom) {
|
|
549
|
+
const stale = seenOrder[seenHead++];
|
|
550
|
+
if (seenUpdates.get(stale.identity) === stale.slot)
|
|
551
|
+
seenUpdates.delete(stale.identity);
|
|
552
|
+
}
|
|
553
|
+
if (seenHead >= 4096 && seenHead * 2 >= seenOrder.length) {
|
|
554
|
+
seenOrder.splice(0, seenHead);
|
|
555
|
+
seenHead = 0;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const processUpdate = ({ update, grpcRecvUs, grpcRecvNs, blockTimeUs }) => {
|
|
559
|
+
const identity = updateIdentity(update);
|
|
560
|
+
const slotForReplay = updateSlot(update);
|
|
561
|
+
if (replayOnReconnect && identity !== undefined && seenUpdates.has(identity)) {
|
|
562
|
+
replayedUpdateCount++;
|
|
563
|
+
rememberParsedUpdate(undefined, slotForReplay);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (order.needsTimer)
|
|
567
|
+
order.flushDue(emitEvent);
|
|
568
|
+
const txUpdate = update.transaction;
|
|
569
|
+
const tx = txUpdate?.transaction;
|
|
570
|
+
if (tx) {
|
|
571
|
+
const parseStartedNs = grpcRecvNs === undefined ? undefined : process.hrtime.bigint();
|
|
572
|
+
const slot = txUpdate?.slot ?? 0n;
|
|
573
|
+
const fallbackSlot = YellowstoneGrpc.protobufNumber(slot);
|
|
574
|
+
const fallbackTxIndex = YellowstoneGrpc.protobufNumber(tx.index ?? 0);
|
|
575
|
+
const txInfo = {
|
|
576
|
+
signature: tx.signature,
|
|
577
|
+
isVote: tx.isVote,
|
|
578
|
+
transactionRaw: tx.transaction,
|
|
579
|
+
metaRaw: tx.meta,
|
|
580
|
+
index: (tx.index ?? 0n),
|
|
581
|
+
};
|
|
582
|
+
const txEvents = (0, yellowstone_parse_js_1.parseDexEventsFromGrpcTransactionInfo)(txInfo, slot, {
|
|
583
|
+
blockTimeUs,
|
|
584
|
+
grpcRecvUs,
|
|
585
|
+
eventTypeFilter,
|
|
586
|
+
});
|
|
587
|
+
const parseFinishedNs = grpcRecvNs === undefined ? undefined : process.hrtime.bigint();
|
|
588
|
+
annotateLocalMetrics(txEvents, grpcRecvNs, parseStartedNs, parseFinishedNs);
|
|
589
|
+
order.pushTransactionEvents(txEvents, fallbackSlot, fallbackTxIndex, emitEvent);
|
|
590
|
+
}
|
|
591
|
+
if (update.account) {
|
|
592
|
+
const parseStartedNs = grpcRecvNs === undefined ? undefined : process.hrtime.bigint();
|
|
593
|
+
const ev = this.parseAccountEventRaw(update.account, grpcRecvUs, eventTypeFilter, blockTimeUs);
|
|
594
|
+
const parseFinishedNs = grpcRecvNs === undefined ? undefined : process.hrtime.bigint();
|
|
595
|
+
if (ev)
|
|
596
|
+
annotateLocalMetrics([ev], grpcRecvNs, parseStartedNs, parseFinishedNs);
|
|
597
|
+
if (ev)
|
|
598
|
+
emitEvent(ev);
|
|
599
|
+
}
|
|
600
|
+
rememberParsedUpdate(identity, slotForReplay);
|
|
601
|
+
};
|
|
602
|
+
const scheduleIngressDrain = () => {
|
|
603
|
+
if (isCancelled || ingressDrainScheduled || ingress.len() === 0)
|
|
604
|
+
return;
|
|
605
|
+
ingressDrainScheduled = true;
|
|
606
|
+
ingressDrainHandle = setImmediate(() => {
|
|
607
|
+
ingressDrainHandle = undefined;
|
|
608
|
+
ingressDrainScheduled = false;
|
|
609
|
+
if (isCancelled) {
|
|
610
|
+
resolveIngressIdle();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
let processed = 0;
|
|
614
|
+
while (processed < parserBatchSize) {
|
|
615
|
+
const pending = ingress.shift();
|
|
616
|
+
if (!pending)
|
|
617
|
+
break;
|
|
618
|
+
try {
|
|
619
|
+
processUpdate(pending);
|
|
620
|
+
}
|
|
621
|
+
catch (err) {
|
|
622
|
+
reportError(err instanceof Error ? err : new Error(String(err)));
|
|
623
|
+
}
|
|
624
|
+
processed++;
|
|
625
|
+
}
|
|
626
|
+
if (ingress.len() > 0)
|
|
627
|
+
scheduleIngressDrain();
|
|
628
|
+
else
|
|
629
|
+
resolveIngressIdle();
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
const waitForIngressIdle = () => {
|
|
633
|
+
if (ingress.len() === 0 && !ingressDrainScheduled)
|
|
634
|
+
return Promise.resolve();
|
|
635
|
+
return new Promise((resolve) => ingressIdleWaiters.push(resolve));
|
|
636
|
+
};
|
|
455
637
|
let currentTransactionFilters = transactionFilters;
|
|
456
638
|
let currentAccountFilters = accountFilters;
|
|
457
639
|
const streamHolder = { current: null };
|
|
@@ -477,6 +659,13 @@ class YellowstoneGrpc {
|
|
|
477
659
|
cancelReconnectDelay?.();
|
|
478
660
|
cancelSubscribeStream(streamHolder.current);
|
|
479
661
|
streamHolder.current = null;
|
|
662
|
+
if (ingressDrainHandle)
|
|
663
|
+
clearImmediate(ingressDrainHandle);
|
|
664
|
+
ingressDrainHandle = undefined;
|
|
665
|
+
ingressDrainScheduled = false;
|
|
666
|
+
ingress.clear();
|
|
667
|
+
ingress.close();
|
|
668
|
+
resolveIngressIdle();
|
|
480
669
|
if (flushTimer)
|
|
481
670
|
clearInterval(flushTimer);
|
|
482
671
|
events.close();
|
|
@@ -502,25 +691,39 @@ class YellowstoneGrpc {
|
|
|
502
691
|
const autoReconnect = options?.autoReconnect !== false;
|
|
503
692
|
const maxBackoffMs = 60_000;
|
|
504
693
|
let backoffMs = this.config.retry_delay_ms;
|
|
694
|
+
let replayAllowed = replayOnReconnect;
|
|
695
|
+
let attempt = 0;
|
|
505
696
|
try {
|
|
506
697
|
while (!isCancelled) {
|
|
698
|
+
let attemptedReplay = false;
|
|
699
|
+
let receivedData = false;
|
|
507
700
|
try {
|
|
701
|
+
attempt++;
|
|
702
|
+
if (attempt > 1)
|
|
703
|
+
reconnectCount++;
|
|
508
704
|
const stream = await this.client.subscribe();
|
|
509
705
|
if (isCancelled) {
|
|
510
706
|
cancelSubscribeStream(stream);
|
|
511
707
|
break;
|
|
512
708
|
}
|
|
513
709
|
streamHolder.current = stream;
|
|
710
|
+
const request = (0, subscribe_builder_js_1.buildSubscribeRequest)(currentTransactionFilters, currentAccountFilters);
|
|
711
|
+
if (attempt > 1 && replayAllowed && lastParsedSlot !== undefined) {
|
|
712
|
+
request.fromSlot = lastParsedSlot.toString();
|
|
713
|
+
attemptedReplay = true;
|
|
714
|
+
}
|
|
514
715
|
await new Promise((resolve, reject) => {
|
|
515
|
-
stream.write(
|
|
716
|
+
stream.write(request, (err) => {
|
|
516
717
|
if (err)
|
|
517
718
|
reject(err);
|
|
518
719
|
else
|
|
519
720
|
resolve();
|
|
520
721
|
});
|
|
521
722
|
});
|
|
723
|
+
streamConnected = true;
|
|
522
724
|
await new Promise((resolve, reject) => {
|
|
523
725
|
const cleanup = () => {
|
|
726
|
+
streamConnected = false;
|
|
524
727
|
stream.removeAllListeners();
|
|
525
728
|
if (streamHolder.current === stream) {
|
|
526
729
|
streamHolder.current = null;
|
|
@@ -529,9 +732,12 @@ class YellowstoneGrpc {
|
|
|
529
732
|
stream.on("data", (update) => {
|
|
530
733
|
if (isCancelled)
|
|
531
734
|
return;
|
|
735
|
+
receivedData = true;
|
|
736
|
+
const grpcRecvNs = this.config.enable_metrics
|
|
737
|
+
? process.hrtime.bigint()
|
|
738
|
+
: undefined;
|
|
739
|
+
const grpcRecvUs = Math.floor(Date.now() * 1000);
|
|
532
740
|
backoffMs = this.config.retry_delay_ms;
|
|
533
|
-
if (order.needsTimer)
|
|
534
|
-
order.flushDue(emitEvent);
|
|
535
741
|
if (update.ping) {
|
|
536
742
|
stream.write(this.subscribePingPongRequest(), (werr) => {
|
|
537
743
|
if (werr && !isCancelled) {
|
|
@@ -540,41 +746,33 @@ class YellowstoneGrpc {
|
|
|
540
746
|
});
|
|
541
747
|
return;
|
|
542
748
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
metaRaw: tx.meta,
|
|
557
|
-
index: (tx.index ?? 0n),
|
|
558
|
-
};
|
|
559
|
-
const txEvents = (0, yellowstone_parse_js_1.parseDexEventsFromGrpcTransactionInfo)(txInfo, slot, { blockTimeUs, grpcRecvUs, eventTypeFilter });
|
|
560
|
-
order.pushTransactionEvents(txEvents, fallbackSlot, fallbackTxIndex, emitEvent);
|
|
561
|
-
}
|
|
562
|
-
if (update.account) {
|
|
563
|
-
const ev = this.parseAccountEventRaw(update.account, grpcRecvUs, eventTypeFilter, blockTimeUs);
|
|
564
|
-
if (ev)
|
|
565
|
-
emitEvent(ev);
|
|
749
|
+
const accepted = ingress.push({
|
|
750
|
+
update,
|
|
751
|
+
grpcRecvUs,
|
|
752
|
+
grpcRecvNs,
|
|
753
|
+
blockTimeUs: YellowstoneGrpc.timestampToMicros(update.createdAt ?? update.created_at),
|
|
754
|
+
});
|
|
755
|
+
if (!accepted) {
|
|
756
|
+
const now = Date.now();
|
|
757
|
+
if (now >= nextIngressDropWarningAt) {
|
|
758
|
+
nextIngressDropWarningAt = now + 10_000;
|
|
759
|
+
reportError(new Error(`Yellowstone ingress queue full; dropped=${ingress.dropped()} ` +
|
|
760
|
+
`buffer_size=${ingressBufferSize} ` +
|
|
761
|
+
`strategy=${options?.queueOverflowStrategy ?? "drop-newest"}`));
|
|
566
762
|
}
|
|
567
763
|
}
|
|
568
|
-
|
|
569
|
-
reportError(err instanceof Error ? err : new Error(String(err)));
|
|
570
|
-
}
|
|
764
|
+
scheduleIngressDrain();
|
|
571
765
|
});
|
|
572
766
|
stream.on("error", (err) => {
|
|
767
|
+
streamDisconnectCount++;
|
|
573
768
|
cleanup();
|
|
574
769
|
reject(err);
|
|
575
770
|
});
|
|
576
771
|
stream.on("end", () => {
|
|
772
|
+
streamDisconnectCount++;
|
|
577
773
|
cleanup();
|
|
774
|
+
if (!isCancelled)
|
|
775
|
+
reportError(new Error("Yellowstone stream ended; reconnecting"));
|
|
578
776
|
resolve();
|
|
579
777
|
});
|
|
580
778
|
});
|
|
@@ -584,15 +782,38 @@ class YellowstoneGrpc {
|
|
|
584
782
|
break;
|
|
585
783
|
cancelSubscribeStream(streamHolder.current);
|
|
586
784
|
streamHolder.current = null;
|
|
785
|
+
streamConnected = false;
|
|
587
786
|
reportError(err instanceof Error ? err : new Error(String(err)));
|
|
588
|
-
|
|
787
|
+
await waitForIngressIdle();
|
|
788
|
+
if (!autoReconnect) {
|
|
789
|
+
if (lastParsedSlot !== undefined)
|
|
790
|
+
continuityBreakCount++;
|
|
589
791
|
break;
|
|
792
|
+
}
|
|
793
|
+
let continuityBreakRecorded = false;
|
|
794
|
+
if (attemptedReplay && !receivedData && replayUnsupported(err)) {
|
|
795
|
+
replayAllowed = false;
|
|
796
|
+
continuityBreakCount++;
|
|
797
|
+
continuityBreakRecorded = true;
|
|
798
|
+
reportError(new Error("Yellowstone provider rejected fromSlot replay; reconnecting live and continuity is not guaranteed"));
|
|
799
|
+
}
|
|
800
|
+
if (!replayAllowed && lastParsedSlot !== undefined && !continuityBreakRecorded) {
|
|
801
|
+
continuityBreakCount++;
|
|
802
|
+
}
|
|
590
803
|
await waitBeforeReconnect(backoffMs);
|
|
591
804
|
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
592
805
|
continue;
|
|
593
806
|
}
|
|
594
|
-
|
|
807
|
+
await waitForIngressIdle();
|
|
808
|
+
if (isCancelled)
|
|
809
|
+
break;
|
|
810
|
+
if (!autoReconnect) {
|
|
811
|
+
if (lastParsedSlot !== undefined)
|
|
812
|
+
continuityBreakCount++;
|
|
595
813
|
break;
|
|
814
|
+
}
|
|
815
|
+
if (!replayAllowed && lastParsedSlot !== undefined)
|
|
816
|
+
continuityBreakCount++;
|
|
596
817
|
await waitBeforeReconnect(backoffMs);
|
|
597
818
|
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
598
819
|
}
|
|
@@ -600,10 +821,14 @@ class YellowstoneGrpc {
|
|
|
600
821
|
finally {
|
|
601
822
|
cancelSubscribeStream(streamHolder.current);
|
|
602
823
|
streamHolder.current = null;
|
|
824
|
+
streamConnected = false;
|
|
603
825
|
if (flushTimer)
|
|
604
826
|
clearInterval(flushTimer);
|
|
605
|
-
if (!isCancelled)
|
|
827
|
+
if (!isCancelled) {
|
|
828
|
+
await waitForIngressIdle();
|
|
606
829
|
order.flushAll(emitEvent);
|
|
830
|
+
}
|
|
831
|
+
ingress.close();
|
|
607
832
|
events.close();
|
|
608
833
|
errors.close();
|
|
609
834
|
this.dexSubscribers.delete(id);
|
|
@@ -612,6 +837,15 @@ class YellowstoneGrpc {
|
|
|
612
837
|
return Object.assign(events, {
|
|
613
838
|
id,
|
|
614
839
|
errors,
|
|
840
|
+
dropped: () => eventDropped() + ingress.dropped(),
|
|
841
|
+
eventDropped,
|
|
842
|
+
ingressLen: () => ingress.len(),
|
|
843
|
+
ingressDropped: () => ingress.dropped(),
|
|
844
|
+
isStreamConnected: () => streamConnected,
|
|
845
|
+
streamDisconnects: () => streamDisconnectCount,
|
|
846
|
+
reconnects: () => reconnectCount,
|
|
847
|
+
replayedUpdates: () => replayedUpdateCount,
|
|
848
|
+
continuityBreaks: () => continuityBreakCount,
|
|
615
849
|
cancel: () => {
|
|
616
850
|
cancel();
|
|
617
851
|
this.dexSubscribers.delete(id);
|
|
@@ -23,5 +23,13 @@ export declare const PROTOCOL_PROGRAM_IDS: Record<Protocol, readonly string[]>;
|
|
|
23
23
|
export declare function getProgramIdsForProtocols(protocols: readonly Protocol[]): string[];
|
|
24
24
|
/** 与 Rust `TransactionFilter::for_protocols` 一致 */
|
|
25
25
|
export declare function transactionFilterForProtocols(protocols: readonly Protocol[]): TransactionFilter;
|
|
26
|
+
/**
|
|
27
|
+
* Server-side transaction filter for one protocol restricted to known accounts.
|
|
28
|
+
*
|
|
29
|
+
* Yellowstone applies `account_include` as ANY and `account_required` as ALL,
|
|
30
|
+
* so matching transactions must contain the protocol program and at least one
|
|
31
|
+
* tracked mint, pool, or other account.
|
|
32
|
+
*/
|
|
33
|
+
export declare function transactionFilterForProtocolAccounts(protocol: Protocol, accounts: readonly string[]): TransactionFilter;
|
|
26
34
|
/** 与 Rust `AccountFilter::for_protocols` 一致 */
|
|
27
35
|
export declare function accountFilterForProtocols(protocols: readonly Protocol[]): AccountFilter;
|
package/dist/grpc/program_ids.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PROTOCOL_PROGRAM_IDS = exports.METEORA_DBC_PROGRAM_ID = exports.METEORA_DLMM_PROGRAM_ID = exports.METEORA_DAMM_V2_PROGRAM_ID = exports.METEORA_POOLS_PROGRAM_ID = exports.ORCA_WHIRLPOOL_PROGRAM_ID = exports.RAYDIUM_AMM_V4_PROGRAM_ID = exports.RAYDIUM_CLMM_PROGRAM_ID = exports.RAYDIUM_CPMM_PROGRAM_ID = exports.RAYDIUM_LAUNCHLAB_PROGRAM_ID = exports.PUMP_FEES_PROGRAM_ID = exports.PUMPSWAP_FEES_PROGRAM_ID = exports.PUMPSWAP_PROGRAM_ID = exports.PUMPFUN_PROGRAM_ID = void 0;
|
|
4
4
|
exports.getProgramIdsForProtocols = getProgramIdsForProtocols;
|
|
5
5
|
exports.transactionFilterForProtocols = transactionFilterForProtocols;
|
|
6
|
+
exports.transactionFilterForProtocolAccounts = transactionFilterForProtocolAccounts;
|
|
6
7
|
exports.accountFilterForProtocols = accountFilterForProtocols;
|
|
7
8
|
exports.PUMPFUN_PROGRAM_ID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
|
|
8
9
|
exports.PUMPSWAP_PROGRAM_ID = "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA";
|
|
@@ -51,6 +52,26 @@ function transactionFilterForProtocols(protocols) {
|
|
|
51
52
|
account_required: [],
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Server-side transaction filter for one protocol restricted to known accounts.
|
|
57
|
+
*
|
|
58
|
+
* Yellowstone applies `account_include` as ANY and `account_required` as ALL,
|
|
59
|
+
* so matching transactions must contain the protocol program and at least one
|
|
60
|
+
* tracked mint, pool, or other account.
|
|
61
|
+
*/
|
|
62
|
+
function transactionFilterForProtocolAccounts(protocol, accounts) {
|
|
63
|
+
const trackedAccounts = [
|
|
64
|
+
...new Set(accounts.map((account) => account.trim()).filter((account) => account.length > 0)),
|
|
65
|
+
].sort();
|
|
66
|
+
if (trackedAccounts.length === 0) {
|
|
67
|
+
throw new RangeError("At least one tracked account is required");
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
account_include: trackedAccounts,
|
|
71
|
+
account_exclude: [],
|
|
72
|
+
account_required: [...exports.PROTOCOL_PROGRAM_IDS[protocol]],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
54
75
|
/** 与 Rust `AccountFilter::for_protocols` 一致 */
|
|
55
76
|
function accountFilterForProtocols(protocols) {
|
|
56
77
|
return {
|
|
@@ -5,15 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.grpcTxIndexFromInfo = grpcTxIndexFromInfo;
|
|
7
7
|
exports.parseDexEventsFromGrpcTransactionInfo = parseDexEventsFromGrpcTransactionInfo;
|
|
8
|
-
/**
|
|
9
|
-
* Yellowstone gRPC 单笔交易:统一交易解析(指令 + 日志 + 与 Rust 相同的账户/数据填充)。
|
|
10
|
-
* 依赖 `@triton-one/yellowstone-grpc` 的 `txEncode`(Binary)得到与 web3 兼容的 meta + 反序列化交易。
|
|
11
|
-
*/
|
|
12
|
-
const yellowstone_grpc_1 = require("@triton-one/yellowstone-grpc");
|
|
13
|
-
const yellowstone_grpc_solana_encoding_wasm_js_1 = require("@triton-one/yellowstone-grpc/dist/encoding/yellowstone_grpc_solana_encoding_wasm.js");
|
|
14
8
|
const bs58_1 = __importDefault(require("bs58"));
|
|
15
|
-
const web3_js_1 = require("@solana/web3.js");
|
|
16
9
|
const rpc_transaction_js_1 = require("../rpc_transaction.js");
|
|
10
|
+
const yellowstone_transaction_adapter_js_1 = require("./yellowstone_transaction_adapter.js");
|
|
17
11
|
/**
|
|
18
12
|
* Yellowstone `SubscribeUpdateTransactionInfo.index` → `EventMetadata.tx_index`。
|
|
19
13
|
* 与 Rust `sol-parser-sdk` gRPC 路径中 `let idx = info.index` 传入 `parse_logs(..., idx, ...)` 一致。
|
|
@@ -41,19 +35,14 @@ function parseDexEventsFromGrpcTransactionInfo(info, slot, options) {
|
|
|
41
35
|
meta: mr,
|
|
42
36
|
index: String(info.index),
|
|
43
37
|
};
|
|
44
|
-
const enc = yellowstone_grpc_1.txEncode.encode(y, yellowstone_grpc_solana_encoding_wasm_js_1.WasmUiTransactionEncoding.Binary, 0, false);
|
|
45
|
-
const vt = web3_js_1.VersionedTransaction.deserialize(bs58_1.default.decode(enc.transaction));
|
|
46
|
-
const meta = enc.meta;
|
|
47
38
|
const slotNum = typeof slot === "bigint" ? Number(slot) : Number(slot);
|
|
48
39
|
const signatureBase58 = bs58_1.default.encode(Uint8Array.from(info.signature));
|
|
49
40
|
const txIndex = grpcTxIndexFromInfo(info);
|
|
50
41
|
const blockTime = options?.blockTimeUs == null ? null : Math.floor(options.blockTimeUs / 1_000_000);
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
transaction: vt,
|
|
56
|
-
}, signatureBase58, options?.eventTypeFilter, {
|
|
42
|
+
const response = (0, yellowstone_transaction_adapter_js_1.yellowstoneTransactionToWeb3)(y.transaction, y.meta, slotNum, blockTime);
|
|
43
|
+
// This reuses the parser for the web3-compatible in-memory shape. It does not
|
|
44
|
+
// perform an RPC request or any other network I/O.
|
|
45
|
+
const parsed = (0, rpc_transaction_js_1.parseRpcTransaction)(response, signatureBase58, options?.eventTypeFilter, {
|
|
57
46
|
blockTimeUs: options?.blockTimeUs,
|
|
58
47
|
grpcRecvUs: options?.grpcRecvUs,
|
|
59
48
|
txIndex,
|
|
@@ -0,0 +1,4 @@
|
|
|
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";
|
|
3
|
+
/** Convert Yellowstone protobuf objects directly, without a WASM/Base58 serialization round trip. */
|
|
4
|
+
export declare function yellowstoneTransactionToWeb3(transaction: YellowstoneTransaction, meta: YellowstoneMeta, slot: number, blockTime: number | null): VersionedTransactionResponse;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.yellowstoneTransactionToWeb3 = yellowstoneTransactionToWeb3;
|
|
7
|
+
const bs58_1 = __importDefault(require("bs58"));
|
|
8
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
9
|
+
function publicKey(bytes) {
|
|
10
|
+
return new web3_js_1.PublicKey(bytes);
|
|
11
|
+
}
|
|
12
|
+
function messageHeader(message) {
|
|
13
|
+
const header = message.header;
|
|
14
|
+
if (!header)
|
|
15
|
+
throw new TypeError("Yellowstone transaction message is missing its header");
|
|
16
|
+
return {
|
|
17
|
+
numRequiredSignatures: header.numRequiredSignatures,
|
|
18
|
+
numReadonlySignedAccounts: header.numReadonlySignedAccounts,
|
|
19
|
+
numReadonlyUnsignedAccounts: header.numReadonlyUnsignedAccounts,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function legacyMessage(message) {
|
|
23
|
+
const instructions = message.instructions.map((instruction) => ({
|
|
24
|
+
programIdIndex: instruction.programIdIndex,
|
|
25
|
+
accounts: [...instruction.accounts],
|
|
26
|
+
data: bs58_1.default.encode(instruction.data),
|
|
27
|
+
}));
|
|
28
|
+
return new web3_js_1.Message({
|
|
29
|
+
header: messageHeader(message),
|
|
30
|
+
accountKeys: message.accountKeys.map(publicKey),
|
|
31
|
+
recentBlockhash: bs58_1.default.encode(message.recentBlockhash),
|
|
32
|
+
instructions,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function versionedMessage(message) {
|
|
36
|
+
const compiledInstructions = message.instructions.map((instruction) => ({
|
|
37
|
+
programIdIndex: instruction.programIdIndex,
|
|
38
|
+
accountKeyIndexes: [...instruction.accounts],
|
|
39
|
+
data: instruction.data,
|
|
40
|
+
}));
|
|
41
|
+
return new web3_js_1.MessageV0({
|
|
42
|
+
header: messageHeader(message),
|
|
43
|
+
staticAccountKeys: message.accountKeys.map(publicKey),
|
|
44
|
+
recentBlockhash: bs58_1.default.encode(message.recentBlockhash),
|
|
45
|
+
compiledInstructions,
|
|
46
|
+
addressTableLookups: message.addressTableLookups.map((lookup) => ({
|
|
47
|
+
accountKey: publicKey(lookup.accountKey),
|
|
48
|
+
writableIndexes: [...lookup.writableIndexes],
|
|
49
|
+
readonlyIndexes: [...lookup.readonlyIndexes],
|
|
50
|
+
})),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function transactionMeta(meta) {
|
|
54
|
+
return {
|
|
55
|
+
err: null,
|
|
56
|
+
fee: Number(meta.fee),
|
|
57
|
+
preBalances: meta.preBalances.map(Number),
|
|
58
|
+
postBalances: meta.postBalances.map(Number),
|
|
59
|
+
innerInstructions: meta.innerInstructionsNone
|
|
60
|
+
? null
|
|
61
|
+
: meta.innerInstructions.map((group) => ({
|
|
62
|
+
index: group.index,
|
|
63
|
+
instructions: group.instructions.map((instruction) => ({
|
|
64
|
+
programIdIndex: instruction.programIdIndex,
|
|
65
|
+
accounts: [...instruction.accounts],
|
|
66
|
+
// The internal parser accepts bytes directly; keep Yellowstone's representation
|
|
67
|
+
// instead of paying for a Base58 encode followed by an immediate decode.
|
|
68
|
+
data: instruction.data,
|
|
69
|
+
...(instruction.stackHeight !== undefined
|
|
70
|
+
? { stackHeight: instruction.stackHeight }
|
|
71
|
+
: {}),
|
|
72
|
+
})),
|
|
73
|
+
})),
|
|
74
|
+
logMessages: meta.logMessagesNone ? null : meta.logMessages,
|
|
75
|
+
preTokenBalances: [],
|
|
76
|
+
postTokenBalances: [],
|
|
77
|
+
loadedAddresses: {
|
|
78
|
+
writable: meta.loadedWritableAddresses.map(publicKey),
|
|
79
|
+
readonly: meta.loadedReadonlyAddresses.map(publicKey),
|
|
80
|
+
},
|
|
81
|
+
...(meta.computeUnitsConsumed !== undefined
|
|
82
|
+
? { computeUnitsConsumed: Number(meta.computeUnitsConsumed) }
|
|
83
|
+
: {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Convert Yellowstone protobuf objects directly, without a WASM/Base58 serialization round trip. */
|
|
87
|
+
function yellowstoneTransactionToWeb3(transaction, meta, slot, blockTime) {
|
|
88
|
+
const message = transaction.message;
|
|
89
|
+
if (!message)
|
|
90
|
+
throw new TypeError("Yellowstone transaction is missing its message");
|
|
91
|
+
return {
|
|
92
|
+
slot,
|
|
93
|
+
blockTime,
|
|
94
|
+
meta: transactionMeta(meta),
|
|
95
|
+
transaction: {
|
|
96
|
+
message: message.versioned ? versionedMessage(message) : legacyMessage(message),
|
|
97
|
+
signatures: transaction.signatures.map((signature) => bs58_1.default.encode(signature)),
|
|
98
|
+
},
|
|
99
|
+
version: message.versioned ? 0 : "legacy",
|
|
100
|
+
};
|
|
101
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,8 +19,8 @@ export { pubkeyBytesToBs58 as pubkey_bytes_to_bs58, collectAccountKeysBs58 as co
|
|
|
19
19
|
export { type OrderMode, type Protocol, type ClientConfig, type StreamingConfig, type TransactionFilter, type AccountFilter, type AccountFilterData, type AccountFilterMemcmp, type SlotFilter, type EventType, type StreamingEventType, type EventTypeFilter, defaultClientConfig, lowLatencyClientConfig, highThroughputClientConfig, newTransactionFilter, transactionFilterFromProgramIds, newAccountFilter, newSlotFilter, slotFilterMinSlot, slotFilterMaxSlot, accountFilterFromProgramOwners, accountFilterMemcmp, eventTypeFilterIncludeOnly, eventTypeFilterExclude, eventTypeFilterIncludesPumpfun, eventTypeFilterIncludesPumpswap, eventTypeFilterIncludesMeteoraDammV2, eventTypeFilterIncludesMeteoraDbc, eventTypeFilterIncludesMeteoraPools, eventTypeFilterIncludesMeteoraDlmm, eventTypeFilterIncludesRaydiumClmm, eventTypeFilterIncludesRaydiumCpmm, eventTypeFilterIncludesRaydiumAmmV4, eventTypeFilterIncludesOrcaWhirlpool, eventTypeFilterIncludesRaydiumLaunchlab, eventTypeFilterIncludesPumpFees, eventTypeFilterAllowsInstructionParsing, ALL_EVENT_TYPES, } from "./grpc/types.js";
|
|
20
20
|
export { ALL_EVENT_TYPES as all_event_types } from "./grpc/types.js";
|
|
21
21
|
export type { StreamingConfig as streaming_config } from "./grpc/types.js";
|
|
22
|
-
export { PROTOCOL_PROGRAM_IDS, PUMPFUN_PROGRAM_ID, PUMPSWAP_PROGRAM_ID, PUMPSWAP_FEES_PROGRAM_ID, PUMP_FEES_PROGRAM_ID, RAYDIUM_LAUNCHLAB_PROGRAM_ID, RAYDIUM_CPMM_PROGRAM_ID, RAYDIUM_CLMM_PROGRAM_ID, RAYDIUM_AMM_V4_PROGRAM_ID, ORCA_WHIRLPOOL_PROGRAM_ID, METEORA_POOLS_PROGRAM_ID, METEORA_DAMM_V2_PROGRAM_ID, METEORA_DLMM_PROGRAM_ID, METEORA_DBC_PROGRAM_ID, getProgramIdsForProtocols, transactionFilterForProtocols, accountFilterForProtocols, } from "./grpc/program_ids.js";
|
|
23
|
-
export { PROTOCOL_PROGRAM_IDS as protocol_program_ids, getProgramIdsForProtocols as get_program_ids_for_protocols, transactionFilterForProtocols as transaction_filter_for_protocols, accountFilterForProtocols as account_filter_for_protocols, } from "./grpc/program_ids.js";
|
|
22
|
+
export { PROTOCOL_PROGRAM_IDS, PUMPFUN_PROGRAM_ID, PUMPSWAP_PROGRAM_ID, PUMPSWAP_FEES_PROGRAM_ID, PUMP_FEES_PROGRAM_ID, RAYDIUM_LAUNCHLAB_PROGRAM_ID, RAYDIUM_CPMM_PROGRAM_ID, RAYDIUM_CLMM_PROGRAM_ID, RAYDIUM_AMM_V4_PROGRAM_ID, ORCA_WHIRLPOOL_PROGRAM_ID, METEORA_POOLS_PROGRAM_ID, METEORA_DAMM_V2_PROGRAM_ID, METEORA_DLMM_PROGRAM_ID, METEORA_DBC_PROGRAM_ID, getProgramIdsForProtocols, transactionFilterForProtocols, transactionFilterForProtocolAccounts, accountFilterForProtocols, } from "./grpc/program_ids.js";
|
|
23
|
+
export { PROTOCOL_PROGRAM_IDS as protocol_program_ids, getProgramIdsForProtocols as get_program_ids_for_protocols, transactionFilterForProtocols as transaction_filter_for_protocols, transactionFilterForProtocolAccounts as transaction_filter_for_protocol_accounts, accountFilterForProtocols as account_filter_for_protocols, } from "./grpc/program_ids.js";
|
|
24
24
|
/** Rust `grpc::types` 蛇形命名别名 */
|
|
25
25
|
export { defaultClientConfig as default_client_config, lowLatencyClientConfig as low_latency_client_config, highThroughputClientConfig as high_throughput_client_config, newTransactionFilter as new_transaction_filter, transactionFilterFromProgramIds as transaction_filter_from_program_ids, newAccountFilter as new_account_filter, newSlotFilter as new_slot_filter, slotFilterMinSlot as slot_filter_min_slot, slotFilterMaxSlot as slot_filter_max_slot, accountFilterFromProgramOwners as account_filter_from_program_owners, accountFilterMemcmp as account_filter_memcmp, eventTypeFilterIncludeOnly as event_type_filter_include_only, eventTypeFilterExclude as event_type_filter_exclude, eventTypeFilterIncludeOnly as include_only, eventTypeFilterExclude as exclude_types, } from "./grpc/types.js";
|
|
26
26
|
export { buildSubscribeRequest, buildSubscribeRequestWithCommitment, buildSubscribeTransactionFiltersNamed, CommitmentLevel, } from "./grpc/subscribe_builder.js";
|
package/dist/index.js
CHANGED
|
@@ -35,9 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.heuristicSolCounterpartiesForWatchedKeys = exports.lamportBalanceDeltas = exports.collectAccountKeysBs58 = exports.pubkeyBytesToBs58 = exports.grpcTxIndexFromInfo = exports.parseDexEventsFromGrpcTransactionInfo = exports.convert_rpc_to_grpc = exports.convertRpcToGrpc = exports.applyAccountFillsToLogEvents = exports.fillDataRpc = exports.fillAccountsFromTransactionDataRpc = exports.parseRpcTransaction = exports.parseTransactionFromRpc = exports.isWarmedUp = exports.warmupParser = exports.build_account_pubkeys_with_cache = exports.buildAccountPubkeysWithCache = exports.AccountPubkeyCache = exports.normalizeBuySellFromInputMint = exports.normalizeBuySellFromTokenDelta = exports.vaultPriceFromBalances = exports.sqrtPriceX64ToPrice = exports.elapsedMicrosSince = exports.nowNanos = exports.nowMicros = exports.nowUs = exports.parse_log = exports.parseLog = exports.parseTransactionWithStreamingListener = exports.parseLogsStreaming = exports.parseTransactionEventsStreaming = exports.parseTransactionWithListener = exports.parseLogsOnly = exports.parseTransactionEvents = exports.enrichPumpfunSameTxPostMerge = exports.enrichPumpfunTradesFromCreateInstructions = exports.enrichCreateV2ObservedFeeRecipient = exports.enrichCreateV2FromCreateEvents = exports.formatParseError = exports.dexEventToJsonString = exports.bigintToJsonReplacer = exports.defaultPubkey = exports.metadataForDexEvent = exports.SLOW_PROCESSING_THRESHOLD_US = exports.DEFAULT_REQUEST_TIMEOUT = exports.DEFAULT_METRICS_WINDOW_SECONDS = exports.DEFAULT_METRICS_PRINT_INTERVAL_SECONDS = exports.DEFAULT_MAX_DECODING_MESSAGE_SIZE = exports.DEFAULT_CONNECT_TIMEOUT = exports.DEFAULT_CHANNEL_SIZE = void 0;
|
|
37
37
|
exports.ORCA_WHIRLPOOL_PROGRAM_ID = exports.RAYDIUM_AMM_V4_PROGRAM_ID = exports.RAYDIUM_CLMM_PROGRAM_ID = exports.RAYDIUM_CPMM_PROGRAM_ID = exports.RAYDIUM_LAUNCHLAB_PROGRAM_ID = exports.PUMP_FEES_PROGRAM_ID = exports.PUMPSWAP_FEES_PROGRAM_ID = exports.PUMPSWAP_PROGRAM_ID = exports.PUMPFUN_PROGRAM_ID = exports.PROTOCOL_PROGRAM_IDS = exports.all_event_types = exports.ALL_EVENT_TYPES = exports.eventTypeFilterAllowsInstructionParsing = exports.eventTypeFilterIncludesPumpFees = exports.eventTypeFilterIncludesRaydiumLaunchlab = exports.eventTypeFilterIncludesOrcaWhirlpool = exports.eventTypeFilterIncludesRaydiumAmmV4 = exports.eventTypeFilterIncludesRaydiumCpmm = exports.eventTypeFilterIncludesRaydiumClmm = exports.eventTypeFilterIncludesMeteoraDlmm = exports.eventTypeFilterIncludesMeteoraPools = exports.eventTypeFilterIncludesMeteoraDbc = exports.eventTypeFilterIncludesMeteoraDammV2 = exports.eventTypeFilterIncludesPumpswap = exports.eventTypeFilterIncludesPumpfun = exports.eventTypeFilterExclude = exports.eventTypeFilterIncludeOnly = exports.accountFilterMemcmp = exports.accountFilterFromProgramOwners = exports.slotFilterMaxSlot = exports.slotFilterMinSlot = exports.newSlotFilter = exports.newAccountFilter = exports.transactionFilterFromProgramIds = exports.newTransactionFilter = exports.highThroughputClientConfig = exports.lowLatencyClientConfig = exports.defaultClientConfig = exports.try_yellowstone_signature = exports.collect_watch_transfer_counterparty_pairs = exports.spl_token_counterparty_by_owner = exports.token_balance_raw_amount = exports.heuristic_sol_counterparties_for_watched_keys = exports.lamport_balance_deltas = exports.collect_account_keys_bs58 = exports.pubkey_bytes_to_bs58 = exports.tryYellowstoneSignature = exports.collectWatchTransferCounterpartyPairs = exports.splTokenCounterpartyByOwner = exports.tokenBalanceRawAmount = void 0;
|
|
38
|
-
exports.
|
|
39
|
-
exports.
|
|
40
|
-
exports.high_throughput_shred_stream_config = exports.low_latency_shred_stream_config = exports.default_shred_stream_config = exports.highThroughputShredStreamConfig = exports.lowLatencyShredStreamConfig = exports.defaultShredStreamConfig = exports.wireBytesToShredWasmTx = exports.decodeShredstreamEntriesBincode = exports.decodeEntriesBincodeNested = exports.decodeEntriesBincodeFlat = exports.bincodeVecEntryCount = exports.loadAddressLookupTableAccounts = exports.fullAccountKeyStringsFromShredTx = exports.dexEventsFromShredWasmTxWithFullKeys = exports.dexEventsFromShredWasmTx = void 0;
|
|
38
|
+
exports.lookup_discriminator = exports.lookupDiscriminator = exports.discriminator_to_protocol = exports.discriminatorToProtocol = exports.discriminator_to_name = exports.discriminatorToName = exports.parse_log_unified = exports.parse_log_optimized = exports.parse_meteora_dlmm_log = exports.parse_meteora_damm_log = exports.parseProgramCompleteInfo = exports.parseLogUnified = exports.parseLogOptimizedWithProgramId = exports.parseLogOptimized = exports.parseInvokeInfo = exports.build_subscribe_transaction_filters_named = exports.build_subscribe_request_with_commitment = exports.build_subscribe_request = exports.CommitmentLevel = exports.buildSubscribeTransactionFiltersNamed = exports.buildSubscribeRequestWithCommitment = exports.buildSubscribeRequest = exports.exclude_types = exports.include_only = exports.event_type_filter_exclude = exports.event_type_filter_include_only = exports.account_filter_memcmp = exports.account_filter_from_program_owners = exports.slot_filter_max_slot = exports.slot_filter_min_slot = exports.new_slot_filter = exports.new_account_filter = exports.transaction_filter_from_program_ids = exports.new_transaction_filter = exports.high_throughput_client_config = exports.low_latency_client_config = exports.default_client_config = exports.account_filter_for_protocols = exports.transaction_filter_for_protocol_accounts = exports.transaction_filter_for_protocols = exports.get_program_ids_for_protocols = exports.protocol_program_ids = exports.accountFilterForProtocols = exports.transactionFilterForProtocolAccounts = exports.transactionFilterForProtocols = exports.getProgramIdsForProtocols = exports.METEORA_DBC_PROGRAM_ID = exports.METEORA_DLMM_PROGRAM_ID = exports.METEORA_DAMM_V2_PROGRAM_ID = exports.METEORA_POOLS_PROGRAM_ID = void 0;
|
|
39
|
+
exports.geyser_grpc_channel_options = exports.default_geyser_connect_config = exports.connect_yellowstone_geyser = exports.event_parser = exports.geyserGrpcChannelOptions = exports.defaultGeyserConnectConfig = exports.connectYellowstoneGeyser = exports.YellowstoneGrpc = exports.programIds = exports.parse_pump_fees_instruction = exports.parse_meteora_pools_instruction = exports.parse_meteora_dlmm_instruction = exports.parse_meteora_damm_instruction = exports.parse_pumpswap_instruction = exports.parse_pumpfun_instruction = exports.parse_instruction_unified = exports.parsePumpFeesInstruction = exports.parseMeteoraPoolsInstruction = exports.parseMeteoraDlmmInstruction = exports.parseMeteoraDammInstruction = exports.parsePumpswapInstruction = exports.parsePumpfunInstruction = exports.parseInstructionUnified = exports.user_wallet_pubkey_for_onchain_account = exports.rpc_resolve_user_wallet_pubkey = exports.parse_pumpswap_pool = exports.parse_pumpswap_global_config = exports.parse_pumpfun_global = exports.parse_token_account = exports.parse_nonce_account = exports.parse_account_unified = exports.rpcResolveUserWalletPubkey = exports.userWalletPubkeyForOnchainAccount = exports.hasDiscriminator = exports.isPoolAccount = exports.isGlobalConfigAccount = exports.parsePumpswapAccount = exports.parsePumpswapPool = exports.parsePumpswapGlobalConfig = exports.isPumpfunGlobalAccount = exports.parsePumpfunAccount = exports.parsePumpfunGlobal = exports.parseTokenAccount = exports.isNonceAccount = exports.parseNonceAccount = exports.parseAccountUnified = exports.u64leDiscriminator = exports.PUMPSWAP_DISC = exports.PROGRAM_LOG_DISC = exports.parseMeteoraDammLog = void 0;
|
|
40
|
+
exports.high_throughput_shred_stream_config = exports.low_latency_shred_stream_config = exports.default_shred_stream_config = exports.highThroughputShredStreamConfig = exports.lowLatencyShredStreamConfig = exports.defaultShredStreamConfig = exports.wireBytesToShredWasmTx = exports.decodeShredstreamEntriesBincode = exports.decodeEntriesBincodeNested = exports.decodeEntriesBincodeFlat = exports.bincodeVecEntryCount = exports.loadAddressLookupTableAccounts = exports.fullAccountKeyStringsFromShredTx = exports.dexEventsFromShredWasmTxWithFullKeys = exports.dexEventsFromShredWasmTx = exports.ShredEventQueue = exports.ShredStreamClient = void 0;
|
|
41
41
|
var constants_js_1 = require("./common/constants.js");
|
|
42
42
|
Object.defineProperty(exports, "DEFAULT_CHANNEL_SIZE", { enumerable: true, get: function () { return constants_js_1.DEFAULT_CHANNEL_SIZE; } });
|
|
43
43
|
Object.defineProperty(exports, "DEFAULT_CONNECT_TIMEOUT", { enumerable: true, get: function () { return constants_js_1.DEFAULT_CONNECT_TIMEOUT; } });
|
|
@@ -162,11 +162,13 @@ Object.defineProperty(exports, "METEORA_DLMM_PROGRAM_ID", { enumerable: true, ge
|
|
|
162
162
|
Object.defineProperty(exports, "METEORA_DBC_PROGRAM_ID", { enumerable: true, get: function () { return program_ids_js_1.METEORA_DBC_PROGRAM_ID; } });
|
|
163
163
|
Object.defineProperty(exports, "getProgramIdsForProtocols", { enumerable: true, get: function () { return program_ids_js_1.getProgramIdsForProtocols; } });
|
|
164
164
|
Object.defineProperty(exports, "transactionFilterForProtocols", { enumerable: true, get: function () { return program_ids_js_1.transactionFilterForProtocols; } });
|
|
165
|
+
Object.defineProperty(exports, "transactionFilterForProtocolAccounts", { enumerable: true, get: function () { return program_ids_js_1.transactionFilterForProtocolAccounts; } });
|
|
165
166
|
Object.defineProperty(exports, "accountFilterForProtocols", { enumerable: true, get: function () { return program_ids_js_1.accountFilterForProtocols; } });
|
|
166
167
|
var program_ids_js_2 = require("./grpc/program_ids.js");
|
|
167
168
|
Object.defineProperty(exports, "protocol_program_ids", { enumerable: true, get: function () { return program_ids_js_2.PROTOCOL_PROGRAM_IDS; } });
|
|
168
169
|
Object.defineProperty(exports, "get_program_ids_for_protocols", { enumerable: true, get: function () { return program_ids_js_2.getProgramIdsForProtocols; } });
|
|
169
170
|
Object.defineProperty(exports, "transaction_filter_for_protocols", { enumerable: true, get: function () { return program_ids_js_2.transactionFilterForProtocols; } });
|
|
171
|
+
Object.defineProperty(exports, "transaction_filter_for_protocol_accounts", { enumerable: true, get: function () { return program_ids_js_2.transactionFilterForProtocolAccounts; } });
|
|
170
172
|
Object.defineProperty(exports, "account_filter_for_protocols", { enumerable: true, get: function () { return program_ids_js_2.accountFilterForProtocols; } });
|
|
171
173
|
/** Rust `grpc::types` 蛇形命名别名 */
|
|
172
174
|
var types_js_3 = require("./grpc/types.js");
|
package/dist/rpc_transaction.js
CHANGED
|
@@ -30,6 +30,19 @@ const PUMPFUN_TRADE_EVENT_NAMES = new Set([
|
|
|
30
30
|
]);
|
|
31
31
|
const EVENT_CPI_PREFIX = Uint8Array.from([228, 69, 165, 46, 81, 203, 154, 29]);
|
|
32
32
|
const LEGACY_EVENT_CPI_SUFFIX = Uint8Array.from([155, 167, 108, 32, 122, 76, 173, 64]);
|
|
33
|
+
const SUPPORTED_INSTRUCTION_PROGRAM_IDS = new Set([
|
|
34
|
+
program_ids_js_1.PUMPFUN_PROGRAM_ID,
|
|
35
|
+
program_ids_js_1.PUMPSWAP_PROGRAM_ID,
|
|
36
|
+
program_ids_js_1.PUMP_FEES_PROGRAM_ID,
|
|
37
|
+
program_ids_js_1.RAYDIUM_LAUNCHLAB_PROGRAM_ID,
|
|
38
|
+
program_ids_js_1.RAYDIUM_CPMM_PROGRAM_ID,
|
|
39
|
+
program_ids_js_1.RAYDIUM_CLMM_PROGRAM_ID,
|
|
40
|
+
program_ids_js_1.RAYDIUM_AMM_V4_PROGRAM_ID,
|
|
41
|
+
program_ids_js_1.ORCA_WHIRLPOOL_PROGRAM_ID,
|
|
42
|
+
program_ids_js_1.METEORA_POOLS_PROGRAM_ID,
|
|
43
|
+
program_ids_js_1.METEORA_DAMM_V2_PROGRAM_ID,
|
|
44
|
+
program_ids_js_1.METEORA_DLMM_PROGRAM_ID,
|
|
45
|
+
]);
|
|
33
46
|
function bytesEqualAt(data, expected, offset) {
|
|
34
47
|
for (let i = 0; i < expected.length; i++) {
|
|
35
48
|
if (data[offset + i] !== expected[i])
|
|
@@ -379,6 +392,8 @@ function parseOuterAndInnerInstructions(message, meta, signature, slot, txIndex,
|
|
|
379
392
|
const programId = (0, rpc_invoke_map_js_1.accountKeyToBase58)(resolver.get(ix.programIdIndex));
|
|
380
393
|
if (!programId)
|
|
381
394
|
continue;
|
|
395
|
+
if (!SUPPORTED_INSTRUCTION_PROGRAM_IDS.has(programId))
|
|
396
|
+
continue;
|
|
382
397
|
const data = (0, rpc_invoke_map_js_1.decodeIxData)(ix.data);
|
|
383
398
|
const accounts = resolveAccounts(resolver, ix.accountKeyIndexes);
|
|
384
399
|
const ev = (0, mod_js_1.parseInstructionUnified)(data, accounts, signature, slot, txIndex, blockTimeUs, grpcRecvUs, filter, programId);
|
|
@@ -399,6 +414,8 @@ function parseOuterAndInnerInstructions(message, meta, signature, slot, txIndex,
|
|
|
399
414
|
const programId = (0, rpc_invoke_map_js_1.accountKeyToBase58)(resolver.get(ix.programIdIndex));
|
|
400
415
|
if (!programId)
|
|
401
416
|
continue;
|
|
417
|
+
if (!SUPPORTED_INSTRUCTION_PROGRAM_IDS.has(programId))
|
|
418
|
+
continue;
|
|
402
419
|
const data = (0, rpc_invoke_map_js_1.decodeIxData)(ix.data);
|
|
403
420
|
const accounts = resolveAccounts(resolver, ix.accounts);
|
|
404
421
|
const ev = (0, inner_js_1.parseInnerCompiledInstructionIfSupported)(data, accounts, signature, slot, txIndex, blockTimeUs, grpcRecvUs, filter, programId) ??
|
|
@@ -493,8 +510,7 @@ function parseRpcTransaction(tx, signature, filter, options) {
|
|
|
493
510
|
activeProgramStack.length = idx;
|
|
494
511
|
}
|
|
495
512
|
}
|
|
496
|
-
applyRpcFills(instructionEvents, msg, meta);
|
|
497
|
-
applyRpcFills(logEvents, msg, meta);
|
|
513
|
+
applyRpcFills([...instructionEvents, ...logEvents], msg, meta);
|
|
498
514
|
const events = (0, log_instr_dedup_js_1.dedupeLogInstructionEvents)(logEvents, instructionEvents);
|
|
499
515
|
(0, pumpfun_fee_enrich_js_1.enrichPumpfunSameTxPostMerge)(events);
|
|
500
516
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sol-parser-sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
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",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"test": "vitest run",
|
|
33
33
|
"check:migration": "npm run check:dex-events && npm run verify:discriminators && npm run check:discriminator-lut && npm run check:json-utils && npm run build",
|
|
34
34
|
"example:grpc:devtrades": "tsx examples/devtrades_low_latency.ts",
|
|
35
|
+
"example:grpc:devtrades:pumpswap": "tsx examples/devtrades_pumpfun_pumpswap_low_latency.ts",
|
|
36
|
+
"example:grpc:pumpswap": "tsx examples/pumpswap_low_latency.ts",
|
|
35
37
|
"example:shredstream:subscribe": "tsx examples/shredstream_example.ts",
|
|
36
38
|
"example:shredstream:pumpfun-json": "tsx examples/shredstream_pumpfun_json.ts",
|
|
37
39
|
"example:shredstream": "tsx examples/shredstream_example.ts",
|