sol-parser-sdk-python 0.5.8__py3-none-any.whl → 0.5.10__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,
@@ -449,6 +517,7 @@ class YellowstoneGrpc:
449
517
  signature = base58.b58encode(bytes(info.signature)).decode("ascii") if info.signature else ""
450
518
 
451
519
  from .grpc_instruction_parser import (
520
+ subscribe_transaction_failed,
452
521
  detect_pumpfun_create_from_logs,
453
522
  enrich_dex_events_with_subscribe_tx_info,
454
523
  parse_instructions_enhanced_from_subscribe_tx_info,
@@ -458,6 +527,9 @@ class YellowstoneGrpc:
458
527
  from .pumpfun_fee_enrich import enrich_pumpfun_same_tx_post_merge
459
528
  from .grpc_types import EventType
460
529
 
530
+ if subscribe_transaction_failed(info):
531
+ return # Suppress both instruction events and rolled-back log events.
532
+
461
533
  instruction_events = parse_instructions_enhanced_from_subscribe_tx_info(
462
534
  info,
463
535
  slot,
@@ -554,6 +626,15 @@ class YellowstoneGrpc:
554
626
  block_time_us=0 if block_time_us is None else block_time_us,
555
627
  grpc_recv_us=grpc_recv_us,
556
628
  )
629
+ # Raw snapshots are opt-in, including closure updates with empty bytes.
630
+ from .event_types import DexEvent
631
+ from .grpc_types import EventType
632
+ from .liquidity_snapshot import RawAccountSnapshotEvent
633
+ if EventType.ACCOUNT_RAW_SNAPSHOT in (getattr(event_type_filter, "include_only", None) or []):
634
+ self._queue_event_nowait(queue, DexEvent(
635
+ type=EventType.ACCOUNT_RAW_SNAPSHOT,
636
+ data=RawAccountSnapshotEvent(metadata, account, int(acc.write_version), bool(update.account.is_startup)),
637
+ ))
557
638
  ev = parse_account_unified(account, metadata, event_type_filter)
558
639
  if ev is not None:
559
640
  self._queue_event_nowait(queue, ev)
@@ -582,6 +663,7 @@ class YellowstoneGrpc:
582
663
 
583
664
  try:
584
665
  while not cancel_event.is_set():
666
+ if isinstance(queue, DexEventQueue): queue.transition("connecting")
585
667
  outgoing: asyncio.Queue = asyncio.Queue()
586
668
 
587
669
  async def request_iterator():
@@ -606,6 +688,7 @@ class YellowstoneGrpc:
606
688
  async for pb_update in self._client.Subscribe(request_iterator(), metadata=metadata):
607
689
  if cancel_event.is_set():
608
690
  break
691
+ if isinstance(queue, DexEventQueue): queue.transition("connected")
609
692
  if pb_update.HasField("ping"):
610
693
  await outgoing.put(
611
694
  geyser_pb2.SubscribeRequest(
@@ -627,6 +710,13 @@ class YellowstoneGrpc:
627
710
  block_time_us,
628
711
  order_dispatcher,
629
712
  )
713
+ if update.block_meta:
714
+ from .block_meta import parse_block_meta_update
715
+ from .grpc_types import EventType
716
+ if event_type_filter is None or event_type_filter.should_include(EventType.BLOCK_META):
717
+ # Match transaction/account overflow semantics. A full
718
+ # consumer queue must not stall gRPC reads and pings.
719
+ self._queue_event_nowait(queue, parse_block_meta_update(update.block_meta,grpc_recv_us,block_time_us))
630
720
  if update.account:
631
721
  await self._enqueue_account_dex_event(
632
722
  queue,
@@ -635,10 +725,15 @@ class YellowstoneGrpc:
635
725
  grpc_recv_us,
636
726
  block_time_us,
637
727
  )
728
+ if not cancel_event.is_set():
729
+ raise ConnectionError("Yellowstone DEX stream ended")
638
730
  delay = 1.0
639
731
  except asyncio.CancelledError:
640
732
  break
641
- except Exception:
733
+ except Exception as error:
734
+ if isinstance(queue, DexEventQueue):
735
+ queue.report_error(error)
736
+ queue.transition("reconnecting", continuity_broken=True, reconnect=True)
642
737
  order_dispatcher.flush_all(lambda ev: self._queue_event_nowait(queue, ev))
643
738
  if cancel_event.is_set():
644
739
  break
@@ -646,10 +741,13 @@ class YellowstoneGrpc:
646
741
  delay = min(delay * 2.0, 60.0)
647
742
  finally:
648
743
  pump_task.cancel()
744
+ await asyncio.gather(pump_task, return_exceptions=True)
649
745
  await outgoing.put(None)
650
746
  finally:
651
747
  if flush_task is not None:
652
748
  flush_task.cancel()
749
+ await asyncio.gather(flush_task, return_exceptions=True)
750
+ if isinstance(queue, DexEventQueue): queue.transition("stopped", continuity_broken=True)
653
751
  order_dispatcher.flush_all(lambda ev: self._queue_event_nowait(queue, ev))
654
752
 
655
753
  async def subscribe_transactions(
@@ -766,7 +864,7 @@ class YellowstoneGrpc:
766
864
  req.commitment = commitment.value
767
865
 
768
866
  metadata = self._get_metadata()
769
- resp = await self._client.get_latest_blockhash(req, metadata=metadata)
867
+ resp = await self._client.GetLatestBlockhash(req, metadata=metadata)
770
868
 
771
869
  return GetLatestBlockhashResponse(
772
870
  slot=resp.slot,
@@ -789,7 +887,7 @@ class YellowstoneGrpc:
789
887
  req.commitment = commitment.value
790
888
 
791
889
  metadata = self._get_metadata()
792
- resp = await self._client.get_block_height(req, metadata=metadata)
890
+ resp = await self._client.GetBlockHeight(req, metadata=metadata)
793
891
 
794
892
  return GetBlockHeightResponse(block_height=resp.block_height)
795
893
 
@@ -808,7 +906,7 @@ class YellowstoneGrpc:
808
906
  req.commitment = commitment.value
809
907
 
810
908
  metadata = self._get_metadata()
811
- resp = await self._client.get_slot(req, metadata=metadata)
909
+ resp = await self._client.GetSlot(req, metadata=metadata)
812
910
 
813
911
  return GetSlotResponse(slot=resp.slot)
814
912
 
@@ -822,7 +920,7 @@ class YellowstoneGrpc:
822
920
 
823
921
  req = geyser_pb2.GetVersionRequest()
824
922
  metadata = self._get_metadata()
825
- resp = await self._client.get_version(req, metadata=metadata)
923
+ resp = await self._client.GetVersion(req, metadata=metadata)
826
924
 
827
925
  return GetVersionResponse(version=resp.version)
828
926
 
@@ -841,7 +939,7 @@ class YellowstoneGrpc:
841
939
  req.commitment = commitment.value
842
940
 
843
941
  metadata = self._get_metadata()
844
- resp = await self._client.is_blockhash_valid(req, metadata=metadata)
942
+ resp = await self._client.IsBlockhashValid(req, metadata=metadata)
845
943
 
846
944
  return IsBlockhashValidResponse(slot=resp.slot, valid=resp.valid)
847
945
 
@@ -855,7 +953,7 @@ class YellowstoneGrpc:
855
953
 
856
954
  req = geyser_pb2.PingRequest(count=count)
857
955
  metadata = self._get_metadata()
858
- resp = await self._client.ping(req, metadata=metadata)
956
+ resp = await self._client.Ping(req, metadata=metadata)
859
957
 
860
958
  return PongResponse(count=resp.count)
861
959
 
@@ -869,7 +967,7 @@ class YellowstoneGrpc:
869
967
 
870
968
  req = geyser_pb2.SubscribeReplayInfoRequest()
871
969
  metadata = self._get_metadata()
872
- resp = await self._client.subscribe_replay_info(req, metadata=metadata)
970
+ resp = await self._client.SubscribeReplayInfo(req, metadata=metadata)
873
971
 
874
972
  result = SubscribeReplayInfoResponse()
875
973
  if resp.HasField('first_available'):
@@ -229,6 +229,16 @@ def _meta_dict(
229
229
  return m
230
230
 
231
231
 
232
+ def subscribe_transaction_failed(info: SubscribeUpdateTransactionInfo) -> bool:
233
+ """Read protobuf error presence, including errors with an empty encoded payload."""
234
+ if not info.meta_raw:
235
+ return False
236
+ from . import solana_storage_pb2 as sol_pb
237
+ meta = sol_pb.TransactionStatusMeta()
238
+ meta.ParseFromString(info.meta_raw)
239
+ return meta.HasField("err")
240
+
241
+
232
242
  def parse_instructions_enhanced_from_subscribe_tx_info(
233
243
  info: SubscribeUpdateTransactionInfo,
234
244
  slot: int,
@@ -268,7 +278,7 @@ def parse_instructions_enhanced_from_parsed(
268
278
  transaction_pb: Any = None,
269
279
  ) -> List[DexEvent]:
270
280
  """接受已 Parse 的 ``Message`` 与 ``TransactionStatusMeta``(来自 ``solana_storage_pb2``)。"""
271
- if not should_parse_instructions(filter):
281
+ if meta.HasField("err") or not should_parse_instructions(filter):
272
282
  return []
273
283
 
274
284
  grpc_us = int(time.time() * 1_000_000) if grpc_recv_us is None else grpc_recv_us
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"
@@ -435,6 +437,18 @@ class EventMetadata:
435
437
  recent_blockhash: str = ""
436
438
  is_created_buy: bool = False
437
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
+
438
452
 
439
453
  @dataclass
440
454
  class ClientConfig:
@@ -747,6 +761,8 @@ class Protocol(str, Enum):
747
761
  PUMP_SWAP = "PumpSwap"
748
762
  PUMP_FEES = "PumpFees"
749
763
  RAYDIUM_LAUNCHLAB = "RaydiumLaunchlab"
764
+ LAUNCH_LAB = "LaunchLab"
765
+ STONK_FUN = "StonkFun"
750
766
  RAYDIUM_CPMM = "RaydiumCpmm"
751
767
  RAYDIUM_CLMM = "RaydiumClmm"
752
768
  RAYDIUM_AMM_V4 = "RaydiumAmmV4"
@@ -762,6 +778,8 @@ _PROTOCOL_PROGRAM_IDS: Dict[Protocol, List[str]] = {
762
778
  Protocol.PUMP_FUN: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"],
763
779
  Protocol.PUMP_SWAP: ["pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"],
764
780
  Protocol.PUMP_FEES: ["pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"],
781
+ Protocol.LAUNCH_LAB: ["LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"],
782
+ Protocol.STONK_FUN: ["LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"],
765
783
  Protocol.RAYDIUM_LAUNCHLAB: ["LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"],
766
784
  Protocol.RAYDIUM_CPMM: ["CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C"],
767
785
  Protocol.RAYDIUM_CLMM: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"],
@@ -981,6 +999,7 @@ class SubscribeUpdateBlockMeta:
981
999
  parent_slot: int = 0
982
1000
  parent_blockhash: str = ""
983
1001
  executed_transaction_count: int = 0
1002
+ block_time: Optional[int] = None
984
1003
 
985
1004
 
986
1005
  @dataclass
@@ -368,7 +368,7 @@ def normal_instruction_data_may_parse(program_id: str, instruction_data: bytes)
368
368
  if not instruction_data:
369
369
  return False
370
370
  if program_id == RAYDIUM_AMM_V4_PROGRAM_ID:
371
- return instruction_data[0] in (1, 3, 4, 7, 9, 11)
371
+ return instruction_data[0] in (1, 3, 4, 7, 9, 11, 16, 17)
372
372
  if program_id == METEORA_DLMM_PROGRAM_ID:
373
373
  disc = _disc8(instruction_data)
374
374
  return disc in _DLMM_INSTRUCTION_DISCS if disc is not None else False
@@ -851,9 +851,9 @@ def _parse_pumpfun_create_v2(data: bytes, accounts: List[str], meta: EventMetada
851
851
  is_cashback_enabled=is_cashback_enabled,
852
852
  creator_fee_bps=creator_fee_bps,
853
853
  is_holder_reward=is_holder_reward,
854
- quote_mint=_get_account_safe(accounts, 16) or Z,
855
- quote_vault=_get_account_safe(accounts, 17),
856
- quote_token_program=_get_account_safe(accounts, 18),
854
+ quote_mint=Z,
855
+ quote_vault=Z,
856
+ quote_token_program=Z,
857
857
  virtual_quote_reserves=0,
858
858
  observed_fee_recipient="",
859
859
  ),
@@ -1710,21 +1710,27 @@ def parse_raydium_amm_v4_instruction(
1710
1710
  instr_type = data[0]
1711
1711
  meta = _make_meta(signature, slot, tx_index, block_time_us, grpc_recv_us)
1712
1712
 
1713
- if instr_type in (9, 11): # SwapBaseIn / SwapBaseOut
1713
+ if instr_type in (9, 11, 16, 17): # SwapBaseIn / SwapBaseOut
1714
1714
  if len(data) < 17:
1715
1715
  return None
1716
+ modern = instr_type in (16,17)
1717
+ if modern and len(accounts)<8:return None
1716
1718
  first, second = struct.unpack_from("<QQ", data, 1)
1717
1719
  shift = 1 if len(accounts) == 17 else 0
1718
1720
  def g(index: int) -> str:
1719
- return _get_account_safe(accounts, index - shift if index >= 5 else index)
1721
+ if modern:
1722
+ index = {0:0,1:1,2:2,5:3,6:4,15:5,16:6,17:7}.get(index,-1)
1723
+ else:
1724
+ index = index-shift if index>=5 else index
1725
+ return _get_account_safe(accounts,index)
1720
1726
  return legacy_dict_to_dex_event({"RaydiumAmmV4Swap": {
1721
1727
  "metadata": meta,
1722
1728
  "amm": _get_account_safe(accounts, 1),
1723
1729
  "user_source_owner": g(17),
1724
- "amount_in": first if instr_type == 9 else 0,
1725
- "minimum_amount_out": second if instr_type == 9 else 0,
1726
- "max_amount_in": first if instr_type == 11 else 0,
1727
- "amount_out": second if instr_type == 11 else 0,
1730
+ "amount_in": first if instr_type in (9,16) else 0,
1731
+ "minimum_amount_out": second if instr_type in (9,16) else 0,
1732
+ "max_amount_in": first if instr_type in (11,17) else 0,
1733
+ "amount_out": second if instr_type in (11,17) else 0,
1728
1734
  "token_program": g(0), "amm_authority": g(2), "amm_open_orders": g(3),
1729
1735
  "pool_coin_token_account": g(5), "pool_pc_token_account": g(6),
1730
1736
  "serum_program": g(7), "serum_market": g(8), "serum_bids": g(9),
@@ -1843,7 +1849,7 @@ def parse_raydium_launchlab_instruction(
1843
1849
  payload = data[8:]
1844
1850
 
1845
1851
  if discriminator == _DISC_RAYDIUM_LAUNCHLAB_TRADE:
1846
- if len(payload) < 139:
1852
+ if len(payload) != 139 or payload[136] > 1 or payload[137] > 2 or payload[138] > 1:
1847
1853
  return None
1848
1854
  return legacy_dict_to_dex_event({"RaydiumLaunchlabTrade": {
1849
1855
  "metadata": meta,
@@ -1854,6 +1860,19 @@ def parse_raydium_launchlab_instruction(
1854
1860
  "is_buy": payload[136] == 0,
1855
1861
  "trade_direction": "Buy" if payload[136] == 0 else "Sell",
1856
1862
  "exact_in": payload[138] == 1,
1863
+ "pool_status": ("Fund","Migrate","Trade")[payload[137]],
1864
+ "total_base_sell": _u64_payload(payload,32),
1865
+ "virtual_base": _u64_payload(payload,40),
1866
+ "virtual_quote": _u64_payload(payload,48),
1867
+ "real_base_before": _u64_payload(payload,56),
1868
+ "real_quote_before": _u64_payload(payload,64),
1869
+ "real_base_after": _u64_payload(payload,72),
1870
+ "real_quote_after": _u64_payload(payload,80),
1871
+ "protocol_fee": _u64_payload(payload,104),
1872
+ "platform_fee": _u64_payload(payload,112),
1873
+ "creator_fee": _u64_payload(payload,120),
1874
+ "share_fee": _u64_payload(payload,128),
1875
+
1857
1876
  }})
1858
1877
  if discriminator == _DISC_RAYDIUM_LAUNCHLAB_POOL_CREATE:
1859
1878
  if len(payload) < 97:
@@ -1897,6 +1916,20 @@ def parse_raydium_launchlab_instruction(
1897
1916
  "is_buy": is_buy,
1898
1917
  "trade_direction": "Buy" if is_buy else "Sell",
1899
1918
  "exact_in": exact_in,
1919
+ "global_config": _get_account_safe(accounts,2),
1920
+ "platform_config": _get_account_safe(accounts,3),
1921
+ "user_base_token": _get_account_safe(accounts,5),
1922
+ "user_quote_token": _get_account_safe(accounts,6),
1923
+ "base_vault": _get_account_safe(accounts,7),
1924
+ "quote_vault": _get_account_safe(accounts,8),
1925
+ "base_mint": _get_account_safe(accounts,9),
1926
+ "quote_mint": _get_account_safe(accounts,10),
1927
+ "base_token_program": _get_account_safe(accounts,11),
1928
+ "quote_token_program": _get_account_safe(accounts,12),
1929
+ "system_program": _get_account_safe(accounts,15),
1930
+ "platform_associated_account": _get_account_safe(accounts,16),
1931
+ "creator_associated_account": _get_account_safe(accounts,17),
1932
+
1900
1933
  }})
1901
1934
  if discriminator in (
1902
1935
  _IX_RAYDIUM_LAUNCHLAB_INITIALIZE,
@@ -1922,7 +1955,16 @@ def parse_raydium_launchlab_instruction(
1922
1955
  _IX_RAYDIUM_LAUNCHLAB_MIGRATE_TO_AMM,
1923
1956
  _IX_RAYDIUM_LAUNCHLAB_MIGRATE_TO_CPSWAP,
1924
1957
  ):
1925
- return None
1958
+ cp = discriminator == _IX_RAYDIUM_LAUNCHLAB_MIGRATE_TO_CPSWAP
1959
+ if len(accounts) < (28 if cp else 32) or (not cp and len(payload) < 9): return None
1960
+ return legacy_dict_to_dex_event({"RaydiumLaunchlabMigrateAmm": {
1961
+ "metadata": meta, "old_pool": _get_account_safe(accounts,17 if cp else 23),
1962
+ "new_pool": _get_account_safe(accounts,5 if cp else 13), "user": _get_account_safe(accounts,0),
1963
+ "liquidity_amount": 0, "liquidity_amount_known": False,
1964
+ "base_mint": _get_account_safe(accounts,1), "quote_mint": _get_account_safe(accounts,2),
1965
+ "platform_config": _get_account_safe(accounts,3) if cp else Z,
1966
+ "destination_program": _get_account_safe(accounts,4 if cp else 12),
1967
+ }})
1926
1968
 
1927
1969
  return None
1928
1970
 
sol_parser/json_util.py CHANGED
@@ -11,7 +11,7 @@ from .grpc_types import EventType
11
11
 
12
12
 
13
13
  def dex_event_json_dumps(obj: Any, **kwargs: Any) -> str:
14
- return json.dumps(obj, default=str, **kwargs)
14
+ return json.dumps(_precise_metadata(dex_event_to_jsonable(obj)), default=str, **kwargs)
15
15
 
16
16
 
17
17
  def dex_event_to_jsonable(ev: Any) -> Any:
@@ -39,3 +39,17 @@ def format_dex_event_json(ev: Any, *, indent: int = 2, ensure_ascii: bool = Fals
39
39
  indent=indent,
40
40
  ensure_ascii=ensure_ascii,
41
41
  )
42
+
43
+
44
+ def _precise_metadata(obj):
45
+ """Match Node exact metadata JSON without changing floating latency metrics."""
46
+ if isinstance(obj,dict):
47
+ result={k:_precise_metadata(v) for k,v in obj.items()}
48
+ if "signature" in obj and all(k in obj for k in ("slot","tx_index","block_time_us","grpc_recv_us")):
49
+ from .grpc_types import EventMetadata
50
+ validated=EventMetadata(**{k:obj[k] for k in ("signature","slot","tx_index","block_time_us","grpc_recv_us")})
51
+ for k in ("slot","tx_index","block_time_us","grpc_recv_us"):result[k]=str(getattr(validated,k))
52
+ return result
53
+ if isinstance(obj,(tuple,list)):return [_precise_metadata(v) for v in obj]
54
+ if is_dataclass(obj):return _precise_metadata(asdict(obj))
55
+ return obj
@@ -0,0 +1,101 @@
1
+ """Native liquidity identity decoders and immutable raw subscription snapshots."""
2
+
3
+ from __future__ import annotations
4
+ from dataclasses import dataclass
5
+ import base58
6
+ from .accounts import AccountData
7
+ from .grpc_types import EventMetadata
8
+ from .transaction_route import PROGRAMS
9
+
10
+
11
+ @dataclass
12
+ class RawAccountSnapshotEvent:
13
+ metadata: EventMetadata
14
+ account: AccountData
15
+ write_version: int
16
+ is_startup: bool
17
+
18
+
19
+ @dataclass
20
+ class LiquidityAccountSnapshotEvent:
21
+ metadata: EventMetadata
22
+ pubkey: str
23
+ owner: str
24
+ kind: dict | str
25
+ data: bytes
26
+
27
+
28
+ def parse_liquidity_account(account: AccountData, metadata: EventMetadata):
29
+ if account.lamports <= 0 or account.executable:
30
+ return None
31
+ d, owner = bytes(account.data), account.owner
32
+ disc = d[:8]
33
+
34
+ def key(o):
35
+ return base58.b58encode(d[o : o + 32]).decode()
36
+
37
+ def number(o, n, signed=False):
38
+ return int.from_bytes(d[o : o + n], "little", signed=signed)
39
+
40
+ kind = None
41
+ if owner == PROGRAMS["LaunchLab"]:
42
+ if disc == bytes([247, 237, 227, 245, 215, 195, 222, 70]) and len(d) >= 429:
43
+ kind = {
44
+ "LaunchLabPool": dict(
45
+ base_mint=key(205),
46
+ quote_mint=key(237),
47
+ global_config=key(141),
48
+ platform_config=key(173),
49
+ )
50
+ }
51
+ elif disc == bytes([149, 8, 156, 202, 160, 252, 176, 217]) and len(d) >= 35:
52
+ kind = "LaunchLabGlobalConfig"
53
+ elif disc == bytes([160, 78, 128, 0, 248, 83, 230, 160]) and len(d) >= 728:
54
+ kind = "LaunchLabPlatformConfig"
55
+ elif owner == PROGRAMS["MeteoraDlmm"]:
56
+ if disc == bytes([33, 11, 49, 98, 181, 101, 177, 13]) and len(d) >= 904:
57
+ kind = {
58
+ "DlmmPool": dict(
59
+ token_x_mint=key(88),
60
+ token_y_mint=key(120),
61
+ active_id=number(76, 4, True),
62
+ bin_step=number(80, 2),
63
+ )
64
+ }
65
+ elif disc == bytes([92, 142, 92, 220, 5, 148, 70, 181]) and len(d) >= 10136:
66
+ kind = {"DlmmBinArray": dict(pool=key(24), index=number(8, 8, True))}
67
+ elif disc == bytes([80, 111, 124, 113, 55, 237, 18, 5]) and len(d) >= 1576:
68
+ kind = {"DlmmBitmap": dict(pool=key(8))}
69
+ elif owner == PROGRAMS["OrcaWhirlpool"]:
70
+ if disc == bytes([17, 216, 246, 142, 225, 199, 218, 56]) and len(d) >= 148:
71
+ bitmap, offset = number(44, 16), 60
72
+ if bitmap >> 88:
73
+ return None
74
+ for i in range(88):
75
+ if (
76
+ offset >= len(d)
77
+ or d[offset] not in (0, 1)
78
+ or d[offset] != (bitmap >> i) & 1
79
+ ):
80
+ return None
81
+ offset += 1 + (112 if d[offset] else 0)
82
+ if offset > len(d):
83
+ return None
84
+ kind = {
85
+ "OrcaDynamicTickArray": dict(
86
+ pool=key(12),
87
+ start_tick_index=number(8, 4, True),
88
+ tick_bitmap=bitmap,
89
+ )
90
+ }
91
+ elif disc == bytes([139, 194, 131, 179, 140, 179, 229, 244]) and len(d) >= 254:
92
+ kind = {"OrcaAdaptiveOracle": dict(pool=key(8))}
93
+ elif (
94
+ owner == PROGRAMS["RaydiumClmm"]
95
+ and disc == bytes([60, 150, 36, 219, 97, 128, 139, 153])
96
+ and len(d) >= 1832
97
+ ):
98
+ kind = {"ClmmBitmap": dict(pool=key(8))}
99
+ if kind is None:
100
+ return None
101
+ return LiquidityAccountSnapshotEvent(metadata, account.pubkey, owner, kind, d)