sol-parser-sdk-python 0.4.4__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.
Files changed (54) hide show
  1. sol_parser/__init__.py +400 -0
  2. sol_parser/account_dispatcher.py +209 -0
  3. sol_parser/account_fillers/__init__.py +5 -0
  4. sol_parser/account_fillers/bonk.py +30 -0
  5. sol_parser/account_fillers/meteora.py +51 -0
  6. sol_parser/account_fillers/orca.py +40 -0
  7. sol_parser/account_fillers/pumpfun.py +97 -0
  8. sol_parser/account_fillers/pumpswap.py +93 -0
  9. sol_parser/account_fillers/raydium.py +119 -0
  10. sol_parser/accounts/__init__.py +461 -0
  11. sol_parser/accounts/rpc_wallet.py +64 -0
  12. sol_parser/accounts/utils.py +71 -0
  13. sol_parser/check_migration.py +18 -0
  14. sol_parser/clock.py +10 -0
  15. sol_parser/common/__init__.py +27 -0
  16. sol_parser/dex_parsers.py +2576 -0
  17. sol_parser/entries_decode.py +186 -0
  18. sol_parser/env_config.py +215 -0
  19. sol_parser/event_types.py +1750 -0
  20. sol_parser/geyser_pb2.py +148 -0
  21. sol_parser/geyser_pb2_grpc.py +398 -0
  22. sol_parser/grpc/__init__.py +61 -0
  23. sol_parser/grpc/geyser_connect.py +42 -0
  24. sol_parser/grpc/subscribe_builder.py +133 -0
  25. sol_parser/grpc/transaction_meta.py +183 -0
  26. sol_parser/grpc_client.py +870 -0
  27. sol_parser/grpc_instruction_parser.py +318 -0
  28. sol_parser/grpc_types.py +919 -0
  29. sol_parser/inner_instruction_parser.py +281 -0
  30. sol_parser/instr/__init__.py +15 -0
  31. sol_parser/instr_account_utils.py +58 -0
  32. sol_parser/instructions.py +1026 -0
  33. sol_parser/json_util.py +41 -0
  34. sol_parser/log_instr_dedup.py +284 -0
  35. sol_parser/logs/__init__.py +15 -0
  36. sol_parser/merger.py +233 -0
  37. sol_parser/order_buffer.py +171 -0
  38. sol_parser/parser.py +300 -0
  39. sol_parser/pumpfun_fee_enrich.py +75 -0
  40. sol_parser/rpc_parser.py +655 -0
  41. sol_parser/rust_api_inventory.py +42 -0
  42. sol_parser/rust_event_json.py +50 -0
  43. sol_parser/shredstream_client.py +191 -0
  44. sol_parser/shredstream_pb2.py +40 -0
  45. sol_parser/shredstream_pb2_grpc.py +81 -0
  46. sol_parser/shredstream_pumpfun.py +296 -0
  47. sol_parser/solana_storage_pb2.py +75 -0
  48. sol_parser/solana_storage_pb2_grpc.py +24 -0
  49. sol_parser/u128_parity.py +115 -0
  50. sol_parser/verify_discriminators.py +85 -0
  51. sol_parser_sdk_python-0.4.4.dist-info/METADATA +14 -0
  52. sol_parser_sdk_python-0.4.4.dist-info/RECORD +54 -0
  53. sol_parser_sdk_python-0.4.4.dist-info/WHEEL +4 -0
  54. sol_parser_sdk_python-0.4.4.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,318 @@
