sol-parser-sdk-python 0.5.7__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/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'):
@@ -7,17 +7,18 @@ from typing import Any, Dict, List, Optional, Tuple
7
7
 
8
8
  import base58
9
9
 
10
- from .account_dispatcher import fill_accounts_with_owned_keys, fill_data
10
+ from .account_dispatcher import fill_accounts_with_owned_keys, fill_data, fill_token_balances
11
11
  from .event_types import DexEvent
12
12
  from .grpc_types import (
13
13
  EventMetadata,
14
+ EventType,
14
15
  EventTypeFilter,
15
16
  SubscribeUpdateTransactionInfo,
16
17
  event_type_filter_allows_instruction_parsing,
17
18
  )
18
19
  from .inner_instruction_parser import parse_inner_instruction
19
20
  from .instructions import parse_inner_compiled_instruction_if_supported, parse_instruction_unified
20
- from .merger import merge_dex_events
21
+ from .merger import try_merge_dex_events
21
22
  from .pumpfun_fee_enrich import enrich_pumpfun_same_tx_post_merge
22
23
 
23
24
 
@@ -73,6 +74,7 @@ def apply_account_fill_to_events(
73
74
  for ev in events:
74
75
  fill_accounts_with_owned_keys(ev, meta_pb, tx_pb, invokes_raw)
75
76
  fill_data(ev, meta_pb, tx_pb, invokes_str)
77
+ fill_token_balances(ev, meta_pb, tx_pb)
76
78
  recent_bh = ""
77
79
  if msg.recent_blockhash:
78
80
  recent_bh = base58.b58encode(bytes(msg.recent_blockhash)).decode("ascii")
@@ -124,36 +126,86 @@ def should_parse_instructions(filter: Optional[EventTypeFilter]) -> bool:
124
126
  return event_type_filter_allows_instruction_parsing(list(inc))
125
127
 
126
128
 
127
- def merge_instruction_events(
128
- events: List[Tuple[int, Optional[int], DexEvent]],
129
- ) -> List[DexEvent]:
129
+ _DLMM_EVENT_TYPES = {
130
+ EventType.METEORA_DLMM_SWAP,
131
+ EventType.METEORA_DLMM_ADD_LIQUIDITY,
132
+ EventType.METEORA_DLMM_REMOVE_LIQUIDITY,
133
+ EventType.METEORA_DLMM_INITIALIZE_POOL,
134
+ EventType.METEORA_DLMM_INITIALIZE_BIN_ARRAY,
135
+ EventType.METEORA_DLMM_CREATE_POSITION,
136
+ EventType.METEORA_DLMM_CLOSE_POSITION,
137
+ EventType.METEORA_DLMM_CLAIM_FEE,
138
+ }
139
+
140
+
141
+ def _is_dlmm_event_cpi(data: bytes) -> bool:
142
+ return len(data) >= 16 and (
143
+ data[:8] == bytes((228, 69, 165, 46, 81, 203, 154, 29))
144
+ or data[8:16] == bytes((155, 167, 108, 32, 122, 76, 173, 64))
145
+ )
146
+
147
+
148
+ def merge_instruction_events(events: List[Tuple[Any, ...]]) -> List[DexEvent]:
130
149
  """对齐 Rust ``merge_instruction_events``。"""
131
150
  if not events:
132
151
  return []
133
- events = sorted(events, key=lambda x: (x[0], 0 if x[1] is None else 1 + x[1]))
152
+ normalized = events if all(len(item) == 5 for item in events) else [
153
+ (item[0], item[1], None, False, item[2]) if len(item) == 3 else item
154
+ for item in events
155
+ ]
156
+ normalized.sort(key=lambda x: (x[0], 0 if x[1] is None else 1 + x[1]))
134
157
  result: List[DexEvent] = []
135
- pending_outer: Optional[Tuple[int, DexEvent]] = None
158
+ outer_target: Optional[Tuple[int, int]] = None
159
+ dlmm_targets: List[Tuple[int, Optional[int], int]] = []
136
160
 
137
- for outer_idx, inner_idx, event in events:
161
+ for outer_idx, inner_idx, stack_height, is_dlmm_event_cpi, event in normalized:
138
162
  if inner_idx is None:
139
- if pending_outer is not None:
140
- result.append(pending_outer[1])
141
- pending_outer = (outer_idx, event)
142
- else:
143
- if pending_outer is not None:
144
- po_idx, mut_outer = pending_outer
145
- pending_outer = None
146
- if po_idx == outer_idx:
147
- merge_dex_events(mut_outer, event)
148
- pending_outer = (outer_idx, mut_outer)
149
- else:
150
- result.append(mut_outer)
151
- result.append(event)
163
+ result.append(event)
164
+ target_idx = len(result) - 1
165
+ outer_target = (outer_idx, target_idx)
166
+ dlmm_targets = (
167
+ [(outer_idx, stack_height, target_idx)] if event.type in _DLMM_EVENT_TYPES else []
168
+ )
169
+ continue
170
+
171
+ if is_dlmm_event_cpi:
172
+ for candidate_idx in range(len(dlmm_targets) - 1, -1, -1):
173
+ target_outer, target_height, target_idx = dlmm_targets[candidate_idx]
174
+ direct_child = (
175
+ target_height is None
176
+ or stack_height is None
177
+ or stack_height == target_height + 1
178
+ )
179
+ if target_outer == outer_idx and direct_child:
180
+ del dlmm_targets[candidate_idx + 1:]
181
+ if try_merge_dex_events(result[target_idx], event):
182
+ break
152
183
  else:
153
184
  result.append(event)
185
+ continue
186
+
187
+ target_idx: Optional[int] = None
188
+ if outer_target is not None and outer_target[0] == outer_idx:
189
+ if try_merge_dex_events(result[outer_target[1]], event):
190
+ target_idx = outer_target[1]
191
+ if target_idx is None:
192
+ result.append(event)
193
+ target_idx = len(result) - 1
194
+
195
+ if event.type in _DLMM_EVENT_TYPES:
196
+ if stack_height is None:
197
+ dlmm_targets.clear()
198
+ else:
199
+ while dlmm_targets and (
200
+ dlmm_targets[-1][0] != outer_idx
201
+ or (
202
+ dlmm_targets[-1][1] is not None
203
+ and dlmm_targets[-1][1] >= stack_height
204
+ )
205
+ ):
206
+ dlmm_targets.pop()
207
+ dlmm_targets.append((outer_idx, stack_height, target_idx))
154
208
 
155
- if pending_outer is not None:
156
- result.append(pending_outer[1])
157
209
  return result
158
210
 
159
211
 
@@ -260,7 +312,7 @@ def parse_instructions_enhanced_from_parsed(
260
312
 
261
313
  is_created_buy = detect_pumpfun_create_from_logs(list(meta.log_messages))
262
314
 
263
- result: List[Tuple[int, Optional[int], DexEvent]] = []
315
+ result: List[Tuple[Any, ...]] = []
264
316
 
265
317
  for i, ix in enumerate(msg.instructions):
266
318
  pid_idx = ix.program_id_index
@@ -272,7 +324,7 @@ def parse_instructions_enhanced_from_parsed(
272
324
  data, accounts, signature, slot, tx_index, block_time_us, grpc_us, filter, pid
273
325
  )
274
326
  if ev:
275
- result.append((i, None, ev))
327
+ result.append((i, None, 1, False, ev))
276
328
 
277
329
  for inner in meta.inner_instructions:
278
330
  outer_idx = inner.index
@@ -298,7 +350,13 @@ def parse_instructions_enhanced_from_parsed(
298
350
  is_created_buy,
299
351
  )
300
352
  if ev:
301
- result.append((int(outer_idx), j, ev))
353
+ result.append((
354
+ int(outer_idx),
355
+ j,
356
+ int(inner_ix.stack_height) if inner_ix.HasField("stack_height") else None,
357
+ pid == METEORA_DLMM_PROGRAM_ID and _is_dlmm_event_cpi(data),
358
+ ev,
359
+ ))
302
360
 
303
361
  merged = merge_instruction_events(result)
304
362
  enrich_pumpfun_same_tx_post_merge(merged)
@@ -310,6 +368,7 @@ def parse_instructions_enhanced_from_parsed(
310
368
  for ev in merged:
311
369
  fill_accounts_with_owned_keys(ev, meta, transaction_pb, invokes_raw)
312
370
  fill_data(ev, meta, transaction_pb, invokes_str)
371
+ fill_token_balances(ev, meta, transaction_pb)
313
372
 
314
373
  for ev in merged:
315
374
  if isinstance(ev.data, object) and hasattr(ev.data, "metadata"):
sol_parser/grpc_types.py CHANGED
@@ -37,6 +37,8 @@ class EventType(str, Enum):
37
37
  """事件类型"""
38
38
  # Block
39
39
  BLOCK_META = "BlockMeta"
40
+ ACCOUNT_RAW_SNAPSHOT = "AccountRawSnapshot"
41
+ ACCOUNT_LIQUIDITY_SNAPSHOT = "AccountLiquiditySnapshot"
40
42
  # RaydiumLaunchlab
41
43
  RAYDIUM_LAUNCHLAB_TRADE = "RaydiumLaunchlabTrade"
42
44
  RAYDIUM_LAUNCHLAB_POOL_CREATE = "RaydiumLaunchlabPoolCreate"
@@ -115,6 +117,10 @@ class EventType(str, Enum):
115
117
  METEORA_DAMM_V2_INITIALIZE_POOL = "MeteoraDammV2InitializePool"
116
118
  METEORA_DAMM_V2_CREATE_POSITION = "MeteoraDammV2CreatePosition"
117
119
  METEORA_DAMM_V2_CLOSE_POSITION = "MeteoraDammV2ClosePosition"
120
+ METEORA_DAMM_V2_UPDATE_DELEGATE_PERMISSION = "MeteoraDammV2UpdateDelegatePermission"
121
+ METEORA_DAMM_V2_WITHDRAW_DEAD_LIQUIDITY_REWARD = "MeteoraDammV2WithdrawDeadLiquidityReward"
122
+ METEORA_DAMM_V2_CREATE_CONFIG = "MeteoraDammV2CreateConfig"
123
+ METEORA_DAMM_V2_CREATE_DYNAMIC_CONFIG = "MeteoraDammV2CreateDynamicConfig"
118
124
  # Meteora DBC
119
125
  METEORA_DBC_SWAP = "MeteoraDbcSwap"
120
126
  METEORA_DBC_INITIALIZE_POOL = "MeteoraDbcInitializePool"
@@ -206,6 +212,10 @@ METEORA_DAMM_V2_FILTER_TYPES = (
206
212
  EventType.METEORA_DAMM_V2_CLOSE_POSITION,
207
213
  EventType.METEORA_DAMM_V2_INITIALIZE_POOL,
208
214
  EventType.METEORA_DAMM_V2_REMOVE_LIQUIDITY,
215
+ EventType.METEORA_DAMM_V2_UPDATE_DELEGATE_PERMISSION,
216
+ EventType.METEORA_DAMM_V2_WITHDRAW_DEAD_LIQUIDITY_REWARD,
217
+ EventType.METEORA_DAMM_V2_CREATE_CONFIG,
218
+ EventType.METEORA_DAMM_V2_CREATE_DYNAMIC_CONFIG,
209
219
  )
210
220
  METEORA_DBC_FILTER_TYPES = (
211
221
  EventType.METEORA_DBC_SWAP,
@@ -372,6 +382,10 @@ def all_event_types() -> List[EventType]:
372
382
  EventType.METEORA_DAMM_V2_INITIALIZE_POOL,
373
383
  EventType.METEORA_DAMM_V2_CREATE_POSITION,
374
384
  EventType.METEORA_DAMM_V2_CLOSE_POSITION,
385
+ EventType.METEORA_DAMM_V2_UPDATE_DELEGATE_PERMISSION,
386
+ EventType.METEORA_DAMM_V2_WITHDRAW_DEAD_LIQUIDITY_REWARD,
387
+ EventType.METEORA_DAMM_V2_CREATE_CONFIG,
388
+ EventType.METEORA_DAMM_V2_CREATE_DYNAMIC_CONFIG,
375
389
  # Meteora DBC
376
390
  EventType.METEORA_DBC_SWAP,
377
391
  EventType.METEORA_DBC_INITIALIZE_POOL,
@@ -423,6 +437,18 @@ class EventMetadata:
423
437
  recent_blockhash: str = ""
424
438
  is_created_buy: bool = False
425
439
 
440
+ def __post_init__(self):
441
+ for name,signed in (("slot",False),("tx_index",False),("block_time_us",True),("grpc_recv_us",True)):
442
+ value=getattr(self,name)
443
+ if isinstance(value,str):
444
+ import re
445
+ if not re.fullmatch(r"-?\d+",value):raise ValueError(f"Invalid {name} integer")
446
+ value=int(value)
447
+ minimum=-(1<<63) if signed else 0
448
+ maximum=(1<<63)-1 if signed else (1<<64)-1
449
+ if type(value) is not int or not minimum<=value<=maximum:raise ValueError(f"{name} outside {'i64' if signed else 'u64'}")
450
+ setattr(self,name,value)
451
+
426
452
 
427
453
  @dataclass
428
454
  class ClientConfig:
@@ -735,6 +761,8 @@ class Protocol(str, Enum):
735
761
  PUMP_SWAP = "PumpSwap"
736
762
  PUMP_FEES = "PumpFees"
737
763
  RAYDIUM_LAUNCHLAB = "RaydiumLaunchlab"
764
+ LAUNCH_LAB = "LaunchLab"
765
+ STONK_FUN = "StonkFun"
738
766
  RAYDIUM_CPMM = "RaydiumCpmm"
739
767
  RAYDIUM_CLMM = "RaydiumClmm"
740
768
  RAYDIUM_AMM_V4 = "RaydiumAmmV4"
@@ -750,6 +778,8 @@ _PROTOCOL_PROGRAM_IDS: Dict[Protocol, List[str]] = {
750
778
  Protocol.PUMP_FUN: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"],
751
779
  Protocol.PUMP_SWAP: ["pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"],
752
780
  Protocol.PUMP_FEES: ["pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"],
781
+ Protocol.LAUNCH_LAB: ["LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"],
782
+ Protocol.STONK_FUN: ["LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"],
753
783
  Protocol.RAYDIUM_LAUNCHLAB: ["LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"],
754
784
  Protocol.RAYDIUM_CPMM: ["CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C"],
755
785
  Protocol.RAYDIUM_CLMM: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"],
@@ -969,6 +999,7 @@ class SubscribeUpdateBlockMeta:
969
999
  parent_slot: int = 0
970
1000
  parent_blockhash: str = ""
971
1001
  executed_transaction_count: int = 0
1002
+ block_time: Optional[int] = None
972
1003
 
973
1004
 
974
1005
  @dataclass
@@ -43,7 +43,7 @@ from .dex_parsers import (
43
43
  parse_cpmm_swap_in_from_data,
44
44
  parse_cpmm_swap_out_from_data,
45
45
  parse_cpmm_withdraw_from_data,
46
- parse_dlmm_from_program_data,
46
+ parse_dlmm_event_from_data,
47
47
  parse_meteora_damm_from_buf,
48
48
  parse_meteora_add_from_data,
49
49
  parse_meteora_bootstrap_from_data,
@@ -184,11 +184,6 @@ def _damm_buf_from_inner(disc16: bytes, inner: bytes) -> bytes:
184
184
  _RAYDIUM_LAUNCHLAB_TRADE = bytes([189, 219, 127, 211, 78, 230, 97, 238, 155, 167, 108, 32, 122, 76, 173, 64])
185
185
  _RAYDIUM_LAUNCHLAB_POOL_CREATE = bytes([151, 215, 226, 9, 118, 161, 115, 174, 155, 167, 108, 32, 122, 76, 173, 64])
186
186
 
187
- # DLMM(8 字节 event disc + payload)
188
- def _dlmm_buf_from_inner(disc16: bytes, inner: bytes) -> bytes:
189
- return disc16[:8] + inner
190
-
191
-
192
187
  def _meteora_pools_swap_inner(data: bytes, meta_d: dict) -> Optional[DexEvent]:
193
188
  if len(data) < 16:
194
189
  return None
@@ -440,6 +435,9 @@ def parse_inner_instruction(
440
435
  if program_id_b58 == METEORA_DLMM_PROGRAM_ID:
441
436
  if filter is not None and not event_type_filter_includes_meteora_dlmm(filter):
442
437
  return None
443
- return emit(parse_dlmm_from_program_data(_dlmm_buf_from_inner(disc16, inner), meta_d))
438
+ event_disc = _event_cpi_disc8(disc16)
439
+ if event_disc is None:
440
+ return None
441
+ return emit(parse_dlmm_event_from_data(event_disc, inner, meta_d))
444
442
 
445
443
  return None