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/__init__.py +24 -0
- sol_parser/account_dispatcher.py +46 -6
- sol_parser/account_fillers/pumpfun.py +6 -15
- sol_parser/account_fillers/raydium.py +6 -2
- sol_parser/account_fillers/raydium_launchlab.py +4 -0
- sol_parser/accounts/__init__.py +5 -0
- sol_parser/block_meta.py +29 -0
- sol_parser/dex_parsers.py +18 -1
- sol_parser/entries_decode.py +6 -80
- sol_parser/event_types.py +62 -0
- sol_parser/grpc/subscribe_builder.py +8 -0
- sol_parser/grpc_client.py +118 -20
- sol_parser/grpc_instruction_parser.py +11 -1
- sol_parser/grpc_types.py +19 -0
- sol_parser/instructions.py +54 -12
- sol_parser/json_util.py +15 -1
- sol_parser/liquidity_snapshot.py +101 -0
- sol_parser/rpc_parser.py +11 -0
- sol_parser/simulation_route.py +233 -0
- sol_parser/stonkfun_registry.py +139 -0
- sol_parser/transaction_route.py +510 -0
- sol_parser/wire_transaction.py +172 -0
- {sol_parser_sdk_python-0.5.8.dist-info → sol_parser_sdk_python-0.5.10.dist-info}/METADATA +27 -2
- {sol_parser_sdk_python-0.5.8.dist-info → sol_parser_sdk_python-0.5.10.dist-info}/RECORD +26 -20
- {sol_parser_sdk_python-0.5.8.dist-info → sol_parser_sdk_python-0.5.10.dist-info}/WHEEL +1 -1
- {sol_parser_sdk_python-0.5.8.dist-info → sol_parser_sdk_python-0.5.10.dist-info}/entry_points.txt +0 -0
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
|
|
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
|
|
216
|
+
from .grpc.subscribe_builder import build_subscribe_request_with_event_filter
|
|
160
217
|
|
|
161
|
-
req =
|
|
162
|
-
queue: asyncio.Queue =
|
|
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
|
-
|
|
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
|
|
259
|
+
from .grpc.subscribe_builder import build_subscribe_request_with_event_filter
|
|
199
260
|
|
|
200
|
-
req =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
sol_parser/instructions.py
CHANGED
|
@@ -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=
|
|
855
|
-
quote_vault=
|
|
856
|
-
quote_token_program=
|
|
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
|
-
|
|
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
|
|
1725
|
-
"minimum_amount_out": second if instr_type
|
|
1726
|
-
"max_amount_in": first if instr_type
|
|
1727
|
-
"amount_out": second if instr_type
|
|
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)
|
|
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
|
-
|
|
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)
|