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/__init__.py +34 -0
- sol_parser/account_dispatcher.py +98 -4
- sol_parser/account_fillers/meteora.py +10 -2
- sol_parser/account_fillers/raydium.py +9 -4
- sol_parser/account_fillers/raydium_launchlab.py +54 -0
- sol_parser/accounts/__init__.py +52 -2
- sol_parser/block_meta.py +29 -0
- sol_parser/dex_parsers.py +662 -69
- sol_parser/entries_decode.py +6 -80
- sol_parser/event_type_parity.py +4 -0
- sol_parser/event_types.py +269 -3
- sol_parser/grpc/subscribe_builder.py +8 -0
- sol_parser/grpc_client.py +114 -20
- sol_parser/grpc_instruction_parser.py +85 -26
- sol_parser/grpc_types.py +31 -0
- sol_parser/inner_instruction_parser.py +5 -7
- sol_parser/instructions.py +259 -53
- sol_parser/json_util.py +15 -1
- sol_parser/liquidity_snapshot.py +101 -0
- sol_parser/log_instr_dedup.py +56 -12
- sol_parser/merger.py +73 -85
- sol_parser/parser.py +11 -0
- sol_parser/pumpfun_fee_enrich.py +2 -0
- sol_parser/rpc_parser.py +30 -4
- 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.7.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/METADATA +44 -9
- {sol_parser_sdk_python-0.5.7.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/RECORD +32 -26
- {sol_parser_sdk_python-0.5.7.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/WHEEL +1 -1
- {sol_parser_sdk_python-0.5.7.dist-info → sol_parser_sdk_python-0.5.9.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,
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
161
|
+
for outer_idx, inner_idx, stack_height, is_dlmm_event_cpi, event in normalized:
|
|
138
162
|
if inner_idx is None:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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[
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|