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,461 @@
1
+ """账户解析器 — 对齐 Rust ``accounts`` 模块(含 ``utils`` / ``rpc_wallet`` 子模块)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+ from typing import Optional
7
+ from dataclasses import dataclass
8
+
9
+ import base58
10
+
11
+ from ..grpc_types import EventTypeFilter, EventType, EventMetadata
12
+ from ..dex_parsers import DexEvent
13
+
14
+ from . import rpc_wallet
15
+ from . import utils as acc_utils
16
+
17
+ # 程序 ID(与 Rust ``accounts/program_ids`` / ``instr/program_ids`` 一致)
18
+ PUMPFUN_PROGRAM_ID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
19
+ PUMPSWAP_PROGRAM_ID = "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"
20
+
21
+ _DISC_PUMPFUN_GLOBAL = bytes([167, 232, 232, 177, 200, 108, 114, 127])
22
+ _DISC_GLOBAL_CONFIG = bytes([149, 8, 156, 202, 160, 252, 176, 217])
23
+ _DISC_POOL = bytes([241, 154, 109, 4, 17, 177, 109, 188])
24
+ _DISC_NONCE = bytes([1, 0, 0, 0, 1, 0, 0, 0])
25
+
26
+ MINT_SIZE = 82
27
+ TOKEN_ACCOUNT_SIZE = 165
28
+ NONCE_ACCOUNT_SIZE = 80
29
+ PUMPFUN_GLOBAL_BODY = 1021
30
+ GLOBAL_CONFIG_BODY = 634
31
+ POOL_BODY = 244
32
+
33
+ SUPPLY_OFFSET = 36
34
+ DECIMALS_OFFSET = 44
35
+ AMOUNT_OFFSET = 64
36
+ NONCE_AUTHORITY_OFFSET = 8
37
+ NONCE_NONCE_OFFSET = 40
38
+
39
+ EMPTY_PUBKEY = ""
40
+
41
+
42
+ def _account_event(event_type: EventType, data: dict) -> DexEvent:
43
+ return DexEvent(type=event_type, data=data)
44
+
45
+
46
+ @dataclass
47
+ class AccountData:
48
+ pubkey: str
49
+ executable: bool
50
+ lamports: int
51
+ owner: str
52
+ rent_epoch: int
53
+ data: bytes
54
+
55
+
56
+ def has_discriminator(data: bytes, discriminator: bytes) -> bool:
57
+ return acc_utils.has_discriminator(data, discriminator)
58
+
59
+
60
+ def base58_encode_32(data: bytes) -> str:
61
+ return base58.b58encode(data).decode("ascii")
62
+
63
+
64
+ def read_pubkey_fast(data: bytes, offset: int) -> str:
65
+ if offset + 32 > len(data):
66
+ return EMPTY_PUBKEY
67
+ return base58.b58encode(data[offset : offset + 32]).decode("ascii")
68
+
69
+
70
+ def read_u64_fast(data: bytes, offset: int) -> int:
71
+ return struct.unpack_from("<Q", data, offset)[0]
72
+
73
+
74
+ def read_u16_fast(data: bytes, offset: int) -> int:
75
+ return struct.unpack_from("<H", data, offset)[0]
76
+
77
+
78
+ def parse_account_unified(
79
+ account: AccountData,
80
+ metadata: EventMetadata,
81
+ event_type_filter: Optional[EventTypeFilter] = None,
82
+ ) -> Optional[DexEvent]:
83
+ """对齐 Rust ``parse_account_unified``(含 ``Option<EventTypeFilter>`` 语义)。"""
84
+ data = account.data
85
+ if not data:
86
+ return None
87
+
88
+ if event_type_filter is not None:
89
+ inc = getattr(event_type_filter, "include_only", None)
90
+ if inc is not None and len(inc) > 0:
91
+ need = {
92
+ EventType.TOKEN_ACCOUNT,
93
+ EventType.TOKEN_INFO,
94
+ EventType.NONCE_ACCOUNT,
95
+ EventType.ACCOUNT_PUMP_FUN_GLOBAL,
96
+ EventType.ACCOUNT_PUMP_SWAP_GLOBAL_CONFIG,
97
+ EventType.ACCOUNT_PUMP_SWAP_POOL,
98
+ }
99
+ if not any(t in need for t in inc):
100
+ return None
101
+
102
+ if account.owner == PUMPSWAP_PROGRAM_ID and event_type_filter is not None:
103
+ if event_type_filter.should_include(
104
+ EventType.ACCOUNT_PUMP_SWAP_GLOBAL_CONFIG
105
+ ) or event_type_filter.should_include(EventType.ACCOUNT_PUMP_SWAP_POOL):
106
+ ev = _parse_pumpswap_account(account, metadata)
107
+ if ev is not None:
108
+ return ev
109
+
110
+ if account.owner == PUMPFUN_PROGRAM_ID and event_type_filter is not None:
111
+ if event_type_filter.should_include(EventType.ACCOUNT_PUMP_FUN_GLOBAL):
112
+ ev = _parse_pumpfun_account(account, metadata)
113
+ if ev is not None:
114
+ return ev
115
+
116
+ if acc_utils.is_nonce_account(data):
117
+ if event_type_filter is not None:
118
+ if not event_type_filter.should_include(EventType.NONCE_ACCOUNT):
119
+ return None
120
+ return _parse_nonce_fast(account, metadata)
121
+
122
+ if event_type_filter is not None:
123
+ if not event_type_filter.should_include(EventType.TOKEN_ACCOUNT):
124
+ return None
125
+ return parse_token_account(account, metadata)
126
+
127
+
128
+ def _parse_pumpswap_account(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
129
+ if acc_utils.has_discriminator(account.data, _DISC_GLOBAL_CONFIG):
130
+ return parse_pumpswap_global_config(account, metadata)
131
+ if acc_utils.has_discriminator(account.data, _DISC_POOL):
132
+ return parse_pumpswap_pool(account, metadata)
133
+ return None
134
+
135
+
136
+ def _parse_pumpfun_account(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
137
+ if acc_utils.has_discriminator(account.data, _DISC_PUMPFUN_GLOBAL):
138
+ return parse_pumpfun_global(account, metadata)
139
+ return None
140
+
141
+
142
+ def _parse_mint_fast(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
143
+ data = account.data
144
+ return _account_event(
145
+ EventType.TOKEN_INFO,
146
+ {
147
+ "metadata": metadata,
148
+ "pubkey": account.pubkey,
149
+ "executable": account.executable,
150
+ "lamports": account.lamports,
151
+ "owner": account.owner,
152
+ "rent_epoch": account.rent_epoch,
153
+ "supply": struct.unpack_from("<Q", data, SUPPLY_OFFSET)[0],
154
+ "decimals": data[DECIMALS_OFFSET],
155
+ },
156
+ )
157
+
158
+
159
+ def _parse_token_fast(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
160
+ data = account.data
161
+ return _account_event(
162
+ EventType.TOKEN_ACCOUNT,
163
+ {
164
+ "metadata": metadata,
165
+ "pubkey": account.pubkey,
166
+ "executable": account.executable,
167
+ "lamports": account.lamports,
168
+ "owner": account.owner,
169
+ "rent_epoch": account.rent_epoch,
170
+ "amount": struct.unpack_from("<Q", data, AMOUNT_OFFSET)[0],
171
+ },
172
+ )
173
+
174
+
175
+ def _parse_nonce_fast(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
176
+ data = account.data
177
+ authority = base58_encode_32(data[NONCE_AUTHORITY_OFFSET : NONCE_AUTHORITY_OFFSET + 32])
178
+ nonce = base58_encode_32(data[NONCE_NONCE_OFFSET : NONCE_NONCE_OFFSET + 32])
179
+ return _account_event(
180
+ EventType.NONCE_ACCOUNT,
181
+ {
182
+ "metadata": metadata,
183
+ "pubkey": account.pubkey,
184
+ "executable": account.executable,
185
+ "lamports": account.lamports,
186
+ "owner": account.owner,
187
+ "rent_epoch": account.rent_epoch,
188
+ "nonce": nonce,
189
+ "authority": authority,
190
+ },
191
+ )
192
+
193
+
194
+ def _parse_pumpfun_global_fast(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
195
+ data = account.data[8:]
196
+ o = 0
197
+ initialized = data[o] != 0
198
+ o += 1
199
+ authority = read_pubkey_fast(data, o)
200
+ o += 32
201
+ fee_recipient = read_pubkey_fast(data, o)
202
+ o += 32
203
+ initial_virtual_token_reserves = read_u64_fast(data, o)
204
+ o += 8
205
+ initial_virtual_sol_reserves = read_u64_fast(data, o)
206
+ o += 8
207
+ initial_real_token_reserves = read_u64_fast(data, o)
208
+ o += 8
209
+ token_total_supply = read_u64_fast(data, o)
210
+ o += 8
211
+ fee_basis_points = read_u64_fast(data, o)
212
+ o += 8
213
+ withdraw_authority = read_pubkey_fast(data, o)
214
+ o += 32
215
+ enable_migrate = data[o] != 0
216
+ o += 1
217
+ pool_migration_fee = read_u64_fast(data, o)
218
+ o += 8
219
+ creator_fee_basis_points = read_u64_fast(data, o)
220
+ o += 8
221
+ fee_recipients = []
222
+ for _ in range(8):
223
+ fee_recipients.append(read_pubkey_fast(data, o))
224
+ o += 32
225
+ set_creator_authority = read_pubkey_fast(data, o)
226
+ o += 32
227
+ admin_set_creator_authority = read_pubkey_fast(data, o)
228
+ o += 32
229
+ create_v2_enabled = data[o] != 0
230
+ o += 1
231
+ whitelist_pda = read_pubkey_fast(data, o)
232
+ o += 32
233
+ reserved_fee_recipient = read_pubkey_fast(data, o)
234
+ o += 32
235
+ mayhem_mode_enabled = data[o] != 0
236
+ o += 1
237
+ reserved_fee_recipients = []
238
+ for _ in range(7):
239
+ reserved_fee_recipients.append(read_pubkey_fast(data, o))
240
+ o += 32
241
+ return _account_event(
242
+ EventType.ACCOUNT_PUMP_FUN_GLOBAL,
243
+ {
244
+ "metadata": metadata,
245
+ "pubkey": account.pubkey,
246
+ "global": {
247
+ "initialized": initialized,
248
+ "authority": authority,
249
+ "fee_recipient": fee_recipient,
250
+ "initial_virtual_token_reserves": initial_virtual_token_reserves,
251
+ "initial_virtual_sol_reserves": initial_virtual_sol_reserves,
252
+ "initial_real_token_reserves": initial_real_token_reserves,
253
+ "token_total_supply": token_total_supply,
254
+ "fee_basis_points": fee_basis_points,
255
+ "withdraw_authority": withdraw_authority,
256
+ "enable_migrate": enable_migrate,
257
+ "pool_migration_fee": pool_migration_fee,
258
+ "creator_fee_basis_points": creator_fee_basis_points,
259
+ "fee_recipients": fee_recipients,
260
+ "set_creator_authority": set_creator_authority,
261
+ "admin_set_creator_authority": admin_set_creator_authority,
262
+ "create_v2_enabled": create_v2_enabled,
263
+ "whitelist_pda": whitelist_pda,
264
+ "reserved_fee_recipient": reserved_fee_recipient,
265
+ "mayhem_mode_enabled": mayhem_mode_enabled,
266
+ "reserved_fee_recipients": reserved_fee_recipients,
267
+ },
268
+ },
269
+ )
270
+
271
+
272
+ def _parse_pumpswap_global_config_fast(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
273
+ data = account.data[8:]
274
+ o = 0
275
+ admin = read_pubkey_fast(data, o)
276
+ o += 32
277
+ lp_fee = read_u64_fast(data, o)
278
+ o += 8
279
+ protocol_fee = read_u64_fast(data, o)
280
+ o += 8
281
+ disable_flags = data[o]
282
+ o += 1
283
+ recipients = [
284
+ read_pubkey_fast(data, o),
285
+ read_pubkey_fast(data, o + 32),
286
+ read_pubkey_fast(data, o + 64),
287
+ read_pubkey_fast(data, o + 96),
288
+ read_pubkey_fast(data, o + 128),
289
+ read_pubkey_fast(data, o + 160),
290
+ read_pubkey_fast(data, o + 192),
291
+ read_pubkey_fast(data, o + 224),
292
+ ]
293
+ o += 256
294
+ coin_creator_fee = read_u64_fast(data, o)
295
+ o += 8
296
+ admin_auth = read_pubkey_fast(data, o)
297
+ o += 32
298
+ whitelist = read_pubkey_fast(data, o)
299
+ o += 32
300
+ reserved = read_pubkey_fast(data, o)
301
+ o += 32
302
+ mayhem = data[o] != 0
303
+ o += 1
304
+ reserved_list = [
305
+ read_pubkey_fast(data, o),
306
+ read_pubkey_fast(data, o + 32),
307
+ read_pubkey_fast(data, o + 64),
308
+ read_pubkey_fast(data, o + 96),
309
+ read_pubkey_fast(data, o + 128),
310
+ read_pubkey_fast(data, o + 160),
311
+ read_pubkey_fast(data, o + 192),
312
+ ]
313
+ return _account_event(
314
+ EventType.ACCOUNT_PUMP_SWAP_GLOBAL_CONFIG,
315
+ {
316
+ "metadata": metadata,
317
+ "pubkey": account.pubkey,
318
+ "config": {
319
+ "admin": admin,
320
+ "lp_fee_basis_points": lp_fee,
321
+ "protocol_fee_basis_points": protocol_fee,
322
+ "disable_flags": disable_flags,
323
+ "protocol_fee_recipients": recipients,
324
+ "coin_creator_fee_basis_points": coin_creator_fee,
325
+ "admin_set_coin_creator_authority": admin_auth,
326
+ "whitelist_pda": whitelist,
327
+ "reserved_fee_recipient": reserved,
328
+ "mayhem_mode_enabled": mayhem,
329
+ "reserved_fee_recipients": reserved_list,
330
+ },
331
+ },
332
+ )
333
+
334
+
335
+ def _parse_pumpswap_pool_fast(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
336
+ data = account.data[8:]
337
+ o = 0
338
+ pool_bump = data[o]
339
+ o += 1
340
+ index = read_u16_fast(data, o)
341
+ o += 2
342
+ creator = read_pubkey_fast(data, o)
343
+ base_mint = read_pubkey_fast(data, o + 32)
344
+ quote_mint = read_pubkey_fast(data, o + 64)
345
+ lp_mint = read_pubkey_fast(data, o + 96)
346
+ pool_base = read_pubkey_fast(data, o + 128)
347
+ pool_quote = read_pubkey_fast(data, o + 160)
348
+ o += 192
349
+ lp_supply = read_u64_fast(data, o)
350
+ o += 8
351
+ coin_creator = read_pubkey_fast(data, o)
352
+ o += 32
353
+ is_mayhem = data[o] != 0
354
+ is_cashback = data[o + 1] != 0
355
+ return _account_event(
356
+ EventType.ACCOUNT_PUMP_SWAP_POOL,
357
+ {
358
+ "metadata": metadata,
359
+ "pubkey": account.pubkey,
360
+ "pool": {
361
+ "pool_bump": pool_bump,
362
+ "index": index,
363
+ "creator": creator,
364
+ "base_mint": base_mint,
365
+ "quote_mint": quote_mint,
366
+ "lp_mint": lp_mint,
367
+ "pool_base_token_account": pool_base,
368
+ "pool_quote_token_account": pool_quote,
369
+ "lp_supply": lp_supply,
370
+ "coin_creator": coin_creator,
371
+ "is_mayhem_mode": is_mayhem,
372
+ "is_cashback_coin": is_cashback,
373
+ },
374
+ },
375
+ )
376
+
377
+
378
+ def parse_token_account(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
379
+ if len(account.data) <= 100:
380
+ event = _parse_mint_fast(account, metadata)
381
+ if event:
382
+ return event
383
+ return _parse_token_fast(account, metadata)
384
+
385
+
386
+ def parse_nonce_account(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
387
+ if len(account.data) != NONCE_ACCOUNT_SIZE:
388
+ return None
389
+ if not has_discriminator(account.data, _DISC_NONCE):
390
+ return None
391
+ return _parse_nonce_fast(account, metadata)
392
+
393
+
394
+ def parse_pumpfun_global(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
395
+ if len(account.data) < 8 + PUMPFUN_GLOBAL_BODY:
396
+ return None
397
+ if not has_discriminator(account.data, _DISC_PUMPFUN_GLOBAL):
398
+ return None
399
+ return _parse_pumpfun_global_fast(account, metadata)
400
+
401
+
402
+ def is_nonce_account(data: bytes) -> bool:
403
+ return acc_utils.is_nonce_account(data)
404
+
405
+
406
+ def parse_pumpswap_global_config(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
407
+ if len(account.data) < 8 + GLOBAL_CONFIG_BODY:
408
+ return None
409
+ if not has_discriminator(account.data, _DISC_GLOBAL_CONFIG):
410
+ return None
411
+ return _parse_pumpswap_global_config_fast(account, metadata)
412
+
413
+
414
+ def parse_pumpswap_pool(account: AccountData, metadata: EventMetadata) -> Optional[DexEvent]:
415
+ if len(account.data) < 8 + POOL_BODY:
416
+ return None
417
+ if not has_discriminator(account.data, _DISC_POOL):
418
+ return None
419
+ return _parse_pumpswap_pool_fast(account, metadata)
420
+
421
+
422
+ def is_global_config_account(data: bytes) -> bool:
423
+ return has_discriminator(data, _DISC_GLOBAL_CONFIG)
424
+
425
+
426
+ def is_pool_account(data: bytes) -> bool:
427
+ return has_discriminator(data, _DISC_POOL)
428
+
429
+
430
+ def is_pumpfun_global_account(data: bytes) -> bool:
431
+ return has_discriminator(data, _DISC_PUMPFUN_GLOBAL)
432
+
433
+
434
+ base58_encode = base58_encode_32
435
+ read_pubkey = read_pubkey_fast
436
+ read_u64_le = read_u64_fast
437
+ read_u8 = lambda data, offset: data[offset] if offset < len(data) else 0
438
+
439
+ rpc_resolve_user_wallet_pubkey = rpc_wallet.rpc_resolve_user_wallet_pubkey
440
+ user_wallet_pubkey_for_onchain_account = acc_utils.user_wallet_pubkey_for_onchain_account
441
+
442
+ __all__ = [
443
+ "AccountData",
444
+ "parse_account_unified",
445
+ "parse_token_account",
446
+ "parse_nonce_account",
447
+ "parse_pumpfun_global",
448
+ "is_nonce_account",
449
+ "is_pumpfun_global_account",
450
+ "parse_pumpswap_global_config",
451
+ "parse_pumpswap_pool",
452
+ "is_global_config_account",
453
+ "is_pool_account",
454
+ "has_discriminator",
455
+ "PUMPFUN_PROGRAM_ID",
456
+ "PUMPSWAP_PROGRAM_ID",
457
+ "rpc_resolve_user_wallet_pubkey",
458
+ "user_wallet_pubkey_for_onchain_account",
459
+ "rpc_wallet",
460
+ "acc_utils",
461
+ ]
@@ -0,0 +1,64 @@
1
+ """对齐 Rust ``accounts/rpc_wallet.rs``:RPC getAccountInfo + ``user_wallet_pubkey_for_onchain_account``。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.request
7
+ from typing import Any, Dict, Optional
8
+
9
+ from .utils import user_wallet_pubkey_for_onchain_account
10
+
11
+
12
+ def _get_account_info(rpc_url: str, address_bs58: str, timeout_s: float = 15.0) -> Optional[Dict[str, Any]]:
13
+ import base64
14
+
15
+ body = json.dumps(
16
+ {
17
+ "jsonrpc": "2.0",
18
+ "id": 1,
19
+ "method": "getAccountInfo",
20
+ "params": [address_bs58, {"encoding": "base64"}],
21
+ }
22
+ ).encode("utf-8")
23
+ req = urllib.request.Request(
24
+ rpc_url,
25
+ data=body,
26
+ headers={"Content-Type": "application/json"},
27
+ method="POST",
28
+ )
29
+ try:
30
+ with urllib.request.urlopen(req, timeout=timeout_s) as resp:
31
+ raw = json.loads(resp.read().decode("utf-8"))
32
+ except Exception:
33
+ return None
34
+ err = raw.get("error")
35
+ if err:
36
+ return None
37
+ val = (raw.get("result") or {}).get("value")
38
+ if not val:
39
+ return None
40
+ data_b64 = val.get("data")
41
+ if isinstance(data_b64, list) and data_b64:
42
+ data_b64 = data_b64[0]
43
+ if not isinstance(data_b64, str):
44
+ return None
45
+ try:
46
+ data = base64.b64decode(data_b64)
47
+ except Exception:
48
+ return None
49
+ owner = val.get("owner") or ""
50
+ exe = bool(val.get("executable", False))
51
+ return {"data": data, "owner": owner, "executable": exe}
52
+
53
+
54
+ def rpc_resolve_user_wallet_pubkey(rpc_url: str, address_bs58: str) -> Optional[str]:
55
+ """同步 JSON-RPC:``getAccountInfo`` → 用户钱包公钥 base58(与 Rust 语义一致)。"""
56
+ info = _get_account_info(rpc_url, address_bs58)
57
+ if not info:
58
+ return None
59
+ return user_wallet_pubkey_for_onchain_account(
60
+ address_bs58,
61
+ info["owner"],
62
+ info["data"],
63
+ info["executable"],
64
+ )
@@ -0,0 +1,71 @@
1
+ """对齐 Rust ``accounts/utils.rs`` 的通用工具。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+ from typing import Optional
7
+
8
+ # Solana 系统程序、SPL Token / Token-2022(与 Rust 一致)
9
+ SYSTEM_PROGRAM_ID = "11111111111111111111111111111111"
10
+ SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
11
+ SPL_TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
12
+
13
+ SPL_TOKEN_ACCOUNT_LEN = 165
14
+
15
+
16
+ def read_pubkey(data: bytes, offset: int) -> Optional[str]:
17
+ """32 字节 → base58;越界返回 None。"""
18
+ import base58
19
+
20
+ if len(data) < offset + 32:
21
+ return None
22
+ return base58.b58encode(data[offset : offset + 32]).decode("ascii")
23
+
24
+
25
+ def read_u64_le(data: bytes, offset: int) -> Optional[int]:
26
+ if len(data) < offset + 8:
27
+ return None
28
+ return struct.unpack_from("<Q", data, offset)[0]
29
+
30
+
31
+ def read_u16_le(data: bytes, offset: int) -> Optional[int]:
32
+ if len(data) < offset + 2:
33
+ return None
34
+ return struct.unpack_from("<H", data, offset)[0]
35
+
36
+
37
+ def read_u8(data: bytes, offset: int) -> Optional[int]:
38
+ if offset >= len(data):
39
+ return None
40
+ return data[offset]
41
+
42
+
43
+ def is_nonce_account(data: bytes) -> bool:
44
+ return len(data) >= 8 and data[:8] == bytes([1, 0, 0, 0, 1, 0, 0, 0])
45
+
46
+
47
+ def is_token_program_account(owner: str) -> bool:
48
+ return owner in (SPL_TOKEN_PROGRAM_ID, SPL_TOKEN_2022_PROGRAM_ID)
49
+
50
+
51
+ def has_discriminator(data: bytes, discriminator: bytes) -> bool:
52
+ if len(data) < len(discriminator):
53
+ return False
54
+ return data[: len(discriminator)] == discriminator
55
+
56
+
57
+ def user_wallet_pubkey_for_onchain_account(
58
+ address: str,
59
+ owner: str,
60
+ data: bytes,
61
+ executable: bool,
62
+ ) -> Optional[str]:
63
+ """对齐 Rust ``user_wallet_pubkey_for_onchain_account``:系统钱包或 SPL token 账户 owner。"""
64
+ if executable:
65
+ return None
66
+ if owner == SYSTEM_PROGRAM_ID:
67
+ return address if len(data) == 0 else None
68
+ if is_token_program_account(owner) and len(data) == SPL_TOKEN_ACCOUNT_LEN:
69
+ # spl_token::state::Account: owner at offset 32
70
+ return read_pubkey(data, 32)
71
+ return None
@@ -0,0 +1,18 @@
1
+ """Python 迁移门禁入口(与 TS `npm run check:migration` 中的 discriminator 交叉校验步骤对应)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .u128_parity import run_all_u128_checks
8
+ from .verify_discriminators import main as _verify_main
9
+
10
+
11
+ def main() -> None:
12
+ if _verify_main() != 0:
13
+ sys.exit(1)
14
+ sys.exit(run_all_u128_checks())
15
+
16
+
17
+ if __name__ == "__main__":
18
+ main()
sol_parser/clock.py ADDED
@@ -0,0 +1,10 @@
1
+ """高性能时间源(对齐 Rust ``sol_parser_sdk::core::now_micros`` 语义)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+
8
+ def now_micros() -> int:
9
+ """当前时间(微秒),单调性与 ``time.time_ns()`` 一致。"""
10
+ return time.time_ns() // 1000
@@ -0,0 +1,27 @@
1
+ """对齐 Rust ``common`` 模块:常量与共享类型占位。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, TypeVar
6
+
7
+ T = TypeVar("T")
8
+ AnyResult = Any # Rust: Result<T, Box<dyn Error + Send + Sync>>
9
+
10
+ DEFAULT_CONNECT_TIMEOUT = 10
11
+ DEFAULT_REQUEST_TIMEOUT = 60
12
+ DEFAULT_CHANNEL_SIZE = 1000
13
+ DEFAULT_MAX_DECODING_MESSAGE_SIZE = 10 * 1024 * 1024
14
+ DEFAULT_METRICS_WINDOW_SECONDS = 5
15
+ DEFAULT_METRICS_PRINT_INTERVAL_SECONDS = 10
16
+ SLOW_PROCESSING_THRESHOLD_US = 3000.0
17
+
18
+ __all__ = [
19
+ "AnyResult",
20
+ "DEFAULT_CONNECT_TIMEOUT",
21
+ "DEFAULT_REQUEST_TIMEOUT",
22
+ "DEFAULT_CHANNEL_SIZE",
23
+ "DEFAULT_MAX_DECODING_MESSAGE_SIZE",
24
+ "DEFAULT_METRICS_WINDOW_SECONDS",
25
+ "DEFAULT_METRICS_PRINT_INTERVAL_SECONDS",
26
+ "SLOW_PROCESSING_THRESHOLD_US",
27
+ ]