1
+ """Yellowstone 交易:outer + inner 指令解析、合并、CreateV2 fee 回填(对齐 Rust ``grpc/instruction_parser`` 主流程)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any, Dict, List, Optional, Tuple
7
+
8
+ import base58
9
+
10
+ from .account_dispatcher import fill_accounts_with_owned_keys, fill_data
11
+ from .event_types import DexEvent
12
+ from .grpc_types import (
13
+ EventMetadata,
14
+ EventType,
15
+ EventTypeFilter,
16
+ IncludeOnlyFilter,
17
+ SubscribeUpdateTransactionInfo,
18
+ )
19
+ from .inner_instruction_parser import parse_inner_instruction
20
+ from .instructions import parse_instruction_unified
21
+ from .merger import merge_dex_events
22
+ from .pumpfun_fee_enrich import enrich_pumpfun_same_tx_post_merge
23
+
24
+
25
+ def collect_program_invokes(msg: Any, meta: Any) -> Dict[bytes, List[Tuple[int, int]]]:
26
+ """从已解析的 ``Message`` + ``TransactionStatusMeta`` 收集各 program_id 的 (outer, inner) 指令索引。"""
27
+ invokes_raw: Dict[bytes, List[Tuple[int, int]]] = {}
28
+ static_keys: List[bytes] = [bytes(x) for x in msg.account_keys]
29
+ w_keys: List[bytes] = [bytes(x) for x in meta.loaded_writable_addresses]
30
+ r_keys: List[bytes] = [bytes(x) for x in meta.loaded_readonly_addresses]
31
+ keys_len = len(static_keys)
32
+ wlen = len(w_keys)
33
+
34
+ def get_key_raw(i: int) -> Optional[bytes]:
35
+ if i < keys_len:
36
+ return static_keys[i]
37
+ if i < keys_len + wlen:
38
+ return w_keys[i - keys_len]
39
+ j = i - keys_len - wlen
40
+ if j < len(r_keys):
41
+ return r_keys[j]
42
+ return None
43
+
44
+ for i, ix in enumerate(msg.instructions):
45
+ raw_pid = get_key_raw(ix.program_id_index)
46
+ if raw_pid:
47
+ invokes_raw.setdefault(raw_pid, []).append((i, -1))
48
+
49
+ for inner in meta.inner_instructions:
50
+ outer_idx = inner.index
51
+ for j, inner_ix in enumerate(inner.instructions):
52
+ raw_pid = get_key_raw(inner_ix.program_id_index)
53
+ if raw_pid:
54
+ invokes_raw.setdefault(raw_pid, []).append((int(outer_idx), j))
55
+
56
+ return invokes_raw
57
+
58
+
59
+ def apply_account_fill_to_events(
60
+ events: List[DexEvent],
61
+ tx_pb: Any,
62
+ meta_pb: Any,
63
+ ) -> None:
64
+ """对已解析的 ``solana_storage_pb2.Transaction`` + ``TransactionStatusMeta`` 应用账户填充 + ``fill_data``。"""
65
+ if not events or tx_pb is None or meta_pb is None:
66
+ return
67
+ msg = tx_pb.message
68
+ if not msg.account_keys and not msg.instructions:
69
+ return
70
+ invokes_raw = collect_program_invokes(msg, meta_pb)
71
+ invokes_str: Dict[str, List[Tuple[int, int]]] = {
72
+ base58.b58encode(k).decode("ascii"): v for k, v in invokes_raw.items()
73
+ }
74
+ for ev in events:
75
+ fill_accounts_with_owned_keys(ev, meta_pb, tx_pb, invokes_raw)
76
+ fill_data(ev, meta_pb, tx_pb, invokes_str)
77
+ recent_bh = ""
78
+ if msg.recent_blockhash:
79
+ recent_bh = base58.b58encode(bytes(msg.recent_blockhash)).decode("ascii")
80
+ for ev in events:
81
+ if isinstance(ev.data, object) and hasattr(ev.data, "metadata"):
82
+ m = ev.data.metadata
83
+ if isinstance(m, EventMetadata) and recent_bh:
84
+ m.recent_blockhash = recent_bh
85
+
86
+
87
+ def enrich_dex_events_with_subscribe_tx_info(
88
+ events: List[DexEvent],
89
+ info: SubscribeUpdateTransactionInfo,
90
+ ) -> None:
91
+ """对仅由日志解析得到的事件补全账户字段(对齐 Rust ``fill_accounts_with_owned_keys`` + ``fill_data``)。
92
+
93
+ 需要 ``info.transaction_raw`` / ``info.meta_raw``(Yellowstone 订阅里通常有)。
94
+ 无 raw 时静默跳过。
95
+ """
96
+ if not events:
97
+ return
98
+ try:
99
+ from . import solana_storage_pb2 as sol_pb
100
+ except ImportError:
101
+ return
102
+ if not info.transaction_raw or not info.meta_raw:
103
+ return
104
+ tx = sol_pb.Transaction()
105
+ tx.ParseFromString(info.transaction_raw)
106
+ meta = sol_pb.TransactionStatusMeta()
107
+ meta.ParseFromString(info.meta_raw)
108
+ apply_account_fill_to_events(events, tx, meta)
109
+
110
+
111
+ def detect_pumpfun_create_from_logs(log_messages: List[str]) -> bool:
112
+ """对齐 Rust ``detect_pumpfun_create``:Program data 前缀匹配 create 日志。"""
113
+ needle = "Program data: G3KpTd7rY3Y"
114
+ return any(needle in log for log in log_messages)
115
+
116
+
117
+ def should_parse_instructions(filter: Optional[EventTypeFilter]) -> bool:
118
+ if filter is None:
119
+ return True
120
+ inc = getattr(filter, "include_only", None)
121
+ if inc is None or not inc:
122
+ return True
123
+ need = {
124
+ EventType.PUMP_FUN_MIGRATE,
125
+ EventType.METEORA_DAMM_V2_SWAP,
126
+ EventType.METEORA_DAMM_V2_ADD_LIQUIDITY,
127
+ EventType.METEORA_DAMM_V2_CREATE_POSITION,
128
+ EventType.METEORA_DAMM_V2_CLOSE_POSITION,
129
+ EventType.METEORA_DAMM_V2_REMOVE_LIQUIDITY,
130
+ EventType.METEORA_DAMM_V2_INITIALIZE_POOL,
131
+ }
132
+ return any(t in need for t in inc)
133
+
134
+
135
+ def merge_instruction_events(
136
+ events: List[Tuple[int, Optional[int], DexEvent]],
137
+ ) -> List[DexEvent]:
138
+ """对齐 Rust ``merge_instruction_events``。"""
139
+ if not events:
140
+ return []
141
+ events = sorted(events, key=lambda x: (x[0], 0 if x[1] is None else 1 + x[1]))
142
+ result: List[DexEvent] = []
143
+ pending_outer: Optional[Tuple[int, DexEvent]] = None
144
+
145
+ for outer_idx, inner_idx, event in events:
146
+ if inner_idx is None:
147
+ if pending_outer is not None:
148
+ result.append(pending_outer[1])
149
+ pending_outer = (outer_idx, event)
150
+ else:
151
+ if pending_outer is not None:
152
+ po_idx, mut_outer = pending_outer
153
+ pending_outer = None
154
+ if po_idx == outer_idx:
155
+ merge_dex_events(mut_outer, event)
156
+ result.append(mut_outer)
157
+ else:
158
+ result.append(mut_outer)
159
+ result.append(event)
160
+ else:
161
+ result.append(event)
162
+
163
+ if pending_outer is not None:
164
+ result.append(pending_outer[1])
165
+ return result
166
+
167
+
168
+ def _meta_dict(
169
+ signature: str,
170
+ slot: int,
171
+ tx_index: int,
172
+ block_time_us: Optional[int],
173
+ grpc_recv_us: int,
174
+ recent_blockhash: str = "",
175
+ ) -> dict:
176
+ m: dict = {
177
+ "signature": signature,
178
+ "slot": slot,
179
+ "tx_index": tx_index,
180
+ "block_time_us": 0 if block_time_us is None else block_time_us,
181
+ "grpc_recv_us": grpc_recv_us,
182
+ }
183
+ if recent_blockhash:
184
+ m["recent_blockhash"] = recent_blockhash
185
+ return m
186
+
187
+
188
+ def parse_instructions_enhanced_from_subscribe_tx_info(
189
+ info: SubscribeUpdateTransactionInfo,
190
+ slot: int,
191
+ filter: Optional[EventTypeFilter] = None,
192
+ block_time_us: Optional[int] = None,
193
+ grpc_recv_us: Optional[int] = None,
194
+ ) -> List[DexEvent]:
195
+ """从 ``grpc_client`` 转换后的 ``SubscribeUpdateTransactionInfo``(raw 字节)解析指令事件。"""
196
+ try:
197
+ from . import solana_storage_pb2 as sol_pb
198
+ except ImportError:
199
+ return []
200
+ if not info.transaction_raw or not info.meta_raw:
201
+ return []
202
+ tx = sol_pb.Transaction()
203
+ tx.ParseFromString(info.transaction_raw)
204
+ meta = sol_pb.TransactionStatusMeta()
205
+ meta.ParseFromString(info.meta_raw)
206
+ sig = base58.b58encode(bytes(info.signature)).decode("ascii") if info.signature else ""
207
+ msg = tx.message
208
+ if not msg.account_keys and not msg.instructions:
209
+ return []
210
+ return parse_instructions_enhanced_from_parsed(
211
+ msg, meta, sig, slot, int(info.index), block_time_us, grpc_recv_us, filter, tx
212
+ )
213
+
214
+
215
+ def parse_instructions_enhanced_from_parsed(
216
+ msg: Any,
217
+ meta: Any,
218
+ signature: str,
219
+ slot: int,
220
+ tx_index: int,
221
+ block_time_us: Optional[int],
222
+ grpc_recv_us: Optional[int],
223
+ filter: Optional[EventTypeFilter],
224
+ transaction_pb: Any = None,
225
+ ) -> List[DexEvent]:
226
+ """接受已 Parse 的 ``Message`` 与 ``TransactionStatusMeta``(来自 ``solana_storage_pb2``)。"""
227
+ if not should_parse_instructions(filter):
228
+ return []
229
+
230
+ grpc_us = int(time.time() * 1_000_000) if grpc_recv_us is None else grpc_recv_us
231
+ f: EventTypeFilter = filter if filter is not None else IncludeOnlyFilter([])
232
+
233
+ try:
234
+ from . import solana_storage_pb2 as sol_pb
235
+ except ImportError:
236
+ return []
237
+
238
+ if transaction_pb is None:
239
+ tx_try = sol_pb.Transaction()
240
+ tx_try.message.CopyFrom(msg)
241
+ transaction_pb = tx_try
242
+
243
+ recent_bh = ""
244
+ if msg.recent_blockhash:
245
+ recent_bh = base58.b58encode(bytes(msg.recent_blockhash)).decode("ascii")
246
+
247
+ static_keys: List[bytes] = [bytes(x) for x in msg.account_keys]
248
+ w_keys: List[bytes] = [bytes(x) for x in meta.loaded_writable_addresses]
249
+ r_keys: List[bytes] = [bytes(x) for x in meta.loaded_readonly_addresses]
250
+ keys_len = len(static_keys)
251
+ wlen = len(w_keys)
252
+
253
+ def get_key_raw(i: int) -> Optional[bytes]:
254
+ if i < keys_len:
255
+ return static_keys[i]
256
+ if i < keys_len + wlen:
257
+ return w_keys[i - keys_len]
258
+ j = i - keys_len - wlen
259
+ if j < len(r_keys):
260
+ return r_keys[j]
261
+ return None
262
+
263
+ def get_key_b58(i: int) -> str:
264
+ raw = get_key_raw(i)
265
+ if raw is None:
266
+ return ""
267
+ return base58.b58encode(raw).decode("ascii")
268
+
269
+ invokes_raw = collect_program_invokes(msg, meta)
270
+
271
+ is_created_buy = detect_pumpfun_create_from_logs(list(meta.log_messages))
272
+
273
+ result: List[Tuple[int, Optional[int], DexEvent]] = []
274
+
275
+ for i, ix in enumerate(msg.instructions):
276
+ pid_idx = ix.program_id_index
277
+ pid = get_key_b58(pid_idx)
278
+ data = bytes(ix.data)
279
+ acct_bytes = bytes(ix.accounts)
280
+ accounts = [get_key_b58(b) for b in acct_bytes]
281
+ ev = parse_instruction_unified(
282
+ data, accounts, signature, slot, tx_index, block_time_us, grpc_us, f, pid
283
+ )
284
+ if ev:
285
+ result.append((i, None, ev))
286
+
287
+ for inner in meta.inner_instructions:
288
+ outer_idx = inner.index
289
+ for j, inner_ix in enumerate(inner.instructions):
290
+ pid = get_key_b58(inner_ix.program_id_index)
291
+ data = bytes(inner_ix.data)
292
+ ev = parse_inner_instruction(
293
+ data,
294
+ pid,
295
+ _meta_dict(signature, slot, tx_index, block_time_us, grpc_us, recent_bh),
296
+ f,
297
+ is_created_buy,
298
+ )
299
+ if ev:
300
+ result.append((int(outer_idx), j, ev))
301
+
302
+ merged = merge_instruction_events(result)
303
+ enrich_pumpfun_same_tx_post_merge(merged)
304
+
305
+ invokes_str: Dict[str, List[Tuple[int, int]]] = {
306
+ base58.b58encode(k).decode("ascii"): v for k, v in invokes_raw.items()
307
+ }
308
+
309
+ for ev in merged:
310
+ fill_accounts_with_owned_keys(ev, meta, transaction_pb, invokes_raw)
311
+ fill_data(ev, meta, transaction_pb, invokes_str)
312
+
313
+ for ev in merged:
314
+ if isinstance(ev.data, object) and hasattr(ev.data, "metadata"):
315
+ m = ev.data.metadata
316
+ if isinstance(m, EventMetadata):
317
+ m.recent_blockhash = recent_bh
318
+ return merged