sol-parser-sdk-python 0.5.8__py3-none-any.whl → 0.5.9__py3-none-any.whl

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.
sol_parser/__init__.py CHANGED
@@ -466,3 +466,27 @@ __all__ = [
466
466
  "ShredStreamClient",
467
467
  "ShredStreamConfig",
468
468
  ]
469
+
470
+ # Native route and subscription state APIs aligned with Rust 0.7.7.
471
+ from .transaction_route import (analyze_rpc_transaction_routes, TransactionRoute, RouteSwapLeg,
472
+ RouteTokenTransfer, RouteUnknownInvocation, RouteNativeTokenAction, InstructionPosition,
473
+ stonkfun_mode_from_platform_config)
474
+ from .stonkfun_registry import StonkFunPoolRegistry, StonkFunGraduatedPool
475
+ from .liquidity_snapshot import RawAccountSnapshotEvent, LiquidityAccountSnapshotEvent, parse_liquidity_account
476
+ from .wire_transaction import decode_wire_transaction
477
+
478
+ from .simulation_route import analyze_simulation_routes
479
+
480
+ __all__.append("analyze_simulation_routes")
481
+
482
+ from .block_meta import BlockMetaEvent, parse_block_meta_update
483
+ from .grpc.subscribe_builder import build_subscribe_request_with_event_filter
484
+
485
+ from .event_types import (
486
+ LaunchLabPoolCreateEvent, StonkFunPoolCreateEvent,
487
+ LaunchLabTradeEvent, StonkFunTradeEvent,
488
+ )
489
+ __all__ += ["LaunchLabPoolCreateEvent", "StonkFunPoolCreateEvent", "LaunchLabTradeEvent", "StonkFunTradeEvent"]
490
+
491
+ from .grpc_client import DexEventQueue, GrpcStreamStatus
492
+ __all__ += ["DexEventQueue", "GrpcStreamStatus"]
@@ -82,11 +82,26 @@ def fill_accounts_with_owned_keys(
82
82
  w = [bytes(x) for x in meta_pb.loaded_writable_addresses]
83
83
  r = [bytes(x) for x in meta_pb.loaded_readonly_addresses]
84
84
 
85
- def run(program_b58: str, filler) -> None:
85
+ def run(program_b58: str, filler, anchor=None, unambiguous=False) -> None:
86
86
  pid = base58.b58decode(program_b58)
87
87
  inv = invokes.get(pid)
88
88
  if not inv:
89
89
  return
90
+ if anchor is not None or unambiguous:
91
+ pool, index = anchor or (None,1)
92
+ has_anchor=pool and pool != "11111111111111111111111111111111"
93
+ if not has_anchor and not unambiguous:return
94
+ selected = None
95
+ for invocation in inv:
96
+ candidate = get_instruction_account_getter(meta_pb, transaction_pb, static_keys, w, r, invocation)
97
+ if candidate is None or (has_anchor and candidate(index) != pool):
98
+ continue
99
+ if selected is not None and any(selected(i) != candidate(i) for i in range(18)):
100
+ return # Same pool, conflicting wallet/account context: do not guess.
101
+ selected = candidate
102
+ if selected is not None:
103
+ filler(selected)
104
+ return
90
105
  ix = find_instruction_invoke(inv, meta_pb, transaction_pb)
91
106
  if ix is None:
92
107
  return
@@ -146,7 +161,7 @@ def fill_accounts_with_owned_keys(
146
161
  elif et == EventType.RAYDIUM_CPMM_INITIALIZE:
147
162
  run(RAYDIUM_CPMM_PROGRAM_ID, lambda g: raydium.fill_cpmm_initialize_accounts(data, g))
148
163
  elif et == EventType.RAYDIUM_AMM_V4_SWAP:
149
- run(RAYDIUM_AMM_V4_PROGRAM_ID, lambda g: raydium.fill_amm_v4_swap_accounts(data, g))
164
+ run(RAYDIUM_AMM_V4_PROGRAM_ID, lambda g: raydium.fill_amm_v4_swap_accounts(data, g), (data.amm,1), True)
150
165
  elif et == EventType.RAYDIUM_AMM_V4_DEPOSIT:
151
166
  run(RAYDIUM_AMM_V4_PROGRAM_ID, lambda g: raydium.fill_amm_v4_deposit_accounts(data, g))
152
167
  elif et == EventType.RAYDIUM_AMM_V4_WITHDRAW:
@@ -182,9 +197,9 @@ def fill_accounts_with_owned_keys(
182
197
  elif et == EventType.METEORA_DLMM_REMOVE_LIQUIDITY:
183
198
  run(METEORA_DLMM_PROGRAM_ID, lambda g: meteora.fill_dlmm_remove_liquidity_accounts(data, g))
184
199
  elif et == EventType.RAYDIUM_LAUNCHLAB_TRADE:
185
- run(RAYDIUM_LAUNCHLAB_PROGRAM_ID, lambda g: raydium_launchlab.fill_trade_accounts(data, g))
200
+ run(RAYDIUM_LAUNCHLAB_PROGRAM_ID, lambda g: raydium_launchlab.fill_trade_accounts(data, g), (data.pool_state,4))
186
201
  elif et == EventType.RAYDIUM_LAUNCHLAB_POOL_CREATE:
187
- run(RAYDIUM_LAUNCHLAB_PROGRAM_ID, lambda g: raydium_launchlab.fill_pool_create_accounts(data, g))
202
+ run(RAYDIUM_LAUNCHLAB_PROGRAM_ID, lambda g: raydium_launchlab.fill_pool_create_accounts(data, g), (data.pool_state,5))
188
203
 
189
204
 
190
205
  def fill_data(
@@ -100,8 +100,12 @@ def fill_cpmm_initialize_accounts(e: RaydiumCpmmInitializeEvent, get: AccountGet
100
100
 
101
101
 
102
102
  def fill_amm_v4_swap_accounts(e: RaydiumAmmV4SwapEvent, get: AccountGetter) -> None:
103
- if _empty(e.amm):
104
- e.amm = get(1)
103
+ if _empty(e.amm):e.amm=get(1)
104
+ modern=_empty(get(8)) and not _empty(get(7))
105
+ if modern:
106
+ for field,index in [('token_program',0),('amm_authority',2),('pool_coin_token_account',3),('pool_pc_token_account',4),('user_source_token_account',5),('user_destination_token_account',6),('user_source_owner',7)]:
107
+ if _empty(getattr(e,field)):setattr(e,field,get(index))
108
+ elif _empty(e.user_source_owner):e.user_source_owner=get(16) if _empty(get(17)) else get(17)
105
109
 
106
110
 
107
111
  def fill_amm_v4_deposit_accounts(e: RaydiumAmmV4DepositEvent, get: AccountGetter) -> None:
@@ -43,6 +43,10 @@ def fill_trade_accounts(e: RaydiumLaunchlabTradeEvent, get: AccountGetter) -> No
43
43
  if _empty(e.quote_token_program):
44
44
  e.quote_token_program = get(12)
45
45
 
46
+ for name, index in [("system_program", 15), ("platform_associated_account", 16), ("creator_associated_account", 17)]:
47
+ if _empty(getattr(e,name,"")):
48
+ setattr(e,name,get(index))
49
+
46
50
 
47
51
  def fill_pool_create_accounts(e: RaydiumLaunchlabPoolCreateEvent, get: AccountGetter) -> None:
48
52
  if _empty(e.pool_state):
@@ -206,6 +206,11 @@ def parse_account_unified(
206
206
  event_type_filter: Optional[EventTypeFilter] = None,
207
207
  ) -> Optional[DexEvent]:
208
208
  """对齐 Rust ``parse_account_unified``(含 ``Option<EventTypeFilter>`` 语义)。"""
209
+ from ..liquidity_snapshot import parse_liquidity_account
210
+ if event_type_filter is None or event_type_filter.should_include(EventType.ACCOUNT_LIQUIDITY_SNAPSHOT):
211
+ snapshot = parse_liquidity_account(account, metadata)
212
+ if snapshot is not None:
213
+ return DexEvent(type=EventType.ACCOUNT_LIQUIDITY_SNAPSHOT, data=snapshot)
209
214
  data = account.data
210
215
  if not data:
211
216
  return None
@@ -0,0 +1,29 @@
1
+ """Local BlockMeta adapter, matching Rust's saturated timestamp conversion."""
2
+
3
+ from dataclasses import dataclass
4
+ from .grpc_types import EventType, EventMetadata
5
+ from .event_types import DexEvent
6
+
7
+
8
+ @dataclass
9
+ class BlockMetaEvent:
10
+ metadata: EventMetadata
11
+
12
+
13
+ def parse_block_meta_update(meta, grpc_recv_us, fallback_block_us=None):
14
+ block_us = fallback_block_us or 0
15
+ if meta.block_time is not None:
16
+ block_us = max(-(1 << 63), min((1 << 63) - 1, meta.block_time * 1_000_000))
17
+ return DexEvent(
18
+ EventType.BLOCK_META,
19
+ BlockMetaEvent(
20
+ EventMetadata(
21
+ signature="1" * 64,
22
+ slot=meta.slot,
23
+ tx_index=0,
24
+ block_time_us=block_us,
25
+ grpc_recv_us=grpc_recv_us,
26
+ recent_blockhash=meta.blockhash,
27
+ )
28
+ ),
29
+ )
sol_parser/dex_parsers.py CHANGED
@@ -2727,7 +2727,7 @@ def parse_raydium_launchlab_from_discriminator(disc: int, data: bytes, meta: dic
2727
2727
 
2728
2728
 
2729
2729
  def _parse_raydium_launchlab_trade(data: bytes, meta: dict) -> Optional[DexEvent]:
2730
- if len(data) < 139:
2730
+ if len(data) != 139 or data[136] > 1 or data[137] > 2 or data[138] > 1:
2731
2731
  return None
2732
2732
  pool = _pub(data, 0)
2733
2733
  ai = _u64le(data, 88)
@@ -2746,6 +2746,19 @@ def _parse_raydium_launchlab_trade(data: bytes, meta: dict) -> Optional[DexEvent
2746
2746
  is_buy=is_buy,
2747
2747
  trade_direction=d,
2748
2748
  exact_in=ex_in,
2749
+ pool_status=("Fund","Migrate","Trade")[data[137]],
2750
+ total_base_sell=_u64le(data,32),
2751
+ virtual_base=_u64le(data,40),
2752
+ virtual_quote=_u64le(data,48),
2753
+ real_base_before=_u64le(data,56),
2754
+ real_quote_before=_u64le(data,64),
2755
+ real_base_after=_u64le(data,72),
2756
+ real_quote_after=_u64le(data,80),
2757
+ protocol_fee=_u64le(data,104),
2758
+ platform_fee=_u64le(data,112),
2759
+ creator_fee=_u64le(data,120),
2760
+ share_fee=_u64le(data,128),
2761
+
2749
2762
  ),
2750
2763
  )
2751
2764
 
@@ -46,87 +46,13 @@ class _ParsedTx:
46
46
 
47
47
 
48
48
  def parse_transaction_wire(buf: bytes, pos: int) -> Optional[_ParsedTx]:
49
- start = pos
50
- if pos >= len(buf):
51
- return None
52
- sig_count_enc = decode_compact_u16(buf, pos)
53
- if not sig_count_enc:
54
- return None
55
- p = pos + sig_count_enc[1]
56
- sig_count = sig_count_enc[0]
57
- sigs_end = p + sig_count * 64
58
- if sigs_end > len(buf):
59
- return None
60
- sigs: List[bytes] = []
61
- for _ in range(sig_count):
62
- sigs.append(buf[p : p + 64])
63
- p += 64
64
- if p >= len(buf):
65
- return None
66
- msg_first = buf[p]
67
- is_v0 = msg_first >= 0x80
68
- if is_v0:
69
- p += 1
70
- p += 3
71
- if p > len(buf):
72
- return None
73
- acct_enc = decode_compact_u16(buf, p)
74
- if not acct_enc:
75
- return None
76
- p += acct_enc[1]
77
- p += acct_enc[0] * 32
78
- if p > len(buf):
79
- return None
80
- ix_count_enc = decode_compact_u16(buf, p)
81
- if not ix_count_enc:
49
+ from .wire_transaction import decode_wire_transaction
50
+ import base58
51
+ try:
52
+ transaction, length = decode_wire_transaction(buf, pos, require_complete=False)
53
+ return _ParsedTx(length, [base58.b58decode(s) for s in transaction['signatures']])
54
+ except (ValueError, IndexError):
82
55
  return None
83
- p += ix_count_enc[1]
84
- ix_count = ix_count_enc[0]
85
- for _ in range(ix_count):
86
- p += 1
87
- if p > len(buf):
88
- return None
89
- acct_len_enc = decode_compact_u16(buf, p)
90
- if not acct_len_enc:
91
- return None
92
- p += acct_len_enc[1]
93
- p += acct_len_enc[0]
94
- if p > len(buf):
95
- return None
96
- data_len_enc = decode_compact_u16(buf, p)
97
- if not data_len_enc:
98
- return None
99
- p += data_len_enc[1]
100
- p += data_len_enc[0]
101
- if p > len(buf):
102
- return None
103
- if is_v0:
104
- if p >= len(buf):
105
- return None
106
- atl_count_enc = decode_compact_u16(buf, p)
107
- if not atl_count_enc:
108
- return None
109
- p += atl_count_enc[1]
110
- atl_count = atl_count_enc[0]
111
- for _ in range(atl_count):
112
- p += 32
113
- if p > len(buf):
114
- return None
115
- w_len_enc = decode_compact_u16(buf, p)
116
- if not w_len_enc:
117
- return None
118
- p += w_len_enc[1]
119
- p += w_len_enc[0]
120
- if p > len(buf):
121
- return None
122
- r_len_enc = decode_compact_u16(buf, p)
123
- if not r_len_enc:
124
- return None
125
- p += r_len_enc[1]
126
- p += r_len_enc[0]
127
- if p > len(buf):
128
- return None
129
- return _ParsedTx(tx_len=p - start, signatures=sigs)
130
56
 
131
57
 
132
58
  class _BatchDecoder:
sol_parser/event_types.py CHANGED
@@ -1410,6 +1410,28 @@ class RaydiumLaunchlabTradeEvent(DexEventBase):
1410
1410
  base_token_program: str = ""
1411
1411
  quote_token_program: str = ""
1412
1412
 
1413
+ total_base_sell: int = 0
1414
+ virtual_base: int = 0
1415
+ virtual_quote: int = 0
1416
+ real_base_before: int = 0
1417
+ real_quote_before: int = 0
1418
+ real_base_after: int = 0
1419
+ real_quote_after: int = 0
1420
+ protocol_fee: int = 0
1421
+ platform_fee: int = 0
1422
+ creator_fee: int = 0
1423
+ share_fee: int = 0
1424
+ pool_status: str = "Fund"
1425
+ system_program: str = ""
1426
+ platform_associated_account: str = ""
1427
+ creator_associated_account: str = ""
1428
+
1429
+ def stonkfun_mode(self):
1430
+ from .transaction_route import stonkfun_mode_from_platform_config
1431
+ return stonkfun_mode_from_platform_config(self.platform_config)
1432
+
1433
+ def is_stonkfun(self):
1434
+ return self.stonkfun_mode() is not None
1413
1435
 
1414
1436
  @dataclass
1415
1437
  class RaydiumLaunchlabPoolCreateEvent(DexEventBase):
@@ -1428,6 +1450,14 @@ class RaydiumLaunchlabPoolCreateEvent(DexEventBase):
1428
1450
  quote_token_program: str = ""
1429
1451
 
1430
1452
 
1453
+
1454
+ def stonkfun_mode(self):
1455
+ from .transaction_route import stonkfun_mode_from_platform_config
1456
+ return stonkfun_mode_from_platform_config(self.platform_config)
1457
+
1458
+ def is_stonkfun(self):
1459
+ return self.stonkfun_mode() is not None
1460
+
1431
1461
  @dataclass
1432
1462
  class RaydiumLaunchlabMigrateAmmEvent(DexEventBase):
1433
1463
  """RaydiumLaunchlab 迁移 AMM 事件"""
@@ -1436,6 +1466,18 @@ class RaydiumLaunchlabMigrateAmmEvent(DexEventBase):
1436
1466
  user: str = ""
1437
1467
  liquidity_amount: int = 0
1438
1468
 
1469
+ liquidity_amount_known: bool = False
1470
+ base_mint: str = ""
1471
+ quote_mint: str = ""
1472
+ platform_config: str = ""
1473
+ destination_program: str = ""
1474
+
1475
+ def stonkfun_mode(self):
1476
+ from .transaction_route import stonkfun_mode_from_platform_config
1477
+ return stonkfun_mode_from_platform_config(self.platform_config)
1478
+
1479
+ def is_stonkfun(self):
1480
+ return self.stonkfun_mode() is not None
1439
1481
 
1440
1482
  # ============================================================
1441
1483
  # 类型联合
@@ -2379,3 +2421,9 @@ def legacy_dict_to_dex_event(d: dict) -> Optional[DexEvent]:
2379
2421
  except ValueError:
2380
2422
  return None
2381
2423
  return DexEvent(type=et, data=typed)
2424
+
2425
+ # Same classes: preserve isinstance, serialization and platform attribution.
2426
+ LaunchLabPoolCreateEvent = RaydiumLaunchlabPoolCreateEvent
2427
+ StonkFunPoolCreateEvent = RaydiumLaunchlabPoolCreateEvent
2428
+ LaunchLabTradeEvent = RaydiumLaunchlabTradeEvent
2429
+ StonkFunTradeEvent = RaydiumLaunchlabTradeEvent
@@ -131,3 +131,11 @@ __all__ = [
131
131
  "build_subscribe_request_with_commitment",
132
132
  "build_subscribe_transaction_filters_named",
133
133
  ]
134
+
135
+
136
+ def build_subscribe_request_with_event_filter(tx_filters, acc_filters, event_type_filter=None):
137
+ from ..grpc_types import EventType
138
+ request=build_subscribe_request(tx_filters,acc_filters)
139
+ if event_type_filter is not None and event_type_filter.should_include(EventType.BLOCK_META):
140
+ request.blocks_meta["block_meta"].CopyFrom(geyser_pb2.SubscribeRequestFilterBlocksMeta())
141
+ return request
sol_parser/grpc_client.py CHANGED
@@ -11,7 +11,7 @@ import asyncio
11
11
  import time
12
12
  import uuid
13
13
  from typing import Dict, List, Optional, Callable, Any, AsyncGenerator, Tuple
14
- from dataclasses import dataclass
14
+ from dataclasses import dataclass, replace
15
15
  from urllib.parse import urlparse
16
16
 
17
17
  import base58
@@ -97,6 +97,42 @@ class Subscription:
97
97
  callbacks: SubscribeCallbacks
98
98
 
99
99
 
100
+ @dataclass(frozen=True)
101
+ class GrpcStreamStatus:
102
+ state: str = "connecting"
103
+ continuity_broken: bool = False
104
+ reconnects: int = 0
105
+ dropped: int = 0
106
+
107
+ class DexEventQueue(asyncio.Queue):
108
+ """Queue-compatible DEX subscription with observable transport failures."""
109
+ def __init__(self, maxsize):
110
+ super().__init__(maxsize=maxsize)
111
+ self.errors = asyncio.Queue(maxsize=maxsize)
112
+ self.states = asyncio.Queue(maxsize=maxsize)
113
+ self._status = GrpcStreamStatus()
114
+ self.states.put_nowait(self._status)
115
+
116
+ def status(self):
117
+ return self._status
118
+
119
+ def transition(self, state, *, continuity_broken=None, reconnect=False, dropped=False):
120
+ previous = self._status
121
+ self._status = replace(
122
+ self._status, state=state,
123
+ continuity_broken=self._status.continuity_broken if continuity_broken is None else continuity_broken,
124
+ reconnects=self._status.reconnects + int(reconnect),
125
+ dropped=self._status.dropped + int(dropped),
126
+ )
127
+ if self._status == previous: return
128
+ if self.states.full(): self.states.get_nowait()
129
+ self.states.put_nowait(self._status)
130
+
131
+ def report_error(self, error):
132
+ if self.errors.full(): self.errors.get_nowait()
133
+ self.errors.put_nowait(error)
134
+
135
+
100
136
  class YellowstoneGrpc:
101
137
  """Yellowstone gRPC 客户端"""
102
138
 
@@ -109,6 +145,7 @@ class YellowstoneGrpc:
109
145
  self._channel: Optional[aio.Channel] = None
110
146
  self._client: Optional[Any] = None
111
147
  self._lock = asyncio.Lock()
148
+ self._dex_lifecycle = asyncio.Lock()
112
149
  self._dex_event_queue: Optional[asyncio.Queue] = None
113
150
  self._dex_event_filter: Optional[Any] = None
114
151
  self._dex_cancel_event: Optional[asyncio.Event] = None
@@ -140,7 +177,27 @@ class YellowstoneGrpc:
140
177
  inst.set_x_token(token)
141
178
  return inst
142
179
 
143
- async def subscribe_dex_events(
180
+ async def stop(self):
181
+ async with self._dex_lifecycle:
182
+ await self._stop_dex_unlocked()
183
+
184
+ async def _stop_dex_unlocked(self):
185
+ if self._dex_cancel_event is not None: self._dex_cancel_event.set()
186
+ task = self._dex_task
187
+ if task is not None:
188
+ task.cancel()
189
+ await asyncio.gather(task, return_exceptions=True)
190
+ self._dex_task = None
191
+ self._dex_cancel_event = None
192
+ self._dex_request_queue = None
193
+ self._dex_current_req = None
194
+
195
+ async def subscribe_dex_events(self, transaction_filters, account_filters, event_type_filter=None):
196
+ async with self._dex_lifecycle:
197
+ await self._stop_dex_unlocked()
198
+ return await self._subscribe_dex_events(transaction_filters, account_filters, event_type_filter)
199
+
200
+ async def _subscribe_dex_events(
144
201
  self,
145
202
  transaction_filters: List[TransactionFilter],
146
203
  account_filters: List[Any],
@@ -156,10 +213,10 @@ class YellowstoneGrpc:
156
213
  if not self._client:
157
214
  raise RuntimeError("Client not connected")
158
215
 
159
- from .grpc.subscribe_builder import build_subscribe_request
216
+ from .grpc.subscribe_builder import build_subscribe_request_with_event_filter
160
217
 
161
- req = build_subscribe_request(transaction_filters, account_filters)
162
- queue: asyncio.Queue = asyncio.Queue(maxsize=max(1, int(self.config.buffer_size or 100_000)))
218
+ req = build_subscribe_request_with_event_filter(transaction_filters, account_filters, event_type_filter)
219
+ queue: asyncio.Queue = DexEventQueue(maxsize=max(1, int(self.config.buffer_size or 100_000)))
163
220
 
164
221
  if self._dex_cancel_event is not None:
165
222
  self._dex_cancel_event.set()
@@ -178,15 +235,19 @@ class YellowstoneGrpc:
178
235
  )
179
236
  return queue
180
237
 
181
- async def update_subscription(
238
+ async def update_subscription(self, transaction_filters, account_filters):
239
+ async with self._dex_lifecycle:
240
+ return await self._update_subscription(transaction_filters, account_filters)
241
+
242
+ async def _update_subscription(
182
243
  self,
183
244
  transaction_filters: List[TransactionFilter],
184
245
  account_filters: List[Any],
185
246
  ) -> None:
186
247
  """动态更新 DEX 订阅。
187
248
 
188
- Python gRPC runtime 不暴露当前双向流的发送端给外部 API;这里保留原队列,
189
- 取消旧后台流并用新过滤器立即重建,调用方无需替换消费队列。
249
+ 通过原双向流发送最新过滤器,保留事件消费队列。
250
+ 重连后使用最新请求,调用方无需替换消费队列。
190
251
  """
191
252
  if self._dex_event_queue is None:
192
253
  raise RuntimeError("No active DEX subscription")
@@ -195,9 +256,9 @@ class YellowstoneGrpc:
195
256
  if not self._client:
196
257
  raise RuntimeError("Client not connected")
197
258
 
198
- from .grpc.subscribe_builder import build_subscribe_request
259
+ from .grpc.subscribe_builder import build_subscribe_request_with_event_filter
199
260
 
200
- req = build_subscribe_request(transaction_filters, account_filters)
261
+ req = build_subscribe_request_with_event_filter(transaction_filters, account_filters, self._dex_event_filter)
201
262
  self._dex_current_req = req
202
263
  if self._dex_request_queue is None:
203
264
  raise RuntimeError("No active DEX subscription")
@@ -294,6 +355,11 @@ class YellowstoneGrpc:
294
355
  self._connected = True
295
356
 
296
357
  async def disconnect(self) -> None:
358
+ async with self._dex_lifecycle:
359
+ await self._stop_dex_unlocked()
360
+ await self._disconnect()
361
+
362
+ async def _disconnect(self) -> None:
297
363
  """断开连接"""
298
364
  if not self._connected:
299
365
  return
@@ -413,7 +479,8 @@ class YellowstoneGrpc:
413
479
  blockhash=meta.blockhash,
414
480
  parent_slot=meta.parent_slot,
415
481
  parent_blockhash=meta.parent_blockhash,
416
- executed_transaction_count=meta.executed_transaction_count
482
+ executed_transaction_count=meta.executed_transaction_count,
483
+ block_time=meta.block_time.timestamp if meta.HasField("block_time") else None
417
484
  )
418
485
 
419
486
  # 转换 Ping
@@ -432,7 +499,8 @@ class YellowstoneGrpc:
432
499
  queue.put_nowait(event)
433
500
  except asyncio.QueueFull:
434
501
  # 与 Rust 有界 ArrayQueue 的低延迟取舍一致:消费端落后时丢弃新事件,避免阻塞 gRPC 流。
435
- pass
502
+ if isinstance(queue, DexEventQueue):
503
+ queue.transition(queue.status().state, continuity_broken=True, dropped=True)
436
504
 
437
505
  async def _enqueue_transaction_dex_events(
438
506
  self,
@@ -554,6 +622,15 @@ class YellowstoneGrpc:
554
622
  block_time_us=0 if block_time_us is None else block_time_us,
555
623
  grpc_recv_us=grpc_recv_us,
556
624
  )
625
+ # Raw snapshots are opt-in, including closure updates with empty bytes.
626
+ from .event_types import DexEvent
627
+ from .grpc_types import EventType
628
+ from .liquidity_snapshot import RawAccountSnapshotEvent
629
+ if EventType.ACCOUNT_RAW_SNAPSHOT in (getattr(event_type_filter, "include_only", None) or []):
630
+ self._queue_event_nowait(queue, DexEvent(
631
+ type=EventType.ACCOUNT_RAW_SNAPSHOT,
632
+ data=RawAccountSnapshotEvent(metadata, account, int(acc.write_version), bool(update.account.is_startup)),
633
+ ))
557
634
  ev = parse_account_unified(account, metadata, event_type_filter)
558
635
  if ev is not None:
559
636
  self._queue_event_nowait(queue, ev)
@@ -582,6 +659,7 @@ class YellowstoneGrpc:
582
659
 
583
660
  try:
584
661
  while not cancel_event.is_set():
662
+ if isinstance(queue, DexEventQueue): queue.transition("connecting")
585
663
  outgoing: asyncio.Queue = asyncio.Queue()
586
664
 
587
665
  async def request_iterator():
@@ -606,6 +684,7 @@ class YellowstoneGrpc:
606
684
  async for pb_update in self._client.Subscribe(request_iterator(), metadata=metadata):
607
685
  if cancel_event.is_set():
608
686
  break
687
+ if isinstance(queue, DexEventQueue): queue.transition("connected")
609
688
  if pb_update.HasField("ping"):
610
689
  await outgoing.put(
611
690
  geyser_pb2.SubscribeRequest(
@@ -627,6 +706,13 @@ class YellowstoneGrpc:
627
706
  block_time_us,
628
707
  order_dispatcher,
629
708
  )
709
+ if update.block_meta:
710
+ from .block_meta import parse_block_meta_update
711
+ from .grpc_types import EventType
712
+ if event_type_filter is None or event_type_filter.should_include(EventType.BLOCK_META):
713
+ # Match transaction/account overflow semantics. A full
714
+ # consumer queue must not stall gRPC reads and pings.
715
+ self._queue_event_nowait(queue, parse_block_meta_update(update.block_meta,grpc_recv_us,block_time_us))
630
716
  if update.account:
631
717
  await self._enqueue_account_dex_event(
632
718
  queue,
@@ -635,10 +721,15 @@ class YellowstoneGrpc:
635
721
  grpc_recv_us,
636
722
  block_time_us,
637
723
  )
724
+ if not cancel_event.is_set():
725
+ raise ConnectionError("Yellowstone DEX stream ended")
638
726
  delay = 1.0
639
727
  except asyncio.CancelledError:
640
728
  break
641
- except Exception:
729
+ except Exception as error:
730
+ if isinstance(queue, DexEventQueue):
731
+ queue.report_error(error)
732
+ queue.transition("reconnecting", continuity_broken=True, reconnect=True)
642
733
  order_dispatcher.flush_all(lambda ev: self._queue_event_nowait(queue, ev))
643
734
  if cancel_event.is_set():
644
735
  break
@@ -646,10 +737,13 @@ class YellowstoneGrpc:
646
737
  delay = min(delay * 2.0, 60.0)
647
738
  finally:
648
739
  pump_task.cancel()
740
+ await asyncio.gather(pump_task, return_exceptions=True)
649
741
  await outgoing.put(None)
650
742
  finally:
651
743
  if flush_task is not None:
652
744
  flush_task.cancel()
745
+ await asyncio.gather(flush_task, return_exceptions=True)
746
+ if isinstance(queue, DexEventQueue): queue.transition("stopped", continuity_broken=True)
653
747
  order_dispatcher.flush_all(lambda ev: self._queue_event_nowait(queue, ev))
654
748
 
655
749
  async def subscribe_transactions(
@@ -766,7 +860,7 @@ class YellowstoneGrpc:
766
860
  req.commitment = commitment.value
767
861
 
768
862
  metadata = self._get_metadata()
769
- resp = await self._client.get_latest_blockhash(req, metadata=metadata)
863
+ resp = await self._client.GetLatestBlockhash(req, metadata=metadata)
770
864
 
771
865
  return GetLatestBlockhashResponse(
772
866
  slot=resp.slot,
@@ -789,7 +883,7 @@ class YellowstoneGrpc:
789
883
  req.commitment = commitment.value
790
884
 
791
885
  metadata = self._get_metadata()
792
- resp = await self._client.get_block_height(req, metadata=metadata)
886
+ resp = await self._client.GetBlockHeight(req, metadata=metadata)
793
887
 
794
888
  return GetBlockHeightResponse(block_height=resp.block_height)
795
889
 
@@ -808,7 +902,7 @@ class YellowstoneGrpc:
808
902
  req.commitment = commitment.value
809
903
 
810
904
  metadata = self._get_metadata()
811
- resp = await self._client.get_slot(req, metadata=metadata)
905
+ resp = await self._client.GetSlot(req, metadata=metadata)
812
906
 
813
907
  return GetSlotResponse(slot=resp.slot)
814
908
 
@@ -822,7 +916,7 @@ class YellowstoneGrpc:
822
916
 
823
917
  req = geyser_pb2.GetVersionRequest()
824
918
  metadata = self._get_metadata()
825
- resp = await self._client.get_version(req, metadata=metadata)
919
+ resp = await self._client.GetVersion(req, metadata=metadata)
826
920
 
827
921
  return GetVersionResponse(version=resp.version)
828
922
 
@@ -841,7 +935,7 @@ class YellowstoneGrpc:
841
935
  req.commitment = commitment.value
842
936
 
843
937
  metadata = self._get_metadata()
844
- resp = await self._client.is_blockhash_valid(req, metadata=metadata)
938
+ resp = await self._client.IsBlockhashValid(req, metadata=metadata)
845
939
 
846
940
  return IsBlockhashValidResponse(slot=resp.slot, valid=resp.valid)
847
941
 
@@ -855,7 +949,7 @@ class YellowstoneGrpc:
855
949
 
856
950
  req = geyser_pb2.PingRequest(count=count)
857
951
  metadata = self._get_metadata()
858
- resp = await self._client.ping(req, metadata=metadata)
952
+ resp = await self._client.Ping(req, metadata=metadata)
859
953
 
860
954
  return PongResponse(count=resp.count)
861
955
 
@@ -869,7 +963,7 @@ class YellowstoneGrpc:
869
963
 
870
964
  req = geyser_pb2.SubscribeReplayInfoRequest()
871
965
  metadata = self._get_metadata()
872
- resp = await self._client.subscribe_replay_info(req, metadata=metadata)
966
+ resp = await self._client.SubscribeReplayInfo(req, metadata=metadata)
873
967
 
874
968
  result = SubscribeReplayInfoResponse()
875
969
  if resp.HasField('first_available'):