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.
- sol_parser/__init__.py +400 -0
- sol_parser/account_dispatcher.py +209 -0
- sol_parser/account_fillers/__init__.py +5 -0
- sol_parser/account_fillers/bonk.py +30 -0
- sol_parser/account_fillers/meteora.py +51 -0
- sol_parser/account_fillers/orca.py +40 -0
- sol_parser/account_fillers/pumpfun.py +97 -0
- sol_parser/account_fillers/pumpswap.py +93 -0
- sol_parser/account_fillers/raydium.py +119 -0
- sol_parser/accounts/__init__.py +461 -0
- sol_parser/accounts/rpc_wallet.py +64 -0
- sol_parser/accounts/utils.py +71 -0
- sol_parser/check_migration.py +18 -0
- sol_parser/clock.py +10 -0
- sol_parser/common/__init__.py +27 -0
- sol_parser/dex_parsers.py +2576 -0
- sol_parser/entries_decode.py +186 -0
- sol_parser/env_config.py +215 -0
- sol_parser/event_types.py +1750 -0
- sol_parser/geyser_pb2.py +148 -0
- sol_parser/geyser_pb2_grpc.py +398 -0
- sol_parser/grpc/__init__.py +61 -0
- sol_parser/grpc/geyser_connect.py +42 -0
- sol_parser/grpc/subscribe_builder.py +133 -0
- sol_parser/grpc/transaction_meta.py +183 -0
- sol_parser/grpc_client.py +870 -0
- sol_parser/grpc_instruction_parser.py +318 -0
- sol_parser/grpc_types.py +919 -0
- sol_parser/inner_instruction_parser.py +281 -0
- sol_parser/instr/__init__.py +15 -0
- sol_parser/instr_account_utils.py +58 -0
- sol_parser/instructions.py +1026 -0
- sol_parser/json_util.py +41 -0
- sol_parser/log_instr_dedup.py +284 -0
- sol_parser/logs/__init__.py +15 -0
- sol_parser/merger.py +233 -0
- sol_parser/order_buffer.py +171 -0
- sol_parser/parser.py +300 -0
- sol_parser/pumpfun_fee_enrich.py +75 -0
- sol_parser/rpc_parser.py +655 -0
- sol_parser/rust_api_inventory.py +42 -0
- sol_parser/rust_event_json.py +50 -0
- sol_parser/shredstream_client.py +191 -0
- sol_parser/shredstream_pb2.py +40 -0
- sol_parser/shredstream_pb2_grpc.py +81 -0
- sol_parser/shredstream_pumpfun.py +296 -0
- sol_parser/solana_storage_pb2.py +75 -0
- sol_parser/solana_storage_pb2_grpc.py +24 -0
- sol_parser/u128_parity.py +115 -0
- sol_parser/verify_discriminators.py +85 -0
- sol_parser_sdk_python-0.4.4.dist-info/METADATA +14 -0
- sol_parser_sdk_python-0.4.4.dist-info/RECORD +54 -0
- sol_parser_sdk_python-0.4.4.dist-info/WHEEL +4 -0
- 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
|