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
sol_parser/rpc_parser.py
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
"""RPC Transaction Parser - 支持直接从 RPC 解析交易"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple, TypedDict, Union
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
import base58
|
|
10
|
+
|
|
11
|
+
from .dex_parsers import DexEvent, dispatch_program_data, parse_trade_from_data
|
|
12
|
+
from .grpc_types import EventTypeFilter, EventType, IncludeOnlyFilter
|
|
13
|
+
from .instructions import parse_instruction_unified
|
|
14
|
+
from .log_instr_dedup import dedupe_log_instruction_events
|
|
15
|
+
from .pumpfun_fee_enrich import enrich_pumpfun_same_tx_post_merge
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ParseError(Exception):
|
|
19
|
+
"""RPC 解析错误"""
|
|
20
|
+
def __init__(self, kind: str, message: str):
|
|
21
|
+
self.kind = kind
|
|
22
|
+
self.message = message
|
|
23
|
+
super().__init__(f"{kind}: {message}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class RpcCompiledInstruction:
|
|
28
|
+
"""编译指令"""
|
|
29
|
+
program_id_index: int
|
|
30
|
+
accounts: Union[List[int], bytes]
|
|
31
|
+
data: bytes
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class RpcInnerInstructionGroup:
|
|
36
|
+
"""内部指令组"""
|
|
37
|
+
index: int
|
|
38
|
+
instructions: List[RpcCompiledInstruction]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class RpcTokenBalance:
|
|
43
|
+
"""Token 余额"""
|
|
44
|
+
account_index: int
|
|
45
|
+
mint: str
|
|
46
|
+
ui_token_amount: Dict[str, Any]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class RpcLoadedAddresses:
|
|
51
|
+
"""加载地址"""
|
|
52
|
+
writable: List[str]
|
|
53
|
+
readonly: List[str]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class RpcTransactionMeta:
|
|
58
|
+
"""交易元数据"""
|
|
59
|
+
fee: int
|
|
60
|
+
pre_balances: List[int]
|
|
61
|
+
post_balances: List[int]
|
|
62
|
+
log_messages: List[str]
|
|
63
|
+
inner_instructions: List[RpcInnerInstructionGroup]
|
|
64
|
+
pre_token_balances: List[RpcTokenBalance]
|
|
65
|
+
post_token_balances: List[RpcTokenBalance]
|
|
66
|
+
loaded_addresses: Optional[RpcLoadedAddresses]
|
|
67
|
+
compute_units_consumed: Optional[int]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class RpcMessageHeader:
|
|
72
|
+
"""消息头"""
|
|
73
|
+
num_required_signatures: int
|
|
74
|
+
num_readonly_signed_accounts: int
|
|
75
|
+
num_readonly_unsigned_accounts: int
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class RpcMessageAddressTableLookup:
|
|
80
|
+
"""地址表查找"""
|
|
81
|
+
account_key: str
|
|
82
|
+
writable_indexes: List[int]
|
|
83
|
+
readonly_indexes: List[int]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class RpcMessage:
|
|
88
|
+
"""消息"""
|
|
89
|
+
account_keys: List[str]
|
|
90
|
+
header: Optional[RpcMessageHeader]
|
|
91
|
+
recent_blockhash: str
|
|
92
|
+
instructions: List[RpcCompiledInstruction]
|
|
93
|
+
address_table_lookups: List[RpcMessageAddressTableLookup]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class RpcTransaction:
|
|
98
|
+
"""交易"""
|
|
99
|
+
signatures: List[str]
|
|
100
|
+
message: Optional[RpcMessage]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class RpcTransactionResponse:
|
|
105
|
+
"""RPC 交易响应"""
|
|
106
|
+
|
|
107
|
+
slot: int
|
|
108
|
+
block_time: Optional[int]
|
|
109
|
+
meta: Optional[RpcTransactionMeta]
|
|
110
|
+
transaction: Optional[RpcTransaction]
|
|
111
|
+
#: 该笔交易在区块中的序号(``getTransaction`` 的 ``transactionIndex``;单交易拉取时可能为 0)
|
|
112
|
+
transaction_index: int = 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class RpcClient:
|
|
116
|
+
"""RPC 客户端接口"""
|
|
117
|
+
def get_transaction(
|
|
118
|
+
self,
|
|
119
|
+
signature: str,
|
|
120
|
+
max_supported_transaction_version: int = 0
|
|
121
|
+
) -> Optional[RpcTransactionResponse]:
|
|
122
|
+
raise NotImplementedError
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_transaction_from_rpc(
|
|
126
|
+
rpc_client: RpcClient,
|
|
127
|
+
signature: str,
|
|
128
|
+
filter: Optional[EventTypeFilter] = None,
|
|
129
|
+
) -> Tuple[List[DexEvent], Optional[ParseError]]:
|
|
130
|
+
"""通过 RPC 拉取交易并解析
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
rpc_client: RPC 客户端
|
|
134
|
+
signature: 交易签名
|
|
135
|
+
filter: 可选的事件类型过滤器
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
(events, error) 元组
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
tx = rpc_client.get_transaction(signature, max_supported_transaction_version=0)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return [], ParseError("RpcError", f"Failed to fetch transaction: {e}")
|
|
144
|
+
|
|
145
|
+
if tx is None:
|
|
146
|
+
return [], ParseError("RpcError", "Transaction not found or null response (try archive RPC for old txs)")
|
|
147
|
+
|
|
148
|
+
grpc_recv_us = int(time.time() * 1_000_000)
|
|
149
|
+
return parse_rpc_transaction(tx, signature, filter, grpc_recv_us)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def parse_rpc_transaction(
|
|
153
|
+
tx: RpcTransactionResponse,
|
|
154
|
+
signature: str,
|
|
155
|
+
filter: Optional[EventTypeFilter],
|
|
156
|
+
grpc_recv_us: int,
|
|
157
|
+
) -> Tuple[List[DexEvent], Optional[ParseError]]:
|
|
158
|
+
"""解析已获取的 RPC 交易
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
tx: RPC 交易响应
|
|
162
|
+
signature: 交易签名
|
|
163
|
+
filter: 可选的事件类型过滤器
|
|
164
|
+
grpc_recv_us: gRPC 接收时间戳(微秒)
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
(events, error) 元组
|
|
168
|
+
"""
|
|
169
|
+
if tx.transaction is None or tx.transaction.message is None:
|
|
170
|
+
return [], ParseError("ConversionError", "Transaction message is nil")
|
|
171
|
+
|
|
172
|
+
msg = tx.transaction.message
|
|
173
|
+
meta = tx.meta
|
|
174
|
+
if meta is None:
|
|
175
|
+
meta = RpcTransactionMeta(
|
|
176
|
+
fee=0,
|
|
177
|
+
pre_balances=[],
|
|
178
|
+
post_balances=[],
|
|
179
|
+
log_messages=[],
|
|
180
|
+
inner_instructions=[],
|
|
181
|
+
pre_token_balances=[],
|
|
182
|
+
post_token_balances=[],
|
|
183
|
+
loaded_addresses=None,
|
|
184
|
+
compute_units_consumed=None,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
slot = tx.slot
|
|
188
|
+
block_time_us = tx.block_time * 1_000_000 if tx.block_time else None
|
|
189
|
+
block_tx_index = int(getattr(tx, "transaction_index", 0) or 0)
|
|
190
|
+
|
|
191
|
+
instruction_events: List[DexEvent] = []
|
|
192
|
+
|
|
193
|
+
# 解析外层指令
|
|
194
|
+
for i, ix in enumerate(msg.instructions):
|
|
195
|
+
ev = _parse_rpc_instruction(
|
|
196
|
+
ix,
|
|
197
|
+
msg.account_keys,
|
|
198
|
+
signature,
|
|
199
|
+
slot,
|
|
200
|
+
block_tx_index,
|
|
201
|
+
block_time_us,
|
|
202
|
+
grpc_recv_us,
|
|
203
|
+
filter,
|
|
204
|
+
)
|
|
205
|
+
if ev:
|
|
206
|
+
instruction_events.append(ev)
|
|
207
|
+
|
|
208
|
+
# 解析内层指令
|
|
209
|
+
for group in meta.inner_instructions:
|
|
210
|
+
for ix in group.instructions:
|
|
211
|
+
ev = _parse_rpc_instruction(
|
|
212
|
+
ix,
|
|
213
|
+
msg.account_keys,
|
|
214
|
+
signature,
|
|
215
|
+
slot,
|
|
216
|
+
block_tx_index,
|
|
217
|
+
block_time_us,
|
|
218
|
+
grpc_recv_us,
|
|
219
|
+
filter,
|
|
220
|
+
)
|
|
221
|
+
if ev:
|
|
222
|
+
instruction_events.append(ev)
|
|
223
|
+
|
|
224
|
+
# 解析日志
|
|
225
|
+
is_created_buy = False
|
|
226
|
+
recent_blockhash = msg.recent_blockhash
|
|
227
|
+
log_events: List[DexEvent] = []
|
|
228
|
+
|
|
229
|
+
from .parser import parse_log_optimized
|
|
230
|
+
|
|
231
|
+
for log in meta.log_messages:
|
|
232
|
+
ev = parse_log_optimized(
|
|
233
|
+
log,
|
|
234
|
+
signature,
|
|
235
|
+
slot,
|
|
236
|
+
block_tx_index,
|
|
237
|
+
block_time_us,
|
|
238
|
+
grpc_recv_us,
|
|
239
|
+
filter,
|
|
240
|
+
is_created_buy,
|
|
241
|
+
recent_blockhash,
|
|
242
|
+
)
|
|
243
|
+
if ev:
|
|
244
|
+
if ev.type in (EventType.PUMP_FUN_CREATE, EventType.PUMP_FUN_CREATE_V2):
|
|
245
|
+
is_created_buy = True
|
|
246
|
+
log_events.append(ev)
|
|
247
|
+
|
|
248
|
+
tx_pb, meta_pb = rpc_response_to_solana_storage(tx)
|
|
249
|
+
if tx_pb is not None and meta_pb is not None:
|
|
250
|
+
from .grpc_instruction_parser import apply_account_fill_to_events
|
|
251
|
+
|
|
252
|
+
apply_account_fill_to_events(instruction_events, tx_pb, meta_pb)
|
|
253
|
+
apply_account_fill_to_events(log_events, tx_pb, meta_pb)
|
|
254
|
+
|
|
255
|
+
events = dedupe_log_instruction_events(log_events, instruction_events)
|
|
256
|
+
enrich_pumpfun_same_tx_post_merge(events)
|
|
257
|
+
|
|
258
|
+
if tx_pb is not None and meta_pb is not None:
|
|
259
|
+
from .grpc_instruction_parser import apply_account_fill_to_events
|
|
260
|
+
|
|
261
|
+
apply_account_fill_to_events(events, tx_pb, meta_pb)
|
|
262
|
+
enrich_pumpfun_same_tx_post_merge(events)
|
|
263
|
+
|
|
264
|
+
return events, None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _parse_rpc_instruction(
|
|
268
|
+
ix: RpcCompiledInstruction,
|
|
269
|
+
account_keys: List[str],
|
|
270
|
+
signature: str,
|
|
271
|
+
slot: int,
|
|
272
|
+
tx_index: int,
|
|
273
|
+
block_time_us: Optional[int],
|
|
274
|
+
grpc_recv_us: int,
|
|
275
|
+
filter: Optional[EventTypeFilter],
|
|
276
|
+
) -> Optional[DexEvent]:
|
|
277
|
+
"""解析 RPC 指令"""
|
|
278
|
+
# 获取程序 ID
|
|
279
|
+
if ix.program_id_index >= len(account_keys):
|
|
280
|
+
return None
|
|
281
|
+
program_id = account_keys[ix.program_id_index]
|
|
282
|
+
|
|
283
|
+
# 解析指令数据
|
|
284
|
+
data = ix.data
|
|
285
|
+
if len(data) == 0:
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
# 构建账户列表
|
|
289
|
+
accounts = []
|
|
290
|
+
acc_iter = ix.accounts if isinstance(ix.accounts, (list, tuple)) else list(ix.accounts)
|
|
291
|
+
for acc_idx in acc_iter:
|
|
292
|
+
if acc_idx < len(account_keys):
|
|
293
|
+
accounts.append(account_keys[acc_idx])
|
|
294
|
+
|
|
295
|
+
f: EventTypeFilter = filter if filter is not None else IncludeOnlyFilter([])
|
|
296
|
+
return parse_instruction_unified(
|
|
297
|
+
data, accounts, signature, slot, tx_index, block_time_us, grpc_recv_us, f, program_id
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def convert_rpc_to_grpc(
|
|
302
|
+
rpc_tx: RpcTransactionResponse,
|
|
303
|
+
) -> Tuple[Optional[Any], Optional[Any], Optional[ParseError]]:
|
|
304
|
+
"""将 RPC 格式转换为 gRPC 格式
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
(grpc_meta, grpc_tx, error) 元组
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
from . import geyser_pb2
|
|
311
|
+
except ImportError:
|
|
312
|
+
return None, None, ParseError(
|
|
313
|
+
"ImportError",
|
|
314
|
+
"需要 protobuf 生成的代码。请从 https://github.com/rpcpool/yellowstone-grpc 获取 proto 文件并生成 Python 代码。"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
meta = rpc_tx.meta
|
|
318
|
+
if meta is None:
|
|
319
|
+
return None, None, ParseError("ConversionError", "meta is nil")
|
|
320
|
+
|
|
321
|
+
# 转换 TransactionStatusMeta
|
|
322
|
+
grpc_meta = geyser_pb2.TransactionStatusMeta(
|
|
323
|
+
fee=meta.fee,
|
|
324
|
+
pre_balances=meta.pre_balances,
|
|
325
|
+
post_balances=meta.post_balances,
|
|
326
|
+
log_messages=meta.log_messages,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# 转换内部指令
|
|
330
|
+
for group in meta.inner_instructions:
|
|
331
|
+
grpc_group = grpc_meta.inner_instructions.add()
|
|
332
|
+
grpc_group.index = group.index
|
|
333
|
+
for ix in group.instructions:
|
|
334
|
+
grpc_ix = grpc_group.instructions.add()
|
|
335
|
+
grpc_ix.program_id_index = ix.program_id_index
|
|
336
|
+
grpc_ix.accounts = bytes(ix.accounts)
|
|
337
|
+
grpc_ix.data = ix.data
|
|
338
|
+
|
|
339
|
+
# 转换加载的地址
|
|
340
|
+
if meta.loaded_addresses:
|
|
341
|
+
for addr in meta.loaded_addresses.writable:
|
|
342
|
+
grpc_meta.loaded_writable_addresses.append(base58.b58decode(addr))
|
|
343
|
+
for addr in meta.loaded_addresses.readonly:
|
|
344
|
+
grpc_meta.loaded_readonly_addresses.append(base58.b58decode(addr))
|
|
345
|
+
|
|
346
|
+
# 转换交易
|
|
347
|
+
if rpc_tx.transaction is None:
|
|
348
|
+
return None, None, ParseError("ConversionError", "transaction is nil")
|
|
349
|
+
|
|
350
|
+
tx = rpc_tx.transaction
|
|
351
|
+
grpc_tx = geyser_pb2.Transaction()
|
|
352
|
+
|
|
353
|
+
# 转换签名
|
|
354
|
+
for sig in tx.signatures:
|
|
355
|
+
grpc_tx.signatures.append(base58.b58decode(sig))
|
|
356
|
+
|
|
357
|
+
# 转换消息
|
|
358
|
+
if tx.message:
|
|
359
|
+
msg = tx.message
|
|
360
|
+
grpc_msg = grpc_tx.message
|
|
361
|
+
|
|
362
|
+
# 转换账户密钥
|
|
363
|
+
for key in msg.account_keys:
|
|
364
|
+
grpc_msg.account_keys.append(base58.b58decode(key))
|
|
365
|
+
|
|
366
|
+
# 转换最近区块哈希
|
|
367
|
+
if msg.recent_blockhash:
|
|
368
|
+
grpc_msg.recent_blockhash = base58.b58decode(msg.recent_blockhash)
|
|
369
|
+
|
|
370
|
+
# 转换指令
|
|
371
|
+
for ix in msg.instructions:
|
|
372
|
+
grpc_ix = grpc_msg.instructions.add()
|
|
373
|
+
grpc_ix.program_id_index = ix.program_id_index
|
|
374
|
+
grpc_ix.accounts = bytes(ix.accounts)
|
|
375
|
+
grpc_ix.data = ix.data
|
|
376
|
+
|
|
377
|
+
# 转换地址表查找
|
|
378
|
+
for lookup in msg.address_table_lookups:
|
|
379
|
+
grpc_lookup = grpc_msg.address_table_lookups.add()
|
|
380
|
+
grpc_lookup.account_key = base58.b58decode(lookup.account_key)
|
|
381
|
+
grpc_lookup.writable_indexes = bytes(lookup.writable_indexes)
|
|
382
|
+
grpc_lookup.readonly_indexes = bytes(lookup.readonly_indexes)
|
|
383
|
+
|
|
384
|
+
# 转换消息头
|
|
385
|
+
if msg.header:
|
|
386
|
+
grpc_msg.header.num_required_signatures = msg.header.num_required_signatures
|
|
387
|
+
grpc_msg.header.num_readonly_signed_accounts = msg.header.num_readonly_signed_accounts
|
|
388
|
+
grpc_msg.header.num_readonly_unsigned_accounts = msg.header.num_readonly_unsigned_accounts
|
|
389
|
+
|
|
390
|
+
return grpc_meta, grpc_tx, None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _ix_data_from_rpc(ix: dict) -> bytes:
|
|
394
|
+
raw = ix.get("data")
|
|
395
|
+
if raw is None or raw == "":
|
|
396
|
+
return b""
|
|
397
|
+
if isinstance(raw, str):
|
|
398
|
+
try:
|
|
399
|
+
return base58.b58decode(raw)
|
|
400
|
+
except Exception:
|
|
401
|
+
return b""
|
|
402
|
+
return b""
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _account_keys_from_message(msg: dict) -> List[str]:
|
|
406
|
+
keys = msg.get("accountKeys") or msg.get("account_keys") or []
|
|
407
|
+
out: List[str] = []
|
|
408
|
+
for k in keys:
|
|
409
|
+
if isinstance(k, str):
|
|
410
|
+
out.append(k)
|
|
411
|
+
elif isinstance(k, dict):
|
|
412
|
+
pk = k.get("pubkey") or k.get("pubKey")
|
|
413
|
+
if pk:
|
|
414
|
+
out.append(str(pk))
|
|
415
|
+
return out
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _parse_rpc_compiled_ix(ix: dict, account_keys: List[str]) -> RpcCompiledInstruction:
|
|
419
|
+
if "programIdIndex" in ix:
|
|
420
|
+
pidx = int(ix["programIdIndex"])
|
|
421
|
+
elif "programId" in ix:
|
|
422
|
+
pid = ix["programId"]
|
|
423
|
+
try:
|
|
424
|
+
pidx = account_keys.index(pid)
|
|
425
|
+
except ValueError:
|
|
426
|
+
pidx = 0
|
|
427
|
+
else:
|
|
428
|
+
pidx = 0
|
|
429
|
+
accounts = ix.get("accounts") or []
|
|
430
|
+
if isinstance(accounts, str):
|
|
431
|
+
try:
|
|
432
|
+
accounts = list(base58.b58decode(accounts))
|
|
433
|
+
except Exception:
|
|
434
|
+
accounts = []
|
|
435
|
+
elif not isinstance(accounts, list):
|
|
436
|
+
accounts = []
|
|
437
|
+
acc_list = [int(x) for x in accounts]
|
|
438
|
+
return RpcCompiledInstruction(
|
|
439
|
+
program_id_index=pidx,
|
|
440
|
+
accounts=acc_list,
|
|
441
|
+
data=_ix_data_from_rpc(ix),
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def rpc_get_transaction_result_dict_to_response(
|
|
446
|
+
result: Optional[dict],
|
|
447
|
+
) -> Optional[RpcTransactionResponse]:
|
|
448
|
+
"""将 ``getTransaction`` 的 JSON ``result`` 转为 :class:`RpcTransactionResponse`。
|
|
449
|
+
|
|
450
|
+
支持 ``encoding: json`` / ``jsonParsed`` 下的 ``transaction`` 对象;若为仅 ``base64`` 数组则返回 ``None``(需另行解码)。
|
|
451
|
+
"""
|
|
452
|
+
if not result or not isinstance(result, dict):
|
|
453
|
+
return None
|
|
454
|
+
tfield = result.get("transaction")
|
|
455
|
+
if tfield is None:
|
|
456
|
+
return None
|
|
457
|
+
if isinstance(tfield, list):
|
|
458
|
+
return None
|
|
459
|
+
if not isinstance(tfield, dict):
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
slot = int(result.get("slot", 0))
|
|
463
|
+
block_time = result.get("blockTime")
|
|
464
|
+
if block_time is not None:
|
|
465
|
+
block_time = int(block_time)
|
|
466
|
+
tx_idx_raw = result.get("transactionIndex")
|
|
467
|
+
if tx_idx_raw is None:
|
|
468
|
+
tx_idx_raw = result.get("transaction_index")
|
|
469
|
+
transaction_index = int(tx_idx_raw) if tx_idx_raw is not None else 0
|
|
470
|
+
|
|
471
|
+
tx_body = tfield
|
|
472
|
+
sigs = tx_body.get("signatures") or []
|
|
473
|
+
if not isinstance(sigs, list):
|
|
474
|
+
sigs = []
|
|
475
|
+
msg_dict = tx_body.get("message")
|
|
476
|
+
if not isinstance(msg_dict, dict):
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
account_keys = _account_keys_from_message(msg_dict)
|
|
480
|
+
instructions: List[RpcCompiledInstruction] = []
|
|
481
|
+
for ix in msg_dict.get("instructions") or []:
|
|
482
|
+
if not isinstance(ix, dict):
|
|
483
|
+
continue
|
|
484
|
+
if "programIdIndex" not in ix and "programId" not in ix:
|
|
485
|
+
continue
|
|
486
|
+
instructions.append(_parse_rpc_compiled_ix(ix, account_keys))
|
|
487
|
+
|
|
488
|
+
header = None
|
|
489
|
+
h = msg_dict.get("header")
|
|
490
|
+
if isinstance(h, dict):
|
|
491
|
+
header = RpcMessageHeader(
|
|
492
|
+
num_required_signatures=int(h.get("numRequiredSignatures", 0)),
|
|
493
|
+
num_readonly_signed_accounts=int(h.get("numReadonlySignedAccounts", 0)),
|
|
494
|
+
num_readonly_unsigned_accounts=int(h.get("numReadonlyUnsignedAccounts", 0)),
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
lookups: List[RpcMessageAddressTableLookup] = []
|
|
498
|
+
for lu in msg_dict.get("addressTableLookups") or []:
|
|
499
|
+
if not isinstance(lu, dict):
|
|
500
|
+
continue
|
|
501
|
+
wi = lu.get("writableIndexes") or lu.get("writable_indexes") or []
|
|
502
|
+
ri = lu.get("readonlyIndexes") or lu.get("readonly_indexes") or []
|
|
503
|
+
ak = lu.get("accountKey") or lu.get("account_key") or ""
|
|
504
|
+
lookups.append(
|
|
505
|
+
RpcMessageAddressTableLookup(
|
|
506
|
+
account_key=str(ak),
|
|
507
|
+
writable_indexes=[int(x) for x in wi],
|
|
508
|
+
readonly_indexes=[int(x) for x in ri],
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
recent = msg_dict.get("recentBlockhash") or msg_dict.get("recent_blockhash") or ""
|
|
513
|
+
message = RpcMessage(
|
|
514
|
+
account_keys=account_keys,
|
|
515
|
+
header=header,
|
|
516
|
+
recent_blockhash=str(recent),
|
|
517
|
+
instructions=instructions,
|
|
518
|
+
address_table_lookups=lookups,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
rpc_tx = RpcTransaction(signatures=[str(s) for s in sigs], message=message)
|
|
522
|
+
|
|
523
|
+
meta_dict = result.get("meta")
|
|
524
|
+
if meta_dict is None:
|
|
525
|
+
meta = RpcTransactionMeta(
|
|
526
|
+
fee=0,
|
|
527
|
+
pre_balances=[],
|
|
528
|
+
post_balances=[],
|
|
529
|
+
log_messages=[],
|
|
530
|
+
inner_instructions=[],
|
|
531
|
+
pre_token_balances=[],
|
|
532
|
+
post_token_balances=[],
|
|
533
|
+
loaded_addresses=None,
|
|
534
|
+
compute_units_consumed=None,
|
|
535
|
+
)
|
|
536
|
+
else:
|
|
537
|
+
inner_groups: List[RpcInnerInstructionGroup] = []
|
|
538
|
+
for g in meta_dict.get("innerInstructions") or meta_dict.get("inner_instructions") or []:
|
|
539
|
+
if not isinstance(g, dict):
|
|
540
|
+
continue
|
|
541
|
+
ixs: List[RpcCompiledInstruction] = []
|
|
542
|
+
for ix in g.get("instructions") or []:
|
|
543
|
+
if not isinstance(ix, dict):
|
|
544
|
+
continue
|
|
545
|
+
ixs.append(_parse_rpc_compiled_ix(ix, account_keys))
|
|
546
|
+
inner_groups.append(
|
|
547
|
+
RpcInnerInstructionGroup(index=int(g.get("index", 0)), instructions=ixs)
|
|
548
|
+
)
|
|
549
|
+
loaded = None
|
|
550
|
+
la = meta_dict.get("loadedAddresses") or meta_dict.get("loaded_addresses")
|
|
551
|
+
if isinstance(la, dict):
|
|
552
|
+
loaded = RpcLoadedAddresses(
|
|
553
|
+
writable=[str(x) for x in la.get("writable", [])],
|
|
554
|
+
readonly=[str(x) for x in la.get("readonly", [])],
|
|
555
|
+
)
|
|
556
|
+
logs = meta_dict.get("logMessages") or meta_dict.get("log_messages") or []
|
|
557
|
+
if not isinstance(logs, list):
|
|
558
|
+
logs = []
|
|
559
|
+
meta = RpcTransactionMeta(
|
|
560
|
+
fee=int(meta_dict.get("fee", 0)),
|
|
561
|
+
pre_balances=[int(x) for x in meta_dict.get("preBalances") or meta_dict.get("pre_balances") or []],
|
|
562
|
+
post_balances=[int(x) for x in meta_dict.get("postBalances") or meta_dict.get("post_balances") or []],
|
|
563
|
+
log_messages=[str(x) for x in logs],
|
|
564
|
+
inner_instructions=inner_groups,
|
|
565
|
+
pre_token_balances=[],
|
|
566
|
+
post_token_balances=[],
|
|
567
|
+
loaded_addresses=loaded,
|
|
568
|
+
compute_units_consumed=meta_dict.get("computeUnitsConsumed"),
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
return RpcTransactionResponse(
|
|
572
|
+
slot=slot,
|
|
573
|
+
block_time=block_time,
|
|
574
|
+
meta=meta,
|
|
575
|
+
transaction=rpc_tx,
|
|
576
|
+
transaction_index=transaction_index,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def rpc_response_to_solana_storage(
|
|
581
|
+
rpc_tx: RpcTransactionResponse,
|
|
582
|
+
) -> Tuple[Optional[Any], Optional[Any]]:
|
|
583
|
+
"""将 :class:`RpcTransactionResponse` 转为 ``solana_storage_pb2`` 的 Transaction + TransactionStatusMeta。"""
|
|
584
|
+
try:
|
|
585
|
+
from . import solana_storage_pb2 as sol_pb
|
|
586
|
+
except ImportError:
|
|
587
|
+
return None, None
|
|
588
|
+
if rpc_tx.transaction is None or rpc_tx.transaction.message is None:
|
|
589
|
+
return None, None
|
|
590
|
+
if rpc_tx.meta is None:
|
|
591
|
+
return None, None
|
|
592
|
+
|
|
593
|
+
tx = sol_pb.Transaction()
|
|
594
|
+
for sig in rpc_tx.transaction.signatures:
|
|
595
|
+
tx.signatures.append(base58.b58decode(sig))
|
|
596
|
+
|
|
597
|
+
msg = rpc_tx.transaction.message
|
|
598
|
+
out_msg = tx.message
|
|
599
|
+
for k in msg.account_keys:
|
|
600
|
+
out_msg.account_keys.append(base58.b58decode(k))
|
|
601
|
+
if msg.recent_blockhash:
|
|
602
|
+
out_msg.recent_blockhash = base58.b58decode(msg.recent_blockhash)
|
|
603
|
+
for ix in msg.instructions:
|
|
604
|
+
c = out_msg.instructions.add()
|
|
605
|
+
c.program_id_index = ix.program_id_index
|
|
606
|
+
acc = ix.accounts
|
|
607
|
+
c.accounts = bytes(acc) if not isinstance(acc, bytes) else acc
|
|
608
|
+
c.data = ix.data
|
|
609
|
+
if msg.header:
|
|
610
|
+
out_msg.header.num_required_signatures = msg.header.num_required_signatures
|
|
611
|
+
out_msg.header.num_readonly_signed_accounts = msg.header.num_readonly_signed_accounts
|
|
612
|
+
out_msg.header.num_readonly_unsigned_accounts = msg.header.num_readonly_unsigned_accounts
|
|
613
|
+
for lu in msg.address_table_lookups:
|
|
614
|
+
l = out_msg.address_table_lookups.add()
|
|
615
|
+
l.account_key = base58.b58decode(lu.account_key)
|
|
616
|
+
l.writable_indexes = bytes(lu.writable_indexes)
|
|
617
|
+
l.readonly_indexes = bytes(lu.readonly_indexes)
|
|
618
|
+
|
|
619
|
+
m = rpc_tx.meta
|
|
620
|
+
meta = sol_pb.TransactionStatusMeta()
|
|
621
|
+
meta.fee = m.fee
|
|
622
|
+
meta.pre_balances.extend(m.pre_balances)
|
|
623
|
+
meta.post_balances.extend(m.post_balances)
|
|
624
|
+
meta.log_messages.extend(m.log_messages)
|
|
625
|
+
for group in m.inner_instructions:
|
|
626
|
+
g = meta.inner_instructions.add()
|
|
627
|
+
g.index = group.index
|
|
628
|
+
for ix in group.instructions:
|
|
629
|
+
ii = g.instructions.add()
|
|
630
|
+
ii.program_id_index = ix.program_id_index
|
|
631
|
+
acc = ix.accounts
|
|
632
|
+
ii.accounts = bytes(acc) if not isinstance(acc, bytes) else acc
|
|
633
|
+
ii.data = ix.data
|
|
634
|
+
if m.loaded_addresses:
|
|
635
|
+
for w in m.loaded_addresses.writable:
|
|
636
|
+
meta.loaded_writable_addresses.append(base58.b58decode(w))
|
|
637
|
+
for r in m.loaded_addresses.readonly:
|
|
638
|
+
meta.loaded_readonly_addresses.append(base58.b58decode(r))
|
|
639
|
+
return tx, meta
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def enrich_dex_events_from_rpc_get_transaction_result(
|
|
643
|
+
events: List[DexEvent],
|
|
644
|
+
result: Optional[dict],
|
|
645
|
+
) -> None:
|
|
646
|
+
"""对已有事件列表用 ``getTransaction`` 的 JSON ``result`` 做与 gRPC 相同的账户补全。"""
|
|
647
|
+
resp = rpc_get_transaction_result_dict_to_response(result)
|
|
648
|
+
if resp is None:
|
|
649
|
+
return
|
|
650
|
+
tx_pb, meta_pb = rpc_response_to_solana_storage(resp)
|
|
651
|
+
if tx_pb is None or meta_pb is None:
|
|
652
|
+
return
|
|
653
|
+
from .grpc_instruction_parser import apply_account_fill_to_events
|
|
654
|
+
|
|
655
|
+
apply_account_fill_to_events(events, tx_pb, meta_pb)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Rust ``sol-parser-sdk`` 公开 API 与 Python 映射(维护对照,非运行时依赖)。
|
|
2
|
+
|
|
3
|
+
本模块仅作文档与静态清单;实际导出见 ``sol_parser.__init__`` 及各子包。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# crate 根: sol-parser-sdk/src/lib.rs
|
|
7
|
+
CRATE_ROOT_EXPORTS = {
|
|
8
|
+
"parse_logs_only": "sol_parser.parser.parse_logs_only",
|
|
9
|
+
"parse_logs_streaming": "sol_parser.parser.parse_logs_streaming",
|
|
10
|
+
"parse_transaction_events": "sol_parser.parser.parse_transaction_events",
|
|
11
|
+
"parse_transaction_events_streaming": "sol_parser.parser.parse_transaction_events_streaming",
|
|
12
|
+
"parse_transaction_with_listener": "sol_parser.parser.parse_transaction_with_listener",
|
|
13
|
+
"parse_transaction_with_streaming_listener": "sol_parser.parser.parse_transaction_with_streaming_listener",
|
|
14
|
+
"DexEvent": "sol_parser.dex_parsers.DexEvent (dict 形态) / event_types.TypedDexEvent",
|
|
15
|
+
"EventListener": "sol_parser.parser.EventListener",
|
|
16
|
+
"EventMetadata": "sol_parser.grpc_types.EventMetadata",
|
|
17
|
+
"ParsedEvent": "同 DexEvent",
|
|
18
|
+
"StreamingEventListener": "sol_parser.parser.StreamingEventListener",
|
|
19
|
+
"warmup_parser": "sol_parser.parser.warmup_parser",
|
|
20
|
+
"convert_rpc_to_grpc": "sol_parser.rpc_parser.convert_rpc_to_grpc",
|
|
21
|
+
"parse_rpc_transaction": "sol_parser.rpc_parser.parse_rpc_transaction",
|
|
22
|
+
"parse_transaction_from_rpc": "sol_parser.rpc_parser.parse_transaction_from_rpc",
|
|
23
|
+
"ParseError": "sol_parser.rpc_parser.ParseError",
|
|
24
|
+
"rpc_resolve_user_wallet_pubkey": "sol_parser.accounts.rpc_wallet.rpc_resolve_user_wallet_pubkey",
|
|
25
|
+
"user_wallet_pubkey_for_onchain_account": "sol_parser.accounts.utils.user_wallet_pubkey_for_onchain_account",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
SUBMODULES = {
|
|
29
|
+
"accounts": "sol_parser.accounts",
|
|
30
|
+
"common": "sol_parser.common (见 common 子模块或常量分散)",
|
|
31
|
+
"core": "sol_parser.parser / merger / dex_parsers / event_types",
|
|
32
|
+
"instr": "sol_parser.instructions",
|
|
33
|
+
"logs": "sol_parser.logs",
|
|
34
|
+
"utils": "sol_parser.json_util 等",
|
|
35
|
+
"warmup": "sol_parser.parser.warmup_parser",
|
|
36
|
+
"grpc": "sol_parser.grpc",
|
|
37
|
+
"shredstream": "sol_parser.shredstream_client / shredstream 配置",
|
|
38
|
+
"rpc_parser": "sol_parser.rpc_parser",
|
|
39
|
+
"parser": "sol_parser.parser (core 别名)",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
__all__ = ["CRATE_ROOT_EXPORTS", "SUBMODULES"]
|