sol-parser-sdk 0.5.17 → 0.5.19

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.
Files changed (64) hide show
  1. package/README.md +46 -31
  2. package/README_CN.md +45 -30
  3. package/dist/accounts/pumpfun.js +15 -0
  4. package/dist/accounts/pumpswap.js +2 -0
  5. package/dist/core/account_dispatcher_rpc.d.ts +2 -1
  6. package/dist/core/account_dispatcher_rpc.js +94 -9
  7. package/dist/core/account_fill_pumpfun.js +20 -0
  8. package/dist/core/account_fill_pumpswap.d.ts +2 -0
  9. package/dist/core/account_fill_pumpswap.js +55 -6
  10. package/dist/core/account_fill_raydium.js +4 -0
  11. package/dist/core/dex_event.d.ts +149 -0
  12. package/dist/grpc/client.js +2 -0
  13. package/dist/grpc/log_instr_dedup.js +75 -7
  14. package/dist/grpc/order_buffer.d.ts +5 -0
  15. package/dist/grpc/order_buffer.js +53 -17
  16. package/dist/grpc/types.d.ts +4 -1
  17. package/dist/grpc/types.js +23 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/instr/inner.js +28 -1
  21. package/dist/instr/meteora_damm_ix.js +26 -0
  22. package/dist/instr/meteora_pools_ix.js +17 -9
  23. package/dist/instr/orca_whirlpool_ix.js +13 -9
  24. package/dist/instr/pumpfun_ix.js +84 -0
  25. package/dist/instr/pumpswap_ix.js +85 -10
  26. package/dist/instr/raydium_clmm_ix.js +5 -3
  27. package/dist/instr/raydium_cpmm_ix.js +11 -8
  28. package/dist/logs/meteora_damm.d.ts +7 -0
  29. package/dist/logs/meteora_damm.js +211 -0
  30. package/dist/logs/meteora_dbc.d.ts +3 -0
  31. package/dist/logs/meteora_dbc.js +23 -0
  32. package/dist/logs/meteora_dlmm.js +16 -4
  33. package/dist/logs/optimized_matcher.js +93 -10
  34. package/dist/logs/program_log_discriminators.d.ts +11 -0
  35. package/dist/logs/program_log_discriminators.js +11 -0
  36. package/dist/logs/pump.js +6 -3
  37. package/dist/logs/pump_amm.js +8 -2
  38. package/dist/logs/pump_upgrade.d.ts +68 -0
  39. package/dist/logs/pump_upgrade.js +128 -0
  40. package/dist/logs/raydium_cpmm.js +7 -3
  41. package/dist/rpc_transaction.js +84 -7
  42. package/dist/shredstream/alt_cache.d.ts +26 -0
  43. package/dist/shredstream/alt_cache.js +138 -0
  44. package/dist/shredstream/client.d.ts +11 -3
  45. package/dist/shredstream/client.js +76 -72
  46. package/dist/shredstream/config.d.ts +7 -3
  47. package/dist/shredstream/index.d.ts +3 -2
  48. package/dist/shredstream/index.js +5 -3
  49. package/dist/shredstream/instruction_parse.d.ts +1 -1
  50. package/dist/shredstream/instruction_parse.js +1 -0
  51. package/dist/simulation_route.d.ts +1 -1
  52. package/dist/simulation_route.js +17 -4
  53. package/dist/transaction_route.js +20 -1
  54. package/dist/wire_transaction.js +9 -10
  55. package/docs/USAGE.md +161 -0
  56. package/package.json +2 -5
  57. package/NATIVE_ALIGNMENT.md +0 -312
  58. package/NATIVE_DAMM_FACTORY_ALIGNMENT_20261004.md +0 -16
  59. package/NATIVE_DAMM_FACTORY_EVIDENCE_20261004.json +0 -55
  60. package/NATIVE_MIGRATION.md +0 -38
  61. package/NATIVE_PUMPFUN_NATIVE_DEBIT_ALIGNMENT_20261004.md +0 -18
  62. package/NATIVE_PUMPFUN_NATIVE_DEBIT_EVIDENCE_20261004.json +0 -57
  63. package/NATIVE_PUMPFUN_SETTLEMENT_ALIGNMENT_20261004.md +0 -11
  64. package/NATIVE_PUMPFUN_SETTLEMENT_EVIDENCE_20261004.json +0 -70
@@ -41,6 +41,7 @@ const SUPPORTED_INSTRUCTION_PROGRAM_IDS = new Set([
41
41
  program_ids_js_1.RAYDIUM_AMM_V4_PROGRAM_ID,
42
42
  program_ids_js_1.ORCA_WHIRLPOOL_PROGRAM_ID,
43
43
  program_ids_js_1.METEORA_POOLS_PROGRAM_ID,
44
+ program_ids_js_1.METEORA_DBC_PROGRAM_ID,
44
45
  program_ids_js_1.METEORA_DAMM_V2_PROGRAM_ID,
45
46
  program_ids_js_1.METEORA_DLMM_PROGRAM_ID,
46
47
  ]);
