sol-parser-sdk 0.5.9 → 0.5.10
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 +31 -6
- package/README_CN.md +29 -6
- package/dist/accounts/pumpswap.js +15 -2
- package/dist/core/account_fill_raydium.d.ts +1 -1
- package/dist/core/account_fill_raydium.js +5 -1
- package/dist/core/dex_event.d.ts +11 -0
- package/dist/grpc/async_event_queue.d.ts +19 -0
- package/dist/grpc/async_event_queue.js +73 -0
- package/dist/grpc/client.d.ts +15 -4
- package/dist/grpc/client.js +102 -64
- package/dist/grpc/geyser_connect.d.ts +7 -1
- package/dist/grpc/geyser_connect.js +7 -1
- package/dist/grpc/log_instr_dedup.js +30 -12
- package/dist/grpc/types.d.ts +5 -3
- package/dist/grpc/types.js +5 -2
- package/dist/index.d.ts +1 -1
- package/dist/instr/inner.js +37 -15
- package/dist/instr/meteora_dlmm_ix.js +130 -38
- package/dist/instr/orca_whirlpool_ix.js +2 -1
- package/dist/instr/pumpswap_ix.js +10 -0
- package/dist/instr/raydium_amm_v4_ix.js +59 -36
- package/dist/instr/raydium_cpmm_ix.js +2 -2
- package/dist/logs/discriminator_lut.js +6 -2
- package/dist/logs/meteora_dlmm.d.ts +1 -0
- package/dist/logs/meteora_dlmm.js +135 -11
- package/dist/logs/optimized_matcher.js +25 -10
- package/dist/logs/program_log_discriminators.d.ts +1 -0
- package/dist/logs/program_log_discriminators.js +2 -0
- package/dist/logs/pump_amm.js +90 -22
- package/dist/logs/raydium_amm.d.ts +2 -0
- package/dist/logs/raydium_amm.js +36 -0
- package/dist/logs/raydium_cpmm.d.ts +2 -0
- package/dist/logs/raydium_cpmm.js +43 -0
- package/dist/rpc_transaction.js +118 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,12 +16,6 @@
|
|
|
16
16
|
<a href="https://discord.gg/vuazbGkqQE">Discord</a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
> ☕ **Support This Project**
|
|
20
|
-
>
|
|
21
|
-
> This SDK is completely free and open source. However, maintaining and continuously updating it requires significant AI computing resources and token consumption. If this SDK helps with your development, consider making a monthly SOL donation — any amount is appreciated and helps keep this project alive!
|
|
22
|
-
>
|
|
23
|
-
> **Donation Wallet:** `6oW7AXz1yRb57pYSxysuXnMs2aR1ha5rzGzReZ1MjPV8`
|
|
24
|
-
|
|
25
19
|
---
|
|
26
20
|
|
|
27
21
|
## Other language SDKs
|
|
@@ -153,6 +147,37 @@ const sub = await client.subscribeTransactions(filter, {
|
|
|
153
147
|
console.log("subscribed", sub.id);
|
|
154
148
|
```
|
|
155
149
|
|
|
150
|
+
### Long-running low-latency DEX subscriptions
|
|
151
|
+
|
|
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`, while ring-buffer dequeue,
|
|
154
|
+
Yellowstone update parsing, HTTP/2 receive-window tuning, and reconnect behavior are handled inside
|
|
155
|
+
the SDK:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { YellowstoneGrpc, lowLatencyClientConfig } from "sol-parser-sdk";
|
|
159
|
+
|
|
160
|
+
const client = new YellowstoneGrpc(ENDPOINT, X_TOKEN, lowLatencyClientConfig());
|
|
161
|
+
const sub = await client.subscribeDexEvents(txFilters, accountFilters, eventTypeFilter);
|
|
162
|
+
|
|
163
|
+
void (async () => {
|
|
164
|
+
for await (const error of sub.errors) console.error("subscription error", error);
|
|
165
|
+
})();
|
|
166
|
+
|
|
167
|
+
for await (const event of sub) {
|
|
168
|
+
handleEvent(event);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
If using `for await`, the default overflow behavior remains `drop-newest` for compatibility. A
|
|
173
|
+
tip-oriented consumer can choose `queueOverflowStrategy: "drop-oldest"` and monitor `sub.len()` and
|
|
174
|
+
`sub.dropped()`. Every overflow is counted and periodically reported through `sub.errors`.
|
|
175
|
+
|
|
176
|
+
No bounded in-memory stream can guarantee both zero loss and bounded latency when the consumer's
|
|
177
|
+
average processing rate is below the filtered input rate. Keep the loop body CPU-light, avoid
|
|
178
|
+
per-event synchronous logging, filter at the server, and persist or dispatch slow work outside the
|
|
179
|
+
consumer loop.
|
|
180
|
+
|
|
156
181
|
**Lighter path:** `parseLogsOnly(logs, signature, slot, …)` — no `transactionRaw`; use `applyAccountFillsToLogEvents` if you need filled accounts without full gRPC meta.
|
|
157
182
|
|
|
158
183
|
### 5. ShredStream (HTTP — not Yellowstone gRPC)
|
package/README_CN.md
CHANGED
|
@@ -16,12 +16,6 @@
|
|
|
16
16
|
<a href="https://discord.gg/vuazbGkqQE">Discord</a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
> ☕ **支持本项目**
|
|
20
|
-
>
|
|
21
|
-
> 本 SDK 完全免费且开源。但维护和持续更新需要消耗大量 AI 算力与 Token。如果这个 SDK 对您的开发有帮助,欢迎每月捐赠任意数量的 SOL,您的支持将帮助这个项目持续运行!
|
|
22
|
-
>
|
|
23
|
-
> **捐赠钱包:** `6oW7AXz1yRb57pYSxysuXnMs2aR1ha5rzGzReZ1MjPV8`
|
|
24
|
-
|
|
25
19
|
---
|
|
26
20
|
|
|
27
21
|
## 其他语言 SDK
|
|
@@ -153,6 +147,35 @@ const sub = await client.subscribeTransactions(filter, {
|
|
|
153
147
|
console.log("subscribed", sub.id);
|
|
154
148
|
```
|
|
155
149
|
|
|
150
|
+
### 长时间运行的低延迟 DEX 订阅
|
|
151
|
+
|
|
152
|
+
公开 API 保持队列式调用,与 Rust SDK 的 `subscribe_dex_events` 形态一致。Node 使用异步
|
|
153
|
+
迭代器代替 Rust 的 `ArrayQueue`;环形队列出队、Yellowstone update 解析、HTTP/2 接收窗口
|
|
154
|
+
和重连优化全部由 SDK 内部完成:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { YellowstoneGrpc, lowLatencyClientConfig } from "sol-parser-sdk";
|
|
158
|
+
|
|
159
|
+
const client = new YellowstoneGrpc(ENDPOINT, X_TOKEN, lowLatencyClientConfig());
|
|
160
|
+
const sub = await client.subscribeDexEvents(txFilters, accountFilters, eventTypeFilter);
|
|
161
|
+
|
|
162
|
+
void (async () => {
|
|
163
|
+
for await (const error of sub.errors) console.error("subscription error", error);
|
|
164
|
+
})();
|
|
165
|
+
|
|
166
|
+
for await (const event of sub) {
|
|
167
|
+
handleEvent(event);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
若使用 `for await`,默认溢出策略仍是 `drop-newest`,以保持兼容。更重视贴近链尖的消费者
|
|
172
|
+
可以设置 `queueOverflowStrategy: "drop-oldest"`,并监控 `sub.len()` 和 `sub.dropped()`。
|
|
173
|
+
每次溢出都会计数,并定期通过 `sub.errors` 报告。
|
|
174
|
+
|
|
175
|
+
当消费者平均处理速度低于过滤后的入流速度时,任何有界内存流都无法同时保证零丢失和
|
|
176
|
+
有界延迟。消费循环应保持轻量,避免逐事件同步日志,在服务端尽量缩小过滤范围,并把
|
|
177
|
+
慢 I/O 的持久化或分发移出消费循环。
|
|
178
|
+
|
|
156
179
|
更轻量:仅用日志用 `parseLogsOnly`;要补账户可再配合 `applyAccountFillsToLogEvents`。
|
|
157
180
|
|
|
158
181
|
### 5. ShredStream(HTTP,不是 Yellowstone gRPC)
|
|
@@ -11,7 +11,8 @@ const program_ids_js_1 = require("../instr/program_ids.js");
|
|
|
11
11
|
const GLOBAL_DISC = Uint8Array.from([149, 8, 156, 202, 160, 252, 176, 217]);
|
|
12
12
|
const POOL_DISC = Uint8Array.from([241, 154, 109, 4, 17, 177, 109, 188]);
|
|
13
13
|
const GLOBAL_BODY = 634;
|
|
14
|
-
const
|
|
14
|
+
const POOL_LEGACY_BODY = 244;
|
|
15
|
+
const POOL_BODY = 253;
|
|
15
16
|
function isGlobalConfigAccount(data) {
|
|
16
17
|
return (0, utils_js_1.hasDiscriminator)(data, GLOBAL_DISC);
|
|
17
18
|
}
|
|
@@ -103,8 +104,11 @@ function parsePumpswapGlobalConfig(account, metadata) {
|
|
|
103
104
|
return { PumpSwapGlobalConfigAccount: ev };
|
|
104
105
|
}
|
|
105
106
|
function parsePumpswapPool(account, metadata) {
|
|
106
|
-
if (account.data.length < 8 +
|
|
107
|
+
if (account.data.length < 8 + POOL_LEGACY_BODY)
|
|
107
108
|
return null;
|
|
109
|
+
if (account.data.length !== 8 + POOL_LEGACY_BODY && account.data.length < 8 + POOL_BODY) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
108
112
|
if (!isPoolAccount(account.data))
|
|
109
113
|
return null;
|
|
110
114
|
const d = account.data.subarray(8);
|
|
@@ -156,6 +160,14 @@ function parsePumpswapPool(account, metadata) {
|
|
|
156
160
|
const cashback = (0, binary_js_1.readU8)(d, o);
|
|
157
161
|
if (cashback === null)
|
|
158
162
|
return null;
|
|
163
|
+
o += 1;
|
|
164
|
+
let virtual_quote_reserves = 0n;
|
|
165
|
+
if (d.length >= POOL_BODY) {
|
|
166
|
+
const virtual = (0, binary_js_1.readU128LE)(d, o);
|
|
167
|
+
if (virtual === null)
|
|
168
|
+
return null;
|
|
169
|
+
virtual_quote_reserves = BigInt.asIntN(128, virtual);
|
|
170
|
+
}
|
|
159
171
|
const pool = {
|
|
160
172
|
pool_bump,
|
|
161
173
|
index,
|
|
@@ -169,6 +181,7 @@ function parsePumpswapPool(account, metadata) {
|
|
|
169
181
|
coin_creator,
|
|
170
182
|
is_mayhem_mode: mayhem !== 0,
|
|
171
183
|
is_cashback_coin: cashback !== 0,
|
|
184
|
+
virtual_quote_reserves,
|
|
172
185
|
};
|
|
173
186
|
const ev = {
|
|
174
187
|
metadata,
|
|
@@ -11,7 +11,7 @@ export declare function fillRaydiumClmmOpenPositionWithTokenExtNftAccounts(e: Ra
|
|
|
11
11
|
export declare function fillRaydiumClmmClosePositionAccounts(e: RaydiumClmmClosePositionEvent, get: (i: number) => string): void;
|
|
12
12
|
export declare function fillRaydiumClmmIncreaseLiquidityAccounts(e: RaydiumClmmIncreaseLiquidityEvent, get: (i: number) => string): void;
|
|
13
13
|
export declare function fillRaydiumClmmDecreaseLiquidityAccounts(e: RaydiumClmmDecreaseLiquidityEvent, get: (i: number) => string): void;
|
|
14
|
-
export declare function fillRaydiumCpmmSwapAccounts(
|
|
14
|
+
export declare function fillRaydiumCpmmSwapAccounts(e: RaydiumCpmmSwapEvent, get: (i: number) => string): void;
|
|
15
15
|
export declare function fillRaydiumCpmmDepositAccounts(e: RaydiumCpmmDepositEvent, get: (i: number) => string): void;
|
|
16
16
|
export declare function fillRaydiumCpmmWithdrawAccounts(e: RaydiumCpmmWithdrawEvent, get: (i: number) => string): void;
|
|
17
17
|
export declare function fillRaydiumCpmmInitializeAccounts(e: RaydiumCpmmInitializeEvent, get: (i: number) => string): void;
|
|
@@ -63,7 +63,11 @@ function fillRaydiumClmmDecreaseLiquidityAccounts(e, get) {
|
|
|
63
63
|
if (!e.user || e.user === zero)
|
|
64
64
|
e.user = get(0);
|
|
65
65
|
}
|
|
66
|
-
function fillRaydiumCpmmSwapAccounts(
|
|
66
|
+
function fillRaydiumCpmmSwapAccounts(e, get) {
|
|
67
|
+
const zero = Z();
|
|
68
|
+
if (!e.pool_id || e.pool_id === zero)
|
|
69
|
+
e.pool_id = get(3);
|
|
70
|
+
}
|
|
67
71
|
function fillRaydiumCpmmDepositAccounts(e, get) {
|
|
68
72
|
const zero = Z();
|
|
69
73
|
if (!e.user || e.user === zero)
|
package/dist/core/dex_event.d.ts
CHANGED
|
@@ -480,6 +480,11 @@ export interface PumpSwapBuyEvent {
|
|
|
480
480
|
/** 与 `PumpSwapBuyEvent`:PUMP_CASHBACK / IDL */
|
|
481
481
|
cashback_fee_basis_points: bigint;
|
|
482
482
|
cashback: bigint;
|
|
483
|
+
buyback_fee_basis_points: bigint;
|
|
484
|
+
buyback_fee: bigint;
|
|
485
|
+
virtual_quote_reserves: bigint;
|
|
486
|
+
can_boost: boolean;
|
|
487
|
+
base_supply: bigint;
|
|
483
488
|
/** 由 fees 指令数据填充(见 `fillDataRpc`) */
|
|
484
489
|
is_pump_pool: boolean;
|
|
485
490
|
/** 自指令账户填充(account_fillers/pumpswap) */
|
|
@@ -522,6 +527,11 @@ export interface PumpSwapSellEvent {
|
|
|
522
527
|
coin_creator_fee: bigint;
|
|
523
528
|
cashback_fee_basis_points: bigint;
|
|
524
529
|
cashback: bigint;
|
|
530
|
+
buyback_fee_basis_points: bigint;
|
|
531
|
+
buyback_fee: bigint;
|
|
532
|
+
virtual_quote_reserves: bigint;
|
|
533
|
+
can_boost: boolean;
|
|
534
|
+
base_supply: bigint;
|
|
525
535
|
is_pump_pool: boolean;
|
|
526
536
|
base_mint: string;
|
|
527
537
|
quote_mint: string;
|
|
@@ -1432,6 +1442,7 @@ export interface PumpSwapPool {
|
|
|
1432
1442
|
coin_creator: string;
|
|
1433
1443
|
is_mayhem_mode: boolean;
|
|
1434
1444
|
is_cashback_coin: boolean;
|
|
1445
|
+
virtual_quote_reserves: bigint;
|
|
1435
1446
|
}
|
|
1436
1447
|
export interface PumpSwapPoolAccountEvent {
|
|
1437
1448
|
metadata: EventMetadata;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type EventQueueOverflowStrategy = "drop-newest" | "drop-oldest";
|
|
2
|
+
/** Internal O(1) queue used by the public async-iterable subscription. */
|
|
3
|
+
export declare class AsyncEventQueue<T> implements AsyncIterable<T>, AsyncIterator<T> {
|
|
4
|
+
private readonly maxSize;
|
|
5
|
+
private readonly overflowStrategy;
|
|
6
|
+
private readonly items;
|
|
7
|
+
private readonly waiters;
|
|
8
|
+
private closed;
|
|
9
|
+
private droppedCount;
|
|
10
|
+
private head;
|
|
11
|
+
private size;
|
|
12
|
+
constructor(maxSize: number, overflowStrategy?: EventQueueOverflowStrategy);
|
|
13
|
+
push(item: T): boolean;
|
|
14
|
+
len(): number;
|
|
15
|
+
dropped(): number;
|
|
16
|
+
close(): void;
|
|
17
|
+
next(): Promise<IteratorResult<T>>;
|
|
18
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AsyncEventQueue = void 0;
|
|
4
|
+
/** Internal O(1) queue used by the public async-iterable subscription. */
|
|
5
|
+
class AsyncEventQueue {
|
|
6
|
+
maxSize;
|
|
7
|
+
overflowStrategy;
|
|
8
|
+
items;
|
|
9
|
+
waiters = [];
|
|
10
|
+
closed = false;
|
|
11
|
+
droppedCount = 0;
|
|
12
|
+
head = 0;
|
|
13
|
+
size = 0;
|
|
14
|
+
constructor(maxSize, overflowStrategy = "drop-newest") {
|
|
15
|
+
this.maxSize = maxSize;
|
|
16
|
+
this.overflowStrategy = overflowStrategy;
|
|
17
|
+
if (!Number.isInteger(maxSize) || maxSize < 1) {
|
|
18
|
+
throw new RangeError("AsyncEventQueue maxSize must be a positive integer");
|
|
19
|
+
}
|
|
20
|
+
this.items = new Array(maxSize);
|
|
21
|
+
}
|
|
22
|
+
push(item) {
|
|
23
|
+
if (this.closed)
|
|
24
|
+
return false;
|
|
25
|
+
const waiter = this.waiters.shift();
|
|
26
|
+
if (waiter) {
|
|
27
|
+
waiter({ value: item, done: false });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (this.size < this.maxSize) {
|
|
31
|
+
this.items[(this.head + this.size) % this.maxSize] = item;
|
|
32
|
+
this.size++;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
this.droppedCount++;
|
|
36
|
+
if (this.overflowStrategy === "drop-oldest") {
|
|
37
|
+
this.items[this.head] = item;
|
|
38
|
+
this.head = (this.head + 1) % this.maxSize;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
len() {
|
|
43
|
+
return this.size;
|
|
44
|
+
}
|
|
45
|
+
dropped() {
|
|
46
|
+
return this.droppedCount;
|
|
47
|
+
}
|
|
48
|
+
close() {
|
|
49
|
+
if (this.closed)
|
|
50
|
+
return;
|
|
51
|
+
this.closed = true;
|
|
52
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
53
|
+
waiter({ value: undefined, done: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
next() {
|
|
57
|
+
if (this.size > 0) {
|
|
58
|
+
const item = this.items[this.head];
|
|
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 });
|
|
63
|
+
}
|
|
64
|
+
if (this.closed) {
|
|
65
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
66
|
+
}
|
|
67
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
68
|
+
}
|
|
69
|
+
[Symbol.asyncIterator]() {
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.AsyncEventQueue = AsyncEventQueue;
|
package/dist/grpc/client.d.ts
CHANGED
|
@@ -3,13 +3,25 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { CommitmentLevel } from "@triton-one/yellowstone-grpc";
|
|
5
5
|
import type { DexEvent } from "../core/dex_event.js";
|
|
6
|
+
import { type EventQueueOverflowStrategy } from "./async_event_queue.js";
|
|
6
7
|
import { type AccountFilter, type ClientConfig, type EventTypeFilter, type TransactionFilter, type SubscribeCallbacks } from "./types.js";
|
|
7
8
|
export { SubscribeCallbacks };
|
|
9
|
+
export type { EventQueueOverflowStrategy } from "./async_event_queue.js";
|
|
8
10
|
export interface DexEventSubscription extends AsyncIterable<DexEvent> {
|
|
9
11
|
id: string;
|
|
10
12
|
errors: AsyncIterable<Error>;
|
|
11
13
|
cancel(): void;
|
|
12
14
|
next(): Promise<IteratorResult<DexEvent>>;
|
|
15
|
+
len(): number;
|
|
16
|
+
dropped(): number;
|
|
17
|
+
}
|
|
18
|
+
export interface SubscribeDexEventsOptions {
|
|
19
|
+
autoReconnect?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Queue behavior when an async-iterator consumer cannot keep up. The default
|
|
22
|
+
* preserves the existing behavior; `drop-oldest` favors staying near chain tip.
|
|
23
|
+
*/
|
|
24
|
+
queueOverflowStrategy?: EventQueueOverflowStrategy;
|
|
13
25
|
}
|
|
14
26
|
/** Yellowstone gRPC 客户端包装器 */
|
|
15
27
|
export declare class YellowstoneGrpc {
|
|
@@ -41,6 +53,7 @@ export declare class YellowstoneGrpc {
|
|
|
41
53
|
private static protobufNumber;
|
|
42
54
|
private static timestampToMicros;
|
|
43
55
|
private parseAccountEvent;
|
|
56
|
+
private parseAccountEventRaw;
|
|
44
57
|
private initialSubscribeRequest;
|
|
45
58
|
/** 订阅交易 */
|
|
46
59
|
subscribeTransactions(filter: TransactionFilter, callbacks: SubscribeCallbacks): Promise<{
|
|
@@ -51,11 +64,9 @@ export declare class YellowstoneGrpc {
|
|
|
51
64
|
* 订阅并直接产出 `DexEvent`。
|
|
52
65
|
*
|
|
53
66
|
* 交易更新走统一交易解析(外层/内层指令 + 日志 + 数据填充);账户更新走 `parseAccountUnified`。
|
|
54
|
-
*
|
|
67
|
+
* 事件队列满时按配置丢弃事件,避免回调阻塞 gRPC 读循环并让丢弃可观测。
|
|
55
68
|
*/
|
|
56
|
-
subscribeDexEvents(transactionFilters?: TransactionFilter[], accountFilters?: AccountFilter[], eventTypeFilter?: EventTypeFilter, options?:
|
|
57
|
-
autoReconnect?: boolean;
|
|
58
|
-
}): Promise<DexEventSubscription>;
|
|
69
|
+
subscribeDexEvents(transactionFilters?: TransactionFilter[], accountFilters?: AccountFilter[], eventTypeFilter?: EventTypeFilter, options?: SubscribeDexEventsOptions): Promise<DexEventSubscription>;
|
|
59
70
|
/** 取消订阅 */
|
|
60
71
|
unsubscribe(subId: string): void;
|
|
61
72
|
/** 动态更新当前 DEX 订阅过滤器(与 Rust update_subscription 语义对齐)。 */
|
package/dist/grpc/client.js
CHANGED
|
@@ -45,51 +45,18 @@ const bs58_1 = __importDefault(require("bs58"));
|
|
|
45
45
|
const metadata_js_1 = require("../core/metadata.js");
|
|
46
46
|
const mod_js_1 = require("../accounts/mod.js");
|
|
47
47
|
const geyser_connect_js_1 = require("./geyser_connect.js");
|
|
48
|
+
const async_event_queue_js_1 = require("./async_event_queue.js");
|
|
48
49
|
const order_buffer_js_1 = require("./order_buffer.js");
|
|
49
50
|
const subscribe_builder_js_1 = require("./subscribe_builder.js");
|
|
50
51
|
const yellowstone_parse_js_1 = require("./yellowstone_parse.js");
|
|
51
52
|
const types_js_1 = require("./types.js");
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
push(item) {
|
|
61
|
-
if (this.closed)
|
|
62
|
-
return;
|
|
63
|
-
const waiter = this.waiters.shift();
|
|
64
|
-
if (waiter) {
|
|
65
|
-
waiter({ value: item, done: false });
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (this.items.length < this.maxSize) {
|
|
69
|
-
this.items.push(item);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
close() {
|
|
73
|
-
if (this.closed)
|
|
74
|
-
return;
|
|
75
|
-
this.closed = true;
|
|
76
|
-
for (const waiter of this.waiters.splice(0)) {
|
|
77
|
-
waiter({ value: undefined, done: true });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
next() {
|
|
81
|
-
const item = this.items.shift();
|
|
82
|
-
if (item !== undefined) {
|
|
83
|
-
return Promise.resolve({ value: item, done: false });
|
|
84
|
-
}
|
|
85
|
-
if (this.closed) {
|
|
86
|
-
return Promise.resolve({ value: undefined, done: true });
|
|
87
|
-
}
|
|
88
|
-
return new Promise((resolve) => this.waiters.push(resolve));
|
|
89
|
-
}
|
|
90
|
-
[Symbol.asyncIterator]() {
|
|
91
|
-
return this;
|
|
92
|
-
}
|
|
53
|
+
function cancelSubscribeStream(stream) {
|
|
54
|
+
if (!stream)
|
|
55
|
+
return;
|
|
56
|
+
// grpc-js emits CANCELLED through `error`; guard the short setup window before
|
|
57
|
+
// the normal lifecycle listeners have been installed.
|
|
58
|
+
stream.once("error", () => { });
|
|
59
|
+
stream.cancel();
|
|
93
60
|
}
|
|
94
61
|
/** Yellowstone gRPC 客户端包装器 */
|
|
95
62
|
class YellowstoneGrpc {
|
|
@@ -105,6 +72,9 @@ class YellowstoneGrpc {
|
|
|
105
72
|
...gc,
|
|
106
73
|
keepAliveIntervalMs: config.keep_alive_interval_ms,
|
|
107
74
|
keepAliveTimeoutMs: config.keep_alive_timeout_ms,
|
|
75
|
+
initialReconnectBackoffMs: config.retry_delay_ms,
|
|
76
|
+
maxReconnectBackoffMs: 60_000,
|
|
77
|
+
flowControlWindowBytes: config.flow_control_window_bytes,
|
|
108
78
|
}));
|
|
109
79
|
}
|
|
110
80
|
/** 连接到 gRPC 服务器 */
|
|
@@ -302,6 +272,22 @@ class YellowstoneGrpc {
|
|
|
302
272
|
const metadata = (0, metadata_js_1.makeMetadata)(signatureBytes.length > 0 ? bs58_1.default.encode(signatureBytes) : "", Number(update.slot), 0, blockTimeUs, grpcRecvUs);
|
|
303
273
|
return (0, mod_js_1.parseAccountUnified)(account, metadata, eventTypeFilter);
|
|
304
274
|
}
|
|
275
|
+
parseAccountEventRaw(update, grpcRecvUs, eventTypeFilter, blockTimeUs) {
|
|
276
|
+
const acc = update.account;
|
|
277
|
+
if (!acc)
|
|
278
|
+
return null;
|
|
279
|
+
const signatureBytes = YellowstoneGrpc.toBytes(acc.txnSignature);
|
|
280
|
+
const account = {
|
|
281
|
+
pubkey: bs58_1.default.encode(YellowstoneGrpc.toBytes(acc.pubkey)),
|
|
282
|
+
executable: acc.executable,
|
|
283
|
+
lamports: YellowstoneGrpc.toBigInt(acc.lamports),
|
|
284
|
+
owner: bs58_1.default.encode(YellowstoneGrpc.toBytes(acc.owner)),
|
|
285
|
+
rent_epoch: YellowstoneGrpc.toBigInt(acc.rentEpoch),
|
|
286
|
+
data: YellowstoneGrpc.toBytes(acc.data),
|
|
287
|
+
};
|
|
288
|
+
const metadata = (0, metadata_js_1.makeMetadata)(signatureBytes.length > 0 ? bs58_1.default.encode(signatureBytes) : "", YellowstoneGrpc.protobufNumber(update.slot), 0, blockTimeUs, grpcRecvUs);
|
|
289
|
+
return (0, mod_js_1.parseAccountUnified)(account, metadata, eventTypeFilter);
|
|
290
|
+
}
|
|
305
291
|
initialSubscribeRequest(filter) {
|
|
306
292
|
return {
|
|
307
293
|
transactions: {
|
|
@@ -332,7 +318,7 @@ class YellowstoneGrpc {
|
|
|
332
318
|
const streamHolder = { current: null };
|
|
333
319
|
const cancel = () => {
|
|
334
320
|
isCancelled = true;
|
|
335
|
-
streamHolder.current
|
|
321
|
+
cancelSubscribeStream(streamHolder.current);
|
|
336
322
|
streamHolder.current = null;
|
|
337
323
|
};
|
|
338
324
|
this.subscribers.set(id, { filter, callbacks, cancel });
|
|
@@ -344,6 +330,10 @@ class YellowstoneGrpc {
|
|
|
344
330
|
while (!isCancelled) {
|
|
345
331
|
try {
|
|
346
332
|
const stream = await this.client.subscribe();
|
|
333
|
+
if (isCancelled) {
|
|
334
|
+
cancelSubscribeStream(stream);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
347
337
|
streamHolder.current = stream;
|
|
348
338
|
await new Promise((resolve, reject) => {
|
|
349
339
|
stream.write(this.initialSubscribeRequest(filter), (err) => {
|
|
@@ -353,7 +343,6 @@ class YellowstoneGrpc {
|
|
|
353
343
|
resolve();
|
|
354
344
|
});
|
|
355
345
|
});
|
|
356
|
-
backoffMs = this.config.retry_delay_ms;
|
|
357
346
|
await new Promise((resolve, reject) => {
|
|
358
347
|
const cleanup = () => {
|
|
359
348
|
stream.removeAllListeners();
|
|
@@ -364,6 +353,7 @@ class YellowstoneGrpc {
|
|
|
364
353
|
stream.on("data", (update) => {
|
|
365
354
|
if (isCancelled)
|
|
366
355
|
return;
|
|
356
|
+
backoffMs = this.config.retry_delay_ms;
|
|
367
357
|
if (update.ping) {
|
|
368
358
|
stream.write(this.subscribePingPongRequest(), (werr) => {
|
|
369
359
|
if (werr && callbacks.onError && !isCancelled) {
|
|
@@ -396,6 +386,8 @@ class YellowstoneGrpc {
|
|
|
396
386
|
catch (err) {
|
|
397
387
|
if (isCancelled)
|
|
398
388
|
break;
|
|
389
|
+
cancelSubscribeStream(streamHolder.current);
|
|
390
|
+
streamHolder.current = null;
|
|
399
391
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
400
392
|
callbacks.onError?.(e);
|
|
401
393
|
if (!autoReconnect)
|
|
@@ -415,7 +407,7 @@ class YellowstoneGrpc {
|
|
|
415
407
|
}
|
|
416
408
|
}
|
|
417
409
|
finally {
|
|
418
|
-
streamHolder.current
|
|
410
|
+
cancelSubscribeStream(streamHolder.current);
|
|
419
411
|
streamHolder.current = null;
|
|
420
412
|
this.subscribers.delete(id);
|
|
421
413
|
}
|
|
@@ -432,15 +424,26 @@ class YellowstoneGrpc {
|
|
|
432
424
|
* 订阅并直接产出 `DexEvent`。
|
|
433
425
|
*
|
|
434
426
|
* 交易更新走统一交易解析(外层/内层指令 + 日志 + 数据填充);账户更新走 `parseAccountUnified`。
|
|
435
|
-
*
|
|
427
|
+
* 事件队列满时按配置丢弃事件,避免回调阻塞 gRPC 读循环并让丢弃可观测。
|
|
436
428
|
*/
|
|
437
429
|
async subscribeDexEvents(transactionFilters = [], accountFilters = [], eventTypeFilter, options) {
|
|
438
430
|
await this.connect();
|
|
439
431
|
const id = `dex_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
440
|
-
const events = new AsyncEventQueue(Math.max(1, this.config.buffer_size));
|
|
441
|
-
const errors = new AsyncEventQueue(Math.max(1, this.config.buffer_size));
|
|
432
|
+
const events = new async_event_queue_js_1.AsyncEventQueue(Math.max(1, this.config.buffer_size), options?.queueOverflowStrategy);
|
|
433
|
+
const errors = new async_event_queue_js_1.AsyncEventQueue(Math.max(1, this.config.buffer_size));
|
|
442
434
|
const order = new order_buffer_js_1.OrderDispatcher(this.config);
|
|
443
|
-
|
|
435
|
+
let nextDropWarningAt = 0;
|
|
436
|
+
const reportError = (error) => errors.push(error);
|
|
437
|
+
const emitEvent = (event) => {
|
|
438
|
+
if (events.push(event))
|
|
439
|
+
return;
|
|
440
|
+
const now = Date.now();
|
|
441
|
+
if (now < nextDropWarningAt)
|
|
442
|
+
return;
|
|
443
|
+
nextDropWarningAt = now + 10_000;
|
|
444
|
+
reportError(new Error(`Dex event queue full; dropped=${events.dropped()} buffer_size=${this.config.buffer_size} ` +
|
|
445
|
+
`strategy=${options?.queueOverflowStrategy ?? "drop-newest"}`));
|
|
446
|
+
};
|
|
444
447
|
let flushTimer;
|
|
445
448
|
if (order.needsTimer) {
|
|
446
449
|
const intervalMs = this.config.order_mode === "MicroBatch"
|
|
@@ -452,9 +455,27 @@ class YellowstoneGrpc {
|
|
|
452
455
|
let currentTransactionFilters = transactionFilters;
|
|
453
456
|
let currentAccountFilters = accountFilters;
|
|
454
457
|
const streamHolder = { current: null };
|
|
458
|
+
let cancelReconnectDelay;
|
|
459
|
+
const waitBeforeReconnect = (delayMs) => new Promise((resolve) => {
|
|
460
|
+
let settled = false;
|
|
461
|
+
const finish = () => {
|
|
462
|
+
if (settled)
|
|
463
|
+
return;
|
|
464
|
+
settled = true;
|
|
465
|
+
clearTimeout(timer);
|
|
466
|
+
if (cancelReconnectDelay === finish)
|
|
467
|
+
cancelReconnectDelay = undefined;
|
|
468
|
+
resolve();
|
|
469
|
+
};
|
|
470
|
+
const timer = setTimeout(finish, delayMs);
|
|
471
|
+
cancelReconnectDelay = finish;
|
|
472
|
+
if (isCancelled)
|
|
473
|
+
finish();
|
|
474
|
+
});
|
|
455
475
|
const cancel = () => {
|
|
456
476
|
isCancelled = true;
|
|
457
|
-
|
|
477
|
+
cancelReconnectDelay?.();
|
|
478
|
+
cancelSubscribeStream(streamHolder.current);
|
|
458
479
|
streamHolder.current = null;
|
|
459
480
|
if (flushTimer)
|
|
460
481
|
clearInterval(flushTimer);
|
|
@@ -485,6 +506,10 @@ class YellowstoneGrpc {
|
|
|
485
506
|
while (!isCancelled) {
|
|
486
507
|
try {
|
|
487
508
|
const stream = await this.client.subscribe();
|
|
509
|
+
if (isCancelled) {
|
|
510
|
+
cancelSubscribeStream(stream);
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
488
513
|
streamHolder.current = stream;
|
|
489
514
|
await new Promise((resolve, reject) => {
|
|
490
515
|
stream.write((0, subscribe_builder_js_1.buildSubscribeRequest)(currentTransactionFilters, currentAccountFilters), (err) => {
|
|
@@ -494,7 +519,6 @@ class YellowstoneGrpc {
|
|
|
494
519
|
resolve();
|
|
495
520
|
});
|
|
496
521
|
});
|
|
497
|
-
backoffMs = this.config.retry_delay_ms;
|
|
498
522
|
await new Promise((resolve, reject) => {
|
|
499
523
|
const cleanup = () => {
|
|
500
524
|
stream.removeAllListeners();
|
|
@@ -505,33 +529,44 @@ class YellowstoneGrpc {
|
|
|
505
529
|
stream.on("data", (update) => {
|
|
506
530
|
if (isCancelled)
|
|
507
531
|
return;
|
|
508
|
-
|
|
532
|
+
backoffMs = this.config.retry_delay_ms;
|
|
533
|
+
if (order.needsTimer)
|
|
534
|
+
order.flushDue(emitEvent);
|
|
509
535
|
if (update.ping) {
|
|
510
536
|
stream.write(this.subscribePingPongRequest(), (werr) => {
|
|
511
537
|
if (werr && !isCancelled) {
|
|
512
|
-
|
|
538
|
+
reportError(werr instanceof Error ? werr : new Error(String(werr)));
|
|
513
539
|
}
|
|
514
540
|
});
|
|
515
541
|
return;
|
|
516
542
|
}
|
|
517
543
|
try {
|
|
518
544
|
const grpcRecvUs = Math.floor(Date.now() * 1000);
|
|
519
|
-
const
|
|
520
|
-
const
|
|
545
|
+
const blockTimeUs = YellowstoneGrpc.timestampToMicros(update.createdAt ?? update.created_at);
|
|
546
|
+
const txUpdate = update.transaction;
|
|
547
|
+
const tx = txUpdate?.transaction;
|
|
521
548
|
if (tx) {
|
|
522
|
-
const
|
|
549
|
+
const slot = txUpdate?.slot ?? 0n;
|
|
550
|
+
const fallbackSlot = YellowstoneGrpc.protobufNumber(slot);
|
|
523
551
|
const fallbackTxIndex = YellowstoneGrpc.protobufNumber(tx.index ?? 0);
|
|
524
|
-
const
|
|
552
|
+
const txInfo = {
|
|
553
|
+
signature: tx.signature,
|
|
554
|
+
isVote: tx.isVote,
|
|
555
|
+
transactionRaw: tx.transaction,
|
|
556
|
+
metaRaw: tx.meta,
|
|
557
|
+
index: (tx.index ?? 0n),
|
|
558
|
+
};
|
|
559
|
+
const txEvents = (0, yellowstone_parse_js_1.parseDexEventsFromGrpcTransactionInfo)(txInfo, slot, { blockTimeUs, grpcRecvUs, eventTypeFilter });
|
|
525
560
|
order.pushTransactionEvents(txEvents, fallbackSlot, fallbackTxIndex, emitEvent);
|
|
526
561
|
}
|
|
527
|
-
if (
|
|
528
|
-
const ev = this.
|
|
562
|
+
if (update.account) {
|
|
563
|
+
const ev = this.parseAccountEventRaw(update.account, grpcRecvUs, eventTypeFilter, blockTimeUs);
|
|
529
564
|
if (ev)
|
|
530
565
|
emitEvent(ev);
|
|
531
566
|
}
|
|
532
567
|
}
|
|
533
568
|
catch (err) {
|
|
534
|
-
|
|
569
|
+
reportError(err instanceof Error ? err : new Error(String(err)));
|
|
535
570
|
}
|
|
536
571
|
});
|
|
537
572
|
stream.on("error", (err) => {
|
|
@@ -547,25 +582,28 @@ class YellowstoneGrpc {
|
|
|
547
582
|
catch (err) {
|
|
548
583
|
if (isCancelled)
|
|
549
584
|
break;
|
|
550
|
-
|
|
585
|
+
cancelSubscribeStream(streamHolder.current);
|
|
586
|
+
streamHolder.current = null;
|
|
587
|
+
reportError(err instanceof Error ? err : new Error(String(err)));
|
|
551
588
|
if (!autoReconnect)
|
|
552
589
|
break;
|
|
553
|
-
await
|
|
590
|
+
await waitBeforeReconnect(backoffMs);
|
|
554
591
|
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
555
592
|
continue;
|
|
556
593
|
}
|
|
557
594
|
if (isCancelled || !autoReconnect)
|
|
558
595
|
break;
|
|
559
|
-
await
|
|
596
|
+
await waitBeforeReconnect(backoffMs);
|
|
560
597
|
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
561
598
|
}
|
|
562
599
|
}
|
|
563
600
|
finally {
|
|
564
|
-
streamHolder.current
|
|
601
|
+
cancelSubscribeStream(streamHolder.current);
|
|
565
602
|
streamHolder.current = null;
|
|
566
603
|
if (flushTimer)
|
|
567
604
|
clearInterval(flushTimer);
|
|
568
|
-
|
|
605
|
+
if (!isCancelled)
|
|
606
|
+
order.flushAll(emitEvent);
|
|
569
607
|
events.close();
|
|
570
608
|
errors.close();
|
|
571
609
|
this.dexSubscribers.delete(id);
|
|
@@ -18,8 +18,14 @@ export interface GeyserConnectConfig {
|
|
|
18
18
|
keepAliveIntervalMs?: number;
|
|
19
19
|
/** gRPC keepalive 应答超时(毫秒) */
|
|
20
20
|
keepAliveTimeoutMs?: number;
|
|
21
|
+
/** gRPC 连接初始重试退避(毫秒) */
|
|
22
|
+
initialReconnectBackoffMs?: number;
|
|
23
|
+
/** gRPC 连接最大重试退避(毫秒) */
|
|
24
|
+
maxReconnectBackoffMs?: number;
|
|
25
|
+
/** HTTP/2 本地接收窗口(字节) */
|
|
26
|
+
flowControlWindowBytes?: number;
|
|
21
27
|
}
|
|
22
|
-
/**
|
|
28
|
+
/** 公共字段与 Rust `GeyserConnectConfig::default` 一致,并包含 Node gRPC 通道默认值。 */
|
|
23
29
|
export declare function defaultGeyserConnectConfig(): GeyserConnectConfig;
|
|
24
30
|
/** 与 `ClientConfig` / `grpc.keepalive_*` 对齐,用于长连接抗 NAT/负载均衡 idle 断开 */
|
|
25
31
|
export declare function geyserGrpcChannelOptions(config?: GeyserConnectConfig): ChannelOptions;
|