@@ -51,8 +52,8 @@ function bytesEqualAt(data, expected, offset) {
51
52
  }
52
53
  return true;
53
54
  }
54
- function isDlmmEventCpi(programId, data) {
55
- return programId === program_ids_js_1.METEORA_DLMM_PROGRAM_ID && data.length >= 16 && (bytesEqualAt(data, EVENT_CPI_PREFIX, 0) ||
55
+ function isEventCpi(programId, data) {
56
+ return [program_ids_js_1.METEORA_DLMM_PROGRAM_ID, program_ids_js_1.PUMPFUN_PROGRAM_ID, program_ids_js_1.PUMPSWAP_PROGRAM_ID].includes(programId) && data.length >= 16 && (bytesEqualAt(data, EVENT_CPI_PREFIX, 0) ||
56
57
  bytesEqualAt(data, LEGACY_EVENT_CPI_SUFFIX, 8));
57
58
  }
58
59
  function isDlmmEvent(event) {
@@ -273,6 +274,9 @@ function mergeDlmmInstruction(baseName, base, inner) {
273
274
  if (context)
274
275
  Object.assign(base, context);
275
276
  }
277
+ function pumpIdentityKeyConflicts(a, b) {
278
+ return !isDefaultValue(a) && !isDefaultValue(b) && a !== b;
279
+ }
276
280
  function mergeInstructionEvent(base, inner) {
277
281
  const baseName = eventName(base);
278
282
  const innerName = eventName(inner);
@@ -281,6 +285,8 @@ function mergeInstructionEvent(base, inner) {
281
285
  if (!baseData || !innerData)
282
286
  return false;
283
287
  if (PUMPFUN_TRADE_EVENT_NAMES.has(baseName) && PUMPFUN_TRADE_EVENT_NAMES.has(innerName)) {
288
+ if (baseData.is_buy !== innerData.is_buy || pumpIdentityKeyConflicts(baseData.mint, innerData.mint) || pumpIdentityKeyConflicts(baseData.user, innerData.user))
289
+ return false;
284
290
  mergePumpfunTradeInstruction(baseData, innerData);
285
291
  return true;
286
292
  }
@@ -290,6 +296,8 @@ function mergeInstructionEvent(base, inner) {
290
296
  }
291
297
  if ((baseName === "PumpSwapBuy" && innerName === "PumpSwapBuy") ||
292
298
  (baseName === "PumpSwapSell" && innerName === "PumpSwapSell")) {
299
+ if (pumpIdentityKeyConflicts(baseData.pool, innerData.pool) || pumpIdentityKeyConflicts(baseData.user, innerData.user))
300
+ return false;
293
301
  mergePumpSwapBuySellInstruction(baseData, innerData);
294
302
  return true;
295
303
  }
@@ -304,6 +312,10 @@ function mergeInstructionEvent(base, inner) {
304
312
  }
305
313
  return false;
306
314
  }
315
+ function isPumpTrade(event) {
316
+ const name = eventName(event);
317
+ return PUMPFUN_TRADE_EVENT_NAMES.has(name) || name === "PumpSwapBuy" || name === "PumpSwapSell";
318
+ }
307
319
  function mergeInstructionEvents(events) {
308
320
  if (events.length === 0)
309
321
  return [];
@@ -317,11 +329,15 @@ function mergeInstructionEvents(events) {
317
329
  const out = [];
318
330
  let outerTarget = null;
319
331
  const dlmmTargets = [];
332
+ const pumpTargets = [];
320
333
  for (const item of events) {
321
334
  if (item.innerIdx === null) {
322
335
  out.push(item.event);
323
336
  const resultIdx = out.length - 1;
324
337
  outerTarget = { outerIdx: item.outerIdx, resultIdx };
338
+ pumpTargets.length = 0;
339
+ if (isPumpTrade(item.event))
340
+ pumpTargets.push({ outerIdx: item.outerIdx, stackHeight: item.stackHeight, resultIdx, consumed: false });
325
341
  dlmmTargets.length = 0;
326
342
  if (isDlmmEvent(item.event)) {
327
343
  dlmmTargets.push({
@@ -332,7 +348,40 @@ function mergeInstructionEvents(events) {
332
348
  }
333
349
  continue;
334
350
  }
335
- if (item.isDlmmEventCpi) {
351
+ if (isPumpTrade(item.event) && (item.isEventCpi || !PUMPFUN_TRADE_EVENT_NAMES.has(eventName(item.event)) || eventPayload(item.event)?.ix_name)) {
352
+ if (item.isEventCpi) {
353
+ let target;
354
+ for (let i = pumpTargets.length - 1; i >= 0; i--) {
355
+ const candidate = pumpTargets[i];
356
+ if (candidate.outerIdx === item.outerIdx && (candidate.stackHeight === undefined || item.stackHeight === undefined || item.stackHeight === candidate.stackHeight + 1)) {
357
+ target = candidate;
358
+ break;
359
+ }
360
+ }
361
+ if (target && !target.consumed) {
362
+ target.consumed = true;
363
+ if (mergeInstructionEvent(out[target.resultIdx], item.event))
364
+ continue;
365
+ }
366
+ out.push(item.event);
367
+ }
368
+ else {
369
+ if (item.stackHeight === undefined)
370
+ pumpTargets.length = 0;
371
+ else
372
+ while (pumpTargets.length) {
373
+ const last = pumpTargets[pumpTargets.length - 1];
374
+ if (last.outerIdx !== item.outerIdx || (last.stackHeight !== undefined && last.stackHeight >= item.stackHeight))
375
+ pumpTargets.pop();
376
+ else
377
+ break;
378
+ }
379
+ out.push(item.event);
380
+ pumpTargets.push({ outerIdx: item.outerIdx, stackHeight: item.stackHeight, resultIdx: out.length - 1, consumed: false });
381
+ }
382
+ continue;
383
+ }
384
+ if (item.isEventCpi && isDlmmEvent(item.event)) {
336
385
  let merged = false;
337
386
  for (let i = dlmmTargets.length - 1; i >= 0; i--) {
338
387
  const target = dlmmTargets[i];
@@ -403,7 +452,7 @@ function parseOuterAndInnerInstructions(message, meta, signature, slot, txIndex,
403
452
  outerIdx,
404
453
  innerIdx: null,
405
454
  stackHeight: 1,
406
- isDlmmEventCpi: false,
455
+ isEventCpi: false,
407
456
  event: ev,
408
457
  });
409
458
  }
@@ -426,7 +475,7 @@ function parseOuterAndInnerInstructions(message, meta, signature, slot, txIndex,
426
475
  outerIdx: group.index,
427
476
  innerIdx,
428
477
  stackHeight: ix.stackHeight,
429
- isDlmmEventCpi: isDlmmEventCpi(programId, data),
478
+ isEventCpi: isEventCpi(programId, data),
430
479
  event: ev,
431
480
  });
432
481
  }
@@ -441,8 +490,36 @@ function recentBlockhashBytes(recentBlockhash) {
441
490
  return undefined;
442
491
  }
443
492
  }
444
- function detectPumpfunCreateInLogs(logMessages, _signature, _slot, _txIndex, _blockTimeUs, _grpcRecvUs, _recentBlockhash) {
445
- return logMessages.some((log) => log.includes("Program data: G3KpTd7rY3Y"));
493
+ function detectPumpfunCreateInLogs(logMessages, signature, slot, txIndex, blockTimeUs, grpcRecvUs, recentBlockhash) {
494
+ const stack = [];
495
+ for (const log of logMessages) {
496
+ const invoke = (0, optimized_matcher_js_1.parseInvokeInfo)(log);
497
+ if (invoke && log === `Program ${invoke.programId} invoke [${invoke.depth}]`) {
498
+ stack.length = Math.min(stack.length, invoke.depth - 1);
499
+ stack.push(invoke.programId);
500
+ continue;
501
+ }
502
+ // The last base64 character of the discriminator also contains payload bits.
503
+ if (stack[stack.length - 1] === program_ids_js_1.PUMPFUN_PROGRAM_ID &&
504
+ log.startsWith("Program data: G3KpTd7rY3")) {
505
+ const encoded = log.slice("Program data: ".length);
506
+ if (Buffer.from(encoded, "base64").toString("base64") === encoded) {
507
+ // Ignore the caller's output filter: a real create still classifies buys
508
+ // when Create events are excluded. Decode only rare create candidates.
509
+ const event = (0, optimized_matcher_js_1.parseLogOptimizedWithProgramId)(log, signature, slot, txIndex, blockTimeUs, grpcRecvUs, undefined, false, recentBlockhash, program_ids_js_1.PUMPFUN_PROGRAM_ID);
510
+ if (event && ("PumpFunCreate" in event || "PumpFunCreateV2" in event))
511
+ return true;
512
+ }
513
+ }
514
+ const completed = (0, optimized_matcher_js_1.parseProgramCompleteInfo)(log);
515
+ if (completed && (log === `Program ${completed} success` ||
516
+ log.startsWith(`Program ${completed} failed: `))) {
517
+ const index = stack.lastIndexOf(completed);
518
+ if (index >= 0)
519
+ stack.length = index;
520
+ }
521
+ }
522
+ return false;
446
523
  }
447
524
  function applyRpcFills(events, msg, meta) {
448
525
  const resolver = (0, rpc_invoke_map_js_1.getAccountKeyResolver)(msg, meta);
@@ -0,0 +1,26 @@
1
+ import type { AddressLookupTableAccount, Connection } from "@solana/web3.js";
2
+ import type { ShredWasmTx } from "./instruction_parse.js";
3
+ /** Bounded ALT cache. resolve() only reads memory; its misses are refreshed by a timer. */
4
+ export declare class AddressLookupTableCache {
5
+ private readonly connection?;
6
+ private readonly refreshIntervalMs;
7
+ private static readonly CAPACITY;
8
+ private readonly tables;
9
+ private readonly pending;
10
+ private timer?;
11
+ private refreshing;
12
+ private generation;
13
+ private refreshOffset;
14
+ private failures;
15
+ constructor(connection?: Connection | undefined, initial?: readonly AddressLookupTableAccount[], refreshIntervalMs?: number);
16
+ get refreshFailures(): number;
17
+ private store;
18
+ private schedule;
19
+ /** null means this transaction is unresolved; callers must skip it, never emit guessed keys. */
20
+ resolve(tx: ShredWasmTx): string[] | null;
21
+ /** Explicit pre-subscription network operation, batched to the RPC's 100-account limit. */
22
+ preload(addresses: readonly string[]): Promise<void>;
23
+ start(): void;
24
+ stop(): void;
25
+ private refresh;
26
+ }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AddressLookupTableCache = void 0;
4
+ const alt_lookup_js_1 = require("./alt_lookup.js");
5
+ /** Bounded ALT cache. resolve() only reads memory; its misses are refreshed by a timer. */
6
+ class AddressLookupTableCache {
7
+ connection;
8
+ refreshIntervalMs;
9
+ static CAPACITY = 2048;
10
+ tables = new Map();
11
+ pending = new Set();
12
+ timer;
13
+ refreshing = false;
14
+ generation = 0;
15
+ refreshOffset = 0;
16
+ failures = 0;
17
+ constructor(connection, initial = [], refreshIntervalMs = 1000) {
18
+ this.connection = connection;
19
+ this.refreshIntervalMs = refreshIntervalMs;
20
+ if (!Number.isFinite(refreshIntervalMs) || refreshIntervalMs < 1) {
21
+ throw new RangeError("alt_refresh_interval_ms must be positive");
22
+ }
23
+ this.store(initial);
24
+ }
25
+ get refreshFailures() { return this.failures; }
26
+ store(tables) {
27
+ for (const table of tables) {
28
+ const key = table.key.toBase58();
29
+ // Encode each loaded pubkey once during refresh, never per transaction.
30
+ this.tables.set(key, table.state.addresses.map(address => address.toBase58()));
31
+ this.pending.delete(key);
32
+ if (this.tables.size > AddressLookupTableCache.CAPACITY) {
33
+ this.tables.delete(this.tables.keys().next().value);
34
+ }
35
+ }
36
+ }
37
+ schedule(key) {
38
+ if (this.pending.has(key))
39
+ return;
40
+ // Keep new misses eligible even when old tables remain unavailable forever.
41
+ if (this.pending.size >= AddressLookupTableCache.CAPACITY) {
42
+ this.pending.delete(this.pending.keys().next().value);
43
+ }
44
+ this.pending.add(key);
45
+ }
46
+ /** null means this transaction is unresolved; callers must skip it, never emit guessed keys. */
47
+ resolve(tx) {
48
+ if (tx.messageVersion !== "v0" || !tx.addressTableLookups?.length)
49
+ return tx.accounts;
50
+ let missing = false;
51
+ for (const lookup of tx.addressTableLookups) {
52
+ if (!this.tables.has(lookup.accountKey)) {
53
+ missing = true;
54
+ this.schedule(lookup.accountKey);
55
+ }
56
+ }
57
+ if (missing)
58
+ return null;
59
+ const keys = tx.accounts.slice();
60
+ // Runtime layout is all writable keys across tables, then all readonly keys.
61
+ for (const kind of ["writableIndexes", "readonlyIndexes"]) {
62
+ for (const lookup of tx.addressTableLookups) {
63
+ const table = this.tables.get(lookup.accountKey);
64
+ for (const index of lookup[kind]) {
65
+ const key = table[index];
66
+ if (key === undefined) {
67
+ // A snapshot can predate an ALT extension. Refresh outside parsing.
68
+ this.schedule(lookup.accountKey);
69
+ return null;
70
+ }
71
+ keys.push(key);
72
+ }
73
+ }
74
+ }
75
+ return keys;
76
+ }
77
+ /** Explicit pre-subscription network operation, batched to the RPC's 100-account limit. */
78
+ async preload(addresses) {
79
+ if (!this.connection)
80
+ throw new Error("ALT preloading requires config.connection");
81
+ const unique = [...new Set(addresses)];
82
+ for (let start = 0; start < unique.length; start += 100) {
83
+ this.store((await (0, alt_lookup_js_1.loadAddressLookupTableAccounts)(this.connection, unique.slice(start, start + 100))).values());
84
+ }
85
+ }
86
+ start() {
87
+ if (!this.connection || this.timer)
88
+ return;
89
+ const generation = ++this.generation;
90
+ this.timer = setInterval(() => { void this.refresh(generation); }, this.refreshIntervalMs);
91
+ this.timer.unref?.();
92
+ }
93
+ stop() {
94
+ if (this.timer)
95
+ clearInterval(this.timer);
96
+ this.timer = undefined;
97
+ this.generation++;
98
+ }
99
+ async refresh(generation) {
100
+ if (!this.connection || this.refreshing)
101
+ return;
102
+ // Reserve a share of each batch for existing snapshots under a full miss queue.
103
+ const pendingLimit = 100 - Math.min(25, this.tables.size);
104
+ const keys = [...this.pending].slice(0, pendingLimit);
105
+ // Rotate unresolved misses too; absent tables must not starve the rest of the queue.
106
+ for (const key of keys) {
107
+ this.pending.delete(key);
108
+ this.pending.add(key);
109
+ }
110
+ // Rotate cached snapshots as well, so existing tables can acquire appended addresses.
111
+ const cached = [...this.tables.keys()];
112
+ if (cached.length) {
113
+ let visited = 0;
114
+ while (visited < cached.length && keys.length < 100) {
115
+ const key = cached[(this.refreshOffset + visited) % cached.length];
116
+ visited++;
117
+ if (!keys.includes(key))
118
+ keys.push(key);
119
+ }
120
+ this.refreshOffset = (this.refreshOffset + visited) % cached.length;
121
+ }
122
+ if (!keys.length)
123
+ return;
124
+ this.refreshing = true;
125
+ try {
126
+ const tables = await (0, alt_lookup_js_1.loadAddressLookupTableAccounts)(this.connection, keys);
127
+ if (generation === this.generation)
128
+ this.store(tables.values());
129
+ }
130
+ catch {
131
+ this.failures++;
132
+ }
133
+ finally {
134
+ this.refreshing = false;
135
+ }
136
+ }
137
+ }
138
+ exports.AddressLookupTableCache = AddressLookupTableCache;
@@ -7,6 +7,8 @@ export type ShredStreamReceiveStats = {
7
7
  entryDecodeFailures: number;
8
8
  transactionsDecoded: number;
9
9
  dexEventsQueued: number;
10
+ /** V0 transactions skipped because a complete cached ALT snapshot was unavailable. */
11
+ altCacheMissTransactions: number;
10
12
  };
11
13
  /** 有界 FIFO 队列(容量与 Rust `ArrayQueue::new(100_000)` 一致;`clone()` 共享底层缓冲,同 `Arc<ArrayQueue>`) */
12
14
  export declare class ShredEventQueue {
@@ -25,9 +27,11 @@ export declare class ShredStreamClient {
25
27
  private readonly endpoint;
26
28
  private readonly config;
27
29
  private readonly ServiceClient;
30
+ private lifecycleTail;
28
31
  private loopAbort;
29
32
  private loopPromise;
30
33
  private activeCall;
34
+ private readonly altCache;
31
35
  private receiveStats;
32
36
  private constructor();
33
37
  static connect(endpoint: string): Promise<ShredStreamClient>;
@@ -40,6 +44,10 @@ export declare class ShredStreamClient {
40
44
  clone(): ShredStreamClient;
41
45
  /** 当前订阅周期内 gRPC Entry 条数、解码失败次数、解码出的交易数、入队 DexEvent 数 */
42
46
  getReceiveStats(): Readonly<ShredStreamReceiveStats>;
47
+ /** Prewarm required ALTs before subscribe(); this explicit call may perform RPC. */
48
+ preloadAddressLookupTables(addresses: readonly string[]): Promise<void>;
49
+ /** Background refresh failures; cache misses are reported separately in receive stats. */
50
+ getAltRefreshFailures(): number;
43
51
  /**
44
52
  * 与 Rust `new_with_config` 中 `ShredstreamProxyClient::connect_with_config(endpoint, &config)` 一致:
45
53
  * `connection_timeout_ms` / `request_timeout_ms` / `max_decoding_message_size` 见 `shredstream/proto/mod.rs`。
@@ -54,12 +62,12 @@ export declare class ShredStreamClient {
54
62
  subscribeWithFilter(eventTypeFilter?: EventTypeFilter): Promise<ShredEventQueue>;
55
63
  /** 停止订阅并中止当前流 */
56
64
  stop(): Promise<void>;
65
+ private serializeLifecycle;
66
+ private stopUnlocked;
57
67
  private runReconnectLoop;
58
68
  /** 与 Rust `stream_events` + `connect_with_config`(tonic Endpoint / Grpc 消息上限)一致 */
59
69
  private streamOnce;
60
70
  private processEntryMessage;
61
- /** 无 RPC:仅用静态账户表 */
71
+ /** Synchronous parsing: static keys or complete prewarmed ALT snapshots only. */
62
72
  private processEntryMessageSync;
63
- /** 拉取 ALT 后完整账户表解析 */
64
- private processEntryMessageWithAlt;
65
73
  }
@@ -43,7 +43,7 @@ const protoLoader = __importStar(require("@grpc/proto-loader"));
43
43
  const node_path_1 = require("node:path");
44
44
  const unified_parser_js_1 = require("../core/unified_parser.js");
45
45
  const instruction_parse_js_1 = require("./instruction_parse.js");
46
- const alt_lookup_js_1 = require("./alt_lookup.js");
46
+ const alt_cache_js_1 = require("./alt_cache.js");
47
47
  const config_js_1 = require("./config.js");
48
48
  const entries_decode_js_1 = require("./entries_decode.js");
49
49
  const PROTO_PATH = (0, node_path_1.join)(__dirname, "shredstream.proto");
@@ -79,8 +79,18 @@ function grpcCredentialsForEndpoint(endpoint) {
79
79
  }
80
80
  return grpc.credentials.createInsecure();
81
81
  }
82
- function sleepMs(ms) {
83
- return new Promise((r) => setTimeout(r, ms));
82
+ function sleepMs(ms, signal) {
83
+ if (signal.aborted)
84
+ return Promise.resolve();
85
+ return new Promise((resolve) => {
86
+ const finish = () => {
87
+ clearTimeout(timer);
88
+ signal.removeEventListener("abort", finish);
89
+ resolve();
90
+ };
91
+ const timer = setTimeout(finish, ms);
92
+ signal.addEventListener("abort", finish, { once: true });
93
+ });
84
94
  }
85
95
  function shredDebugEnabled() {
86
96
  const v = process.env.SHREDSTREAM_DEBUG;
@@ -127,18 +137,22 @@ class ShredStreamClient {
127
137
  endpoint;
128
138
  config;
129
139
  ServiceClient;
140
+ lifecycleTail = Promise.resolve();
130
141
  loopAbort = null;
131
142
  loopPromise = null;
132
143
  activeCall = null;
144
+ altCache;
133
145
  receiveStats = {
134
146
  entryMessagesReceived: 0,
135
147
  entryDecodeFailures: 0,
136
148
  transactionsDecoded: 0,
137
149
  dexEventsQueued: 0,
150
+ altCacheMissTransactions: 0,
138
151
  };
139
152
  constructor(endpoint, config) {
140
153
  this.endpoint = endpoint;
141
154
  this.config = config;
155
+ this.altCache = new alt_cache_js_1.AddressLookupTableCache(config.connection, config.address_lookup_tables, config.alt_refresh_interval_ms);
142
156
  const root = loadProtoDefinition();
143
157
  const shred = root.shredstream;
144
158
  this.ServiceClient = shred.ShredstreamProxy;
@@ -167,6 +181,12 @@ class ShredStreamClient {
167
181
  getReceiveStats() {
168
182
  return { ...this.receiveStats };
169
183
  }
184
+ /** Prewarm required ALTs before subscribe(); this explicit call may perform RPC. */
185
+ async preloadAddressLookupTables(addresses) {
186
+ await this.altCache.preload(addresses);
187
+ }
188
+ /** Background refresh failures; cache misses are reported separately in receive stats. */
189
+ getAltRefreshFailures() { return this.altCache.refreshFailures; }
170
190
  /**
171
191
  * 与 Rust `new_with_config` 中 `ShredstreamProxyClient::connect_with_config(endpoint, &config)` 一致:
172
192
  * `connection_timeout_ms` / `request_timeout_ms` / `max_decoding_message_size` 见 `shredstream/proto/mod.rs`。
@@ -195,19 +215,27 @@ class ShredStreamClient {
195
215
  * 重连循环使用订阅时刻的配置快照(与 Rust 在 `tokio::spawn` 前 `config.clone()` 一致)。
196
216
  */
197
217
  async subscribe(eventTypeFilter) {
198
- await this.stop();
199
- this.receiveStats = {
200
- entryMessagesReceived: 0,
201
- entryDecodeFailures: 0,
202
- transactionsDecoded: 0,
203
- dexEventsQueued: 0,
204
- };
205
- const queue = new ShredEventQueue();
206
- const ac = new AbortController();
207
- this.loopAbort = ac;
208
- const configSnapshot = { ...this.config };
209
- this.loopPromise = this.runReconnectLoop(queue, ac.signal, configSnapshot, eventTypeFilter);
210
- return queue;
218
+ return this.serializeLifecycle(async () => {
219
+ await this.stopUnlocked();
220
+ this.receiveStats = {
221
+ entryMessagesReceived: 0,
222
+ entryDecodeFailures: 0,
223
+ transactionsDecoded: 0,
224
+ dexEventsQueued: 0,
225
+ altCacheMissTransactions: 0,
226
+ };
227
+ const queue = new ShredEventQueue();
228
+ const ac = new AbortController();
229
+ this.loopAbort = ac;
230
+ const configSnapshot = { ...this.config };
231
+ this.altCache.start();
232
+ this.loopPromise = this.runReconnectLoop(queue, ac.signal, configSnapshot, eventTypeFilter)
233
+ .finally(() => {
234
+ if (this.loopAbort === ac)
235
+ this.altCache.stop();
236
+ });
237
+ return queue;
238
+ });
211
239
  }
212
240
  /** 与 Rust `subscribe_with_filter` 对齐:在解析热路径按事件类型预过滤。 */
213
241
  async subscribeWithFilter(eventTypeFilter) {
@@ -215,6 +243,15 @@ class ShredStreamClient {
215
243
  }
216
244
  /** 停止订阅并中止当前流 */
217
245
  async stop() {
246
+ await this.serializeLifecycle(() => this.stopUnlocked());
247
+ }
248
+ serializeLifecycle(operation) {
249
+ const result = this.lifecycleTail.then(operation);
250
+ this.lifecycleTail = result.then(() => undefined, () => undefined);
251
+ return result;
252
+ }
253
+ async stopUnlocked() {
254
+ this.altCache.stop();
218
255
  this.loopAbort?.abort();
219
256
  this.loopAbort = null;
220
257
  this.activeCall?.cancel();
@@ -244,7 +281,7 @@ class ShredStreamClient {
244
281
  break;
245
282
  const msg = e instanceof Error ? e.message : String(e);
246
283
  console.error(`ShredStream error: ${msg} - retry in ${delay}ms`);
247
- await sleepMs(delay);
284
+ await sleepMs(delay, signal);
248
285
  if (signal.aborted)
249
286
  break;
250
287
  delay = Math.min(delay * 2, 60_000);
@@ -268,23 +305,31 @@ class ShredStreamClient {
268
305
  if (settled)
269
306
  return;
270
307
  settled = true;
308
+ signal.removeEventListener("abort", abort);
309
+ if (this.activeCall === call)
310
+ this.activeCall = null;
311
+ client.close();
271
312
  fn();
272
313
  };
314
+ const abort = () => {
315
+ call.cancel();
316
+ finish(resolve);
317
+ };
273
318
  const call = client.subscribeEntries({}, new grpc.Metadata());
274
319
  this.activeCall = call;
320
+ signal.addEventListener("abort", abort, { once: true });
275
321
  console.info("ShredStream connected, receiving entries...");
276
322
  call.on("data", (entry) => {
277
- if (signal.aborted)
323
+ if (signal.aborted || settled)
278
324
  return;
279
325
  try {
280
- void this.processEntryMessage(entry, queue, eventTypeFilter);
326
+ this.processEntryMessage(entry, queue, eventTypeFilter);
281
327
  }
282
328
  catch (err) {
283
329
  console.error("processEntryMessage:", err);
284
330
  }
285
331
  });
286
332
  call.on("error", (err) => {
287
- client.close();
288
333
  if (signal.aborted || err.code === grpc.status.CANCELLED) {
289
334
  finish(() => resolve());
290
335
  return;
@@ -293,12 +338,11 @@ class ShredStreamClient {
293
338
  finish(() => reject(err));
294
339
  });
295
340
  call.on("end", () => {
296
- client.close();
297
341
  finish(() => resolve());
298
342
  });
299
343
  });
300
344
  }
301
- async processEntryMessage(entry, queue, eventTypeFilter) {
345
+ processEntryMessage(entry, queue, eventTypeFilter) {
302
346
  this.receiveStats.entryMessagesReceived += 1;
303
347
  const recvUs = (0, unified_parser_js_1.nowUs)();
304
348
  const slotNum = toSlotNumber(entry.slot);
@@ -314,20 +358,9 @@ class ShredStreamClient {
314
358
  console.warn(`[shredstream] bincode 解码失败 slot=${slotNum} bytes=${bytes.length}: ${msg}`);
315
359
  return;
316
360
  }
317
- const conn = this.config.connection;
318
- if (conn) {
319
- try {
320
- await this.processEntryMessageWithAlt(decoded, slotNum, recvUs, queue, bytes.length, conn, eventTypeFilter);
321
- }
322
- catch (e) {
323
- console.warn(`[shredstream] ALT/RPC 解析失败 slot=${slotNum}:`, e);
324
- this.processEntryMessageSync(decoded, slotNum, recvUs, queue, bytes.length, eventTypeFilter);
325
- }
326
- return;
327
- }
328
361
  this.processEntryMessageSync(decoded, slotNum, recvUs, queue, bytes.length, eventTypeFilter);
329
362
  }
330
- /** 无 RPC:仅用静态账户表 */
363
+ /** Synchronous parsing: static keys or complete prewarmed ALT snapshots only. */
331
364
  processEntryMessageSync(decoded, slotNum, recvUs, queue, entriesBytesLen, eventTypeFilter) {
332
365
  let txTotal = 0;
333
366
  let evTotal = 0;
@@ -341,44 +374,15 @@ class ShredStreamClient {
341
374
  const txIndex = globalTxIndex++;
342
375
  if (!tx?.signature)
343
376
  continue;
344
- const events = (0, instruction_parse_js_1.dexEventsFromShredWasmTx)(tx, slotNum, txIndex, recvUs, eventTypeFilter);
345
- evTotal += events.length;
346
- for (const ev of events) {
347
- setGrpcRecvUsMut(ev, recvUs);
348
- queue.push(ev);
349
- }
350
- }
351
- }
352
- this.receiveStats.transactionsDecoded += txTotal;
353
- this.receiveStats.dexEventsQueued += evTotal;
354
- if (shredDebugEnabled()) {
355
- console.info(`[shredstream] slot=${slotNum} entries_bytes=${entriesBytesLen} solana_entries=${decoded.length} txs=${txTotal} dex_events=${evTotal} (static_accounts_only)`);
356
- }
357
- }
358
- /** 拉取 ALT 后完整账户表解析 */
359
- async processEntryMessageWithAlt(decoded, slotNum, recvUs, queue, entriesBytesLen, conn, eventTypeFilter) {
360
- const altKeys = new Set();
361
- for (const outer of decoded) {
362
- for (const tx of outer) {
363
- for (const l of tx.addressTableLookups ?? []) {
364
- altKeys.add(l.accountKey);
365
- }
366
- }
367
- }
368
- const altMap = await (0, alt_lookup_js_1.loadAddressLookupTableAccounts)(conn, [...altKeys]);
369
- let txTotal = 0;
370
- let evTotal = 0;
371
- let globalTxIndex = 0;
372
- for (const outer of decoded) {
373
- const txs = outer;
374
- txTotal += txs.length;
375
- for (let i = 0; i < txs.length; i++) {
376
- const tx = txs[i];
377
- const txIndex = globalTxIndex++;
378
- if (!tx?.signature)
377
+ const useCache = this.config.connection || this.config.address_lookup_tables;
378
+ const fullKeys = useCache ? this.altCache.resolve(tx) : tx.accounts;
379
+ if (!fullKeys) {
380
+ this.receiveStats.altCacheMissTransactions++;
379
381
  continue;
380
- const fullKeys = (0, alt_lookup_js_1.fullAccountKeyStringsFromShredTx)(tx, altMap);
381
- const events = (0, instruction_parse_js_1.dexEventsFromShredWasmTxWithFullKeys)(tx, fullKeys, slotNum, txIndex, recvUs, eventTypeFilter);
382
+ }
383
+ const events = useCache
384
+ ? (0, instruction_parse_js_1.dexEventsFromShredWasmTxWithFullKeys)(tx, fullKeys, slotNum, txIndex, recvUs, eventTypeFilter)
385
+ : (0, instruction_parse_js_1.dexEventsFromShredWasmTx)(tx, slotNum, txIndex, recvUs, eventTypeFilter);
382
386
  evTotal += events.length;
383
387
  for (const ev of events) {
384
388
  setGrpcRecvUsMut(ev, recvUs);
@@ -389,7 +393,7 @@ class ShredStreamClient {
389
393
  this.receiveStats.transactionsDecoded += txTotal;
390
394
  this.receiveStats.dexEventsQueued += evTotal;
391
395
  if (shredDebugEnabled()) {
392
- console.info(`[shredstream] slot=${slotNum} entries_bytes=${entriesBytesLen} solana_entries=${decoded.length} txs=${txTotal} dex_events=${evTotal} alt_tables=${altKeys.size}`);
396
+ console.info(`[shredstream] slot=${slotNum} entries_bytes=${entriesBytesLen} solana_entries=${decoded.length} txs=${txTotal} dex_events=${evTotal} (static_accounts_only)`);
393
397
  }
394
398
  }
395
399
  }
@@ -1,4 +1,4 @@
1
- import type { Connection } from "@solana/web3.js";
1
+ import type { AddressLookupTableAccount, Connection } from "@solana/web3.js";
2
2
  /**
3
3
  * 与 Rust `sol-parser-sdk/src/shredstream/config.rs` 字段一一对应。
4
4
  *
@@ -7,7 +7,7 @@ import type { Connection } from "@solana/web3.js";
7
7
  * (`request_timeout_ms` 保留与 Rust `config.rs` 一致,流式路径不填 deadline)。
8
8
  * `reconnect_delay_ms` / `max_reconnect_attempts` 由 `client.rs` 重连循环使用。
9
9
  *
10
- * **TS 扩展**:`connection` 用于拉取链上 ALT,补全 V0 交易账户表以解析外层指令(主网 Pump 等必需)。
10
+ * **TS 扩展**:ALT 使用预热快照和独立刷新任务;解析热路径不发送 RPC。
11
11
  */
12
12
  export interface ShredStreamConfig {
13
13
  /** 连接超时(毫秒)→ tonic `Endpoint::connect_timeout` / gRPC `waitForReady` */
@@ -20,8 +20,12 @@ export interface ShredStreamConfig {
20
20
  reconnect_delay_ms: number;
21
21
  /** 最大重连次数(0 表示无限重连) */
22
22
  max_reconnect_attempts: number;
23
- /** 可选:主网 RPC,用于解析 V0 地址查找表(ALT) */
23
+ /** 可选:只供显式预热及后台 ALT 刷新使用,解析热路径不发送 RPC。 */
24
24
  connection?: Connection;
25
+ /** 可选:订阅前已加载的 ALT 快照;未命中时跳过整笔 V0 交易并累计计数。 */
26
+ address_lookup_tables?: readonly AddressLookupTableAccount[];
27
+ /** 独立 ALT 刷新间隔(毫秒),默认 1000;最多一个刷新请求在途。 */
28
+ alt_refresh_interval_ms?: number;
25
29
  }
26
30
  export declare function defaultShredStreamConfig(): ShredStreamConfig;
27
31
  /** 低延迟:更短超时与重连间隔 */