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,50 @@
1
+ """将 Rust ``serde_json`` 输出的 DexEvent 列表规范化为 Python ``legacy_dict_to_dex_event`` 可消费的 dict。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ import base58
9
+
10
+ from .event_types import DexEvent, legacy_dict_to_dex_event
11
+
12
+
13
+ def normalize_rust_json_value(v: Any) -> Any:
14
+ """递归:Solana pubkey/signature 的 byte 数组 → base58 字符串。"""
15
+ if isinstance(v, list) and v and all(isinstance(x, int) for x in v):
16
+ if len(v) in (32, 64):
17
+ try:
18
+ return base58.b58encode(bytes(v)).decode("ascii")
19
+ except Exception:
20
+ return v
21
+ if isinstance(v, dict):
22
+ return {k: normalize_rust_json_value(x) for k, x in v.items()}
23
+ if isinstance(v, list):
24
+ return [normalize_rust_json_value(x) for x in v]
25
+ return v
26
+
27
+
28
+ def dex_events_from_rust_json_str(s: str) -> List[DexEvent]:
29
+ """解析 native 扩展返回的 JSON 数组为 ``List[DexEvent]``。"""
30
+ raw = json.loads(s)
31
+ if not isinstance(raw, list):
32
+ return []
33
+ out: List[DexEvent] = []
34
+ for item in raw:
35
+ if not isinstance(item, dict) or len(item) != 1:
36
+ continue
37
+ norm = normalize_rust_json_value(item)
38
+ ev = legacy_dict_to_dex_event(norm)
39
+ if ev is not None:
40
+ out.append(ev)
41
+ return out
42
+
43
+
44
+ def dex_event_from_log_rust_json_str(s: str) -> Optional[DexEvent]:
45
+ """单条日志事件 JSON → ``DexEvent``。"""
46
+ d = json.loads(s)
47
+ if not isinstance(d, dict):
48
+ return None
49
+ norm = normalize_rust_json_value(d)
50
+ return legacy_dict_to_dex_event(norm)
@@ -0,0 +1,191 @@
1
+ """ShredStream gRPC 客户端:SubscribeEntries + bincode 解码 + PumpFun 外层指令(对齐 Node ``shredstream/client.ts`` 子集)。
2
+
3
+ 限制:与 Rust 文档一致——不解析 inner CPI 日志、不解析 ALT 展开;需安装 ``solders``(``pip install 'sol-parser-sdk-python[shredstream]'``)。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import Callable, Generator, List, Optional
11
+ from urllib.parse import urlparse
12
+
13
+ import base58
14
+ import grpc
15
+
16
+ from .entries_decode import decode_entries_bincode_flat
17
+ from .event_types import DexEvent
18
+ from .grpc_types import EventTypeFilter, IncludeOnlyFilter
19
+ from .instructions import PUMPFUN_PROGRAM_ID, parse_instruction_unified
20
+ from .pumpfun_fee_enrich import enrich_pumpfun_same_tx_post_merge
21
+ from .shredstream_pumpfun import detect_pumpfun_create_mints, parse_pumpfun_shred_ix
22
+ from .shredstream_pb2 import SubscribeEntriesRequest
23
+ from .shredstream_pb2_grpc import ShredstreamProxyStub
24
+
25
+
26
+ @dataclass
27
+ class ShredStreamConfig:
28
+ """对齐 Rust ``shredstream::config::ShredStreamConfig``。"""
29
+
30
+ connection_timeout_ms: int = 8000
31
+ request_timeout_ms: int = 15000
32
+ max_decoding_message_size: int = 100 * 1024 * 1024
33
+ reconnect_delay_ms: int = 1000
34
+ max_reconnect_attempts: int = 3
35
+
36
+ @staticmethod
37
+ def low_latency() -> ShredStreamConfig:
38
+ return ShredStreamConfig(
39
+ connection_timeout_ms=5000,
40
+ request_timeout_ms=10000,
41
+ max_decoding_message_size=50 * 1024 * 1024,
42
+ reconnect_delay_ms=100,
43
+ max_reconnect_attempts=1,
44
+ )
45
+
46
+ @staticmethod
47
+ def high_throughput() -> ShredStreamConfig:
48
+ return ShredStreamConfig(
49
+ connection_timeout_ms=10000,
50
+ request_timeout_ms=30000,
51
+ max_decoding_message_size=200 * 1024 * 1024,
52
+ reconnect_delay_ms=2000,
53
+ max_reconnect_attempts=5,
54
+ )
55
+
56
+
57
+ def _parsed_endpoint(endpoint: str) -> tuple[str, str]:
58
+ if "://" not in endpoint:
59
+ endpoint = "http://" + endpoint
60
+ p = urlparse(endpoint)
61
+ return p.scheme, p.netloc or endpoint
62
+
63
+
64
+ def _ix_accounts_bytes(account_indices: object) -> bytes:
65
+ if isinstance(account_indices, (bytes, bytearray, memoryview)):
66
+ return bytes(account_indices)
67
+ return bytes(list(account_indices))
68
+
69
+
70
+ def _events_from_versioned_tx_wire(
71
+ raw: bytes,
72
+ signature: str,
73
+ slot: int,
74
+ tx_index: int,
75
+ recv_us: int,
76
+ filter: EventTypeFilter,
77
+ ) -> List[DexEvent]:
78
+ try:
79
+ from solders.message import Message as LegacyMessage # type: ignore
80
+ from solders.message import MessageV0 # type: ignore
81
+ from solders.transaction import VersionedTransaction # type: ignore
82
+ except ImportError:
83
+ return []
84
+
85
+ vt = VersionedTransaction.from_bytes(raw)
86
+ if not vt.signatures:
87
+ return []
88
+ sig = signature or base58.b58encode(bytes(vt.signatures[0])).decode("ascii")
89
+ msg = vt.message
90
+ out: List[DexEvent] = []
91
+
92
+ if isinstance(msg, MessageV0):
93
+ keys = [str(k) for k in msg.account_keys]
94
+ ixs: List[tuple] = []
95
+ for cix in msg.compiled_instructions:
96
+ pid = keys[cix.program_id_index]
97
+ ixs.append((pid, bytes(cix.data), _ix_accounts_bytes(cix.accounts)))
98
+ elif isinstance(msg, LegacyMessage):
99
+ keys = [str(k) for k in msg.account_keys]
100
+ ixs = []
101
+ for ix in msg.instructions:
102
+ pid = keys[ix.program_id_index]
103
+ ixs.append((pid, bytes(ix.data), _ix_accounts_bytes(ix.accounts)))
104
+ else:
105
+ return []
106
+
107
+ created: set = set()
108
+ mayhem: set = set()
109
+ for pid, data, ix_acc in ixs:
110
+ c, m = detect_pumpfun_create_mints(pid, data, ix_acc, keys)
111
+ created |= c
112
+ mayhem |= m
113
+
114
+ for pid, data, ix_acc in ixs:
115
+ idxs = list(ix_acc)
116
+ accounts = [keys[i] for i in idxs if i < len(keys)]
117
+ if pid == PUMPFUN_PROGRAM_ID:
118
+ ev = parse_pumpfun_shred_ix(
119
+ data, keys, ix_acc, pid, sig, slot, tx_index, recv_us, created, mayhem
120
+ )
121
+ if ev:
122
+ out.append(ev)
123
+ continue
124
+ ev = parse_instruction_unified(
125
+ bytes(data), accounts, sig, slot, tx_index, None, recv_us, filter, pid
126
+ )
127
+ if ev:
128
+ out.append(ev)
129
+
130
+ for ev in out:
131
+ if hasattr(ev.data, "metadata"):
132
+ ev.data.metadata.grpc_recv_us = recv_us
133
+ enrich_pumpfun_same_tx_post_merge(out)
134
+ return out
135
+
136
+
137
+ class ShredStreamClient:
138
+ """阻塞式 gRPC 客户端(可在 asyncio 中用 ``asyncio.to_thread`` 包装)。"""
139
+
140
+ def __init__(self, endpoint: str, config: Optional[ShredStreamConfig] = None):
141
+ self.endpoint = endpoint
142
+ self.config = config or ShredStreamConfig()
143
+
144
+ @classmethod
145
+ def new_with_config(cls, endpoint: str, config: ShredStreamConfig) -> ShredStreamClient:
146
+ """对齐 Rust ``ShredStreamClient::new_with_config``。"""
147
+ return cls(endpoint, config)
148
+
149
+ def iter_dex_events(
150
+ self,
151
+ filter: Optional[EventTypeFilter] = None,
152
+ on_error: Optional[Callable[[Exception], None]] = None,
153
+ ) -> Generator[DexEvent, None, None]:
154
+ """订阅 ``SubscribeEntries``,解码每笔线交易中的 PumpFun 外层指令事件。"""
155
+ f: EventTypeFilter = filter if filter is not None else IncludeOnlyFilter([])
156
+ scheme, target = _parsed_endpoint(self.endpoint)
157
+ opts = [
158
+ ("grpc.max_receive_message_length", self.config.max_decoding_message_size),
159
+ ("grpc.max_send_message_length", self.config.max_decoding_message_size),
160
+ ]
161
+ if scheme == "https":
162
+ channel = grpc.secure_channel(
163
+ target, grpc.ssl_channel_credentials(), options=opts
164
+ )
165
+ else:
166
+ channel = grpc.insecure_channel(target, options=opts)
167
+ stub = ShredstreamProxyStub(channel)
168
+ recv_us = int(time.time() * 1_000_000)
169
+ tx_counter = 0
170
+ try:
171
+ for entry in stub.SubscribeEntries(SubscribeEntriesRequest()):
172
+ slot = entry.slot
173
+ try:
174
+ raws = decode_entries_bincode_flat(bytes(entry.entries))
175
+ except Exception as e:
176
+ if on_error:
177
+ on_error(e)
178
+ continue
179
+ for raw in raws:
180
+ sig0 = ""
181
+ try:
182
+ from solders.transaction import VersionedTransaction # type: ignore
183
+
184
+ sig0 = base58.b58encode(bytes(VersionedTransaction.from_bytes(raw).signatures[0])).decode("ascii")
185
+ except Exception:
186
+ pass
187
+ for ev in _events_from_versioned_tx_wire(raw, sig0, slot, tx_counter, recv_us, f):
188
+ yield ev
189
+ tx_counter += 1
190
+ finally:
191
+ channel.close()
@@ -0,0 +1,40 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: shredstream.proto
5
+ # Protobuf Python Version: 6.31.1
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 31,
16
+ 1,
17
+ '',
18
+ 'shredstream.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11shredstream.proto\x12\x0bshredstream\"\x19\n\x17SubscribeEntriesRequest\"&\n\x05\x45ntry\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x0f\n\x07\x65ntries\x18\x02 \x01(\x0c\x32\x62\n\x10ShredstreamProxy\x12N\n\x10SubscribeEntries\x12$.shredstream.SubscribeEntriesRequest\x1a\x12.shredstream.Entry0\x01\x62\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'shredstream_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ DESCRIPTOR._loaded_options = None
34
+ _globals['_SUBSCRIBEENTRIESREQUEST']._serialized_start=34
35
+ _globals['_SUBSCRIBEENTRIESREQUEST']._serialized_end=59
36
+ _globals['_ENTRY']._serialized_start=61
37
+ _globals['_ENTRY']._serialized_end=99
38
+ _globals['_SHREDSTREAMPROXY']._serialized_start=101
39
+ _globals['_SHREDSTREAMPROXY']._serialized_end=199
40
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,81 @@
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+ import warnings
5
+
6
+ from . import shredstream_pb2 as shredstream__pb2
7
+
8
+
9
+ class ShredstreamProxyStub(object):
10
+ """与 Rust `shredstream/proto/mod.rs` 内嵌定义一致;路径 /shredstream.ShredstreamProxy/SubscribeEntries
11
+ """
12
+
13
+ def __init__(self, channel):
14
+ """Constructor.
15
+
16
+ Args:
17
+ channel: A grpc.Channel.
18
+ """
19
+ self.SubscribeEntries = channel.unary_stream(
20
+ '/shredstream.ShredstreamProxy/SubscribeEntries',
21
+ request_serializer=shredstream__pb2.SubscribeEntriesRequest.SerializeToString,
22
+ response_deserializer=shredstream__pb2.Entry.FromString,
23
+ _registered_method=True)
24
+
25
+
26
+ class ShredstreamProxyServicer(object):
27
+ """与 Rust `shredstream/proto/mod.rs` 内嵌定义一致;路径 /shredstream.ShredstreamProxy/SubscribeEntries
28
+ """
29
+
30
+ def SubscribeEntries(self, request, context):
31
+ """Missing associated documentation comment in .proto file."""
32
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
33
+ context.set_details('Method not implemented!')
34
+ raise NotImplementedError('Method not implemented!')
35
+
36
+
37
+ def add_ShredstreamProxyServicer_to_server(servicer, server):
38
+ rpc_method_handlers = {
39
+ 'SubscribeEntries': grpc.unary_stream_rpc_method_handler(
40
+ servicer.SubscribeEntries,
41
+ request_deserializer=shredstream__pb2.SubscribeEntriesRequest.FromString,
42
+ response_serializer=shredstream__pb2.Entry.SerializeToString,
43
+ ),
44
+ }
45
+ generic_handler = grpc.method_handlers_generic_handler(
46
+ 'shredstream.ShredstreamProxy', rpc_method_handlers)
47
+ server.add_generic_rpc_handlers((generic_handler,))
48
+ server.add_registered_method_handlers('shredstream.ShredstreamProxy', rpc_method_handlers)
49
+
50
+
51
+ # This class is part of an EXPERIMENTAL API.
52
+ class ShredstreamProxy(object):
53
+ """与 Rust `shredstream/proto/mod.rs` 内嵌定义一致;路径 /shredstream.ShredstreamProxy/SubscribeEntries
54
+ """
55
+
56
+ @staticmethod
57
+ def SubscribeEntries(request,
58
+ target,
59
+ options=(),
60
+ channel_credentials=None,
61
+ call_credentials=None,
62
+ insecure=False,
63
+ compression=None,
64
+ wait_for_ready=None,
65
+ timeout=None,
66
+ metadata=None):
67
+ return grpc.experimental.unary_stream(
68
+ request,
69
+ target,
70
+ '/shredstream.ShredstreamProxy/SubscribeEntries',
71
+ shredstream__pb2.SubscribeEntriesRequest.SerializeToString,
72
+ shredstream__pb2.Entry.FromString,
73
+ options,
74
+ channel_credentials,
75
+ insecure,
76
+ call_credentials,
77
+ compression,
78
+ wait_for_ready,
79
+ timeout,
80
+ metadata,
81
+ _registered_method=True)
@@ -0,0 +1,296 @@
1
+ """ShredStream 路径下的 PumpFun 外层指令:mint 检测与 Buy/Sell/BuyExactSolIn(对齐 Rust ``shredstream/client``)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+ from typing import List, Set, Tuple
7
+
8
+ from .event_types import DexEvent, PumpFunTradeEvent
9
+ from .grpc_types import EventMetadata, EventType
10
+ from .instructions import PUMPFUN_PROGRAM_ID, parse_pumpfun_instruction
11
+
12
+ _DISC_CREATE = bytes([24, 30, 200, 40, 5, 28, 7, 119])
13
+ _DISC_CREATE_V2 = bytes([214, 144, 76, 236, 95, 139, 49, 180])
14
+ _DISC_BUY = bytes([102, 6, 61, 18, 1, 218, 235, 234])
15
+ _DISC_SELL = bytes([51, 230, 133, 164, 1, 127, 131, 173])
16
+ _DISC_BUY_EXACT_SOL_IN = bytes([56, 252, 116, 8, 158, 223, 205, 95])
17
+ _DISC_BUY_V2 = bytes([184, 23, 238, 97, 103, 197, 211, 61])
18
+ _DISC_SELL_V2 = bytes([93, 246, 130, 60, 231, 233, 64, 178])
19
+ _DISC_BUY_EXACT_QUOTE_IN_V2 = bytes([194, 171, 28, 70, 104, 77, 91, 47])
20
+
21
+
22
+ def _get_acct(accounts: List[str], ix_accounts: bytes, idx: int) -> str:
23
+ if idx >= len(ix_accounts):
24
+ return ""
25
+ ai = ix_accounts[idx]
26
+ if ai >= len(accounts):
27
+ return ""
28
+ return accounts[ai]
29
+
30
+
31
+ def _parse_create_v2_mayhem(data_after_disc: bytes) -> bool:
32
+ o = 0
33
+ for _ in range(3):
34
+ if len(data_after_disc) < o + 4:
35
+ return False
36
+ (ln,) = struct.unpack_from("<I", data_after_disc, o)
37
+ o += 4 + int(ln)
38
+ if len(data_after_disc) < o + 32 + 1:
39
+ return False
40
+ o += 32
41
+ return data_after_disc[o] != 0
42
+
43
+
44
+ def detect_pumpfun_create_mints(
45
+ program_id: str,
46
+ data: bytes,
47
+ ix_accounts: bytes,
48
+ accounts: List[str],
49
+ ) -> Tuple[Set[str], Set[str]]:
50
+ """返回 ``(created_mints, mayhem_mints)``。"""
51
+ created: Set[str] = set()
52
+ mayhem: Set[str] = set()
53
+ if program_id != PUMPFUN_PROGRAM_ID or len(data) < 8:
54
+ return created, mayhem
55
+ disc = data[:8]
56
+ if disc == _DISC_CREATE or disc == _DISC_CREATE_V2:
57
+ if not ix_accounts:
58
+ return created, mayhem
59
+ mint = _get_acct(accounts, ix_accounts, 0)
60
+ if mint:
61
+ created.add(mint)
62
+ if disc == _DISC_CREATE_V2:
63
+ if _parse_create_v2_mayhem(data[8:]):
64
+ mayhem.add(mint)
65
+ return created, mayhem
66
+
67
+
68
+ def _meta(sig: str, slot: int, tx_index: int, recv_us: int) -> EventMetadata:
69
+ return EventMetadata(
70
+ signature=sig,
71
+ slot=slot,
72
+ tx_index=tx_index,
73
+ block_time_us=0,
74
+ grpc_recv_us=recv_us,
75
+ )
76
+
77
+
78
+ def _token_program_default(tp: str) -> str:
79
+ if not tp or tp == "11111111111111111111111111111111":
80
+ return "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
81
+ return tp
82
+
83
+
84
+ def parse_pumpfun_buy(
85
+ data: bytes,
86
+ accounts: List[str],
87
+ ix_accounts: bytes,
88
+ sig: str,
89
+ slot: int,
90
+ tx_index: int,
91
+ recv_us: int,
92
+ created_mints: Set[str],
93
+ mayhem_mints: Set[str],
94
+ ) -> DexEvent | None:
95
+ if len(ix_accounts) < 7 or len(data) < 8:
96
+ return None
97
+ payload = data[8:]
98
+ ta = struct.unpack_from("<Q", payload, 0)[0] if len(payload) >= 8 else 0
99
+ sa = struct.unpack_from("<Q", payload, 8)[0] if len(payload) >= 16 else 0
100
+ mint = _get_acct(accounts, ix_accounts, 2)
101
+ if not mint:
102
+ return None
103
+ m = _meta(sig, slot, tx_index, recv_us)
104
+ return DexEvent(
105
+ type=EventType.PUMP_FUN_TRADE,
106
+ data=PumpFunTradeEvent(
107
+ metadata=m,
108
+ mint=mint,
109
+ bonding_curve=_get_acct(accounts, ix_accounts, 3),
110
+ user=_get_acct(accounts, ix_accounts, 6),
111
+ sol_amount=sa,
112
+ token_amount=ta,
113
+ fee_recipient=_get_acct(accounts, ix_accounts, 1),
114
+ is_buy=True,
115
+ is_created_buy=mint in created_mints,
116
+ ix_name="buy",
117
+ mayhem_mode=mint in mayhem_mints,
118
+ associated_bonding_curve=_get_acct(accounts, ix_accounts, 4),
119
+ token_program=_token_program_default(_get_acct(accounts, ix_accounts, 8)),
120
+ creator_vault=_get_acct(accounts, ix_accounts, 9),
121
+ ),
122
+ )
123
+
124
+
125
+ def parse_pumpfun_sell(
126
+ data: bytes,
127
+ accounts: List[str],
128
+ ix_accounts: bytes,
129
+ sig: str,
130
+ slot: int,
131
+ tx_index: int,
132
+ recv_us: int,
133
+ ) -> DexEvent | None:
134
+ if len(ix_accounts) < 7 or len(data) < 8:
135
+ return None
136
+ payload = data[8:]
137
+ ta = struct.unpack_from("<Q", payload, 0)[0] if len(payload) >= 8 else 0
138
+ sa = struct.unpack_from("<Q", payload, 8)[0] if len(payload) >= 16 else 0
139
+ mint = _get_acct(accounts, ix_accounts, 2)
140
+ if not mint:
141
+ return None
142
+ m = _meta(sig, slot, tx_index, recv_us)
143
+ return DexEvent(
144
+ type=EventType.PUMP_FUN_TRADE,
145
+ data=PumpFunTradeEvent(
146
+ metadata=m,
147
+ mint=mint,
148
+ bonding_curve=_get_acct(accounts, ix_accounts, 3),
149
+ user=_get_acct(accounts, ix_accounts, 6),
150
+ sol_amount=sa,
151
+ token_amount=ta,
152
+ fee_recipient=_get_acct(accounts, ix_accounts, 1),
153
+ is_buy=False,
154
+ is_created_buy=False,
155
+ ix_name="sell",
156
+ associated_bonding_curve=_get_acct(accounts, ix_accounts, 4),
157
+ token_program=_token_program_default(_get_acct(accounts, ix_accounts, 9)),
158
+ creator_vault=_get_acct(accounts, ix_accounts, 8),
159
+ ),
160
+ )
161
+
162
+
163
+ def parse_pumpfun_buy_exact_sol_in(
164
+ data: bytes,
165
+ accounts: List[str],
166
+ ix_accounts: bytes,
167
+ sig: str,
168
+ slot: int,
169
+ tx_index: int,
170
+ recv_us: int,
171
+ created_mints: Set[str],
172
+ mayhem_mints: Set[str],
173
+ ) -> DexEvent | None:
174
+ if len(ix_accounts) < 7 or len(data) < 8:
175
+ return None
176
+ payload = data[8:]
177
+ sa = struct.unpack_from("<Q", payload, 0)[0] if len(payload) >= 8 else 0
178
+ ta = struct.unpack_from("<Q", payload, 8)[0] if len(payload) >= 16 else 0
179
+ mint = _get_acct(accounts, ix_accounts, 2)
180
+ if not mint:
181
+ return None
182
+ m = _meta(sig, slot, tx_index, recv_us)
183
+ return DexEvent(
184
+ type=EventType.PUMP_FUN_TRADE,
185
+ data=PumpFunTradeEvent(
186
+ metadata=m,
187
+ mint=mint,
188
+ bonding_curve=_get_acct(accounts, ix_accounts, 3),
189
+ user=_get_acct(accounts, ix_accounts, 6),
190
+ sol_amount=sa,
191
+ token_amount=ta,
192
+ fee_recipient=_get_acct(accounts, ix_accounts, 1),
193
+ is_buy=True,
194
+ is_created_buy=mint in created_mints,
195
+ ix_name="buy_exact_sol_in",
196
+ mayhem_mode=mint in mayhem_mints,
197
+ associated_bonding_curve=_get_acct(accounts, ix_accounts, 4),
198
+ token_program=_token_program_default(_get_acct(accounts, ix_accounts, 8)),
199
+ creator_vault=_get_acct(accounts, ix_accounts, 9),
200
+ ),
201
+ )
202
+
203
+
204
+ def parse_pumpfun_trade_v2(
205
+ ix_name: str,
206
+ data: bytes,
207
+ accounts: List[str],
208
+ ix_accounts: bytes,
209
+ sig: str,
210
+ slot: int,
211
+ tx_index: int,
212
+ recv_us: int,
213
+ created_mints: Set[str],
214
+ mayhem_mints: Set[str],
215
+ ) -> DexEvent | None:
216
+ min_accounts = 26 if ix_name == "sell_v2" else 27
217
+ if len(ix_accounts) < min_accounts or len(data) < 8:
218
+ return None
219
+ payload = data[8:]
220
+ first = struct.unpack_from("<Q", payload, 0)[0] if len(payload) >= 8 else 0
221
+ second = struct.unpack_from("<Q", payload, 8)[0] if len(payload) >= 16 else 0
222
+ if ix_name == "buy_exact_quote_in_v2":
223
+ sol_amount, token_amount = first, second
224
+ else:
225
+ token_amount, sol_amount = first, second
226
+ mint = _get_acct(accounts, ix_accounts, 1)
227
+ if not mint:
228
+ return None
229
+ return DexEvent(
230
+ type=EventType.PUMP_FUN_TRADE,
231
+ data=PumpFunTradeEvent(
232
+ metadata=_meta(sig, slot, tx_index, recv_us),
233
+ mint=mint,
234
+ bonding_curve=_get_acct(accounts, ix_accounts, 10),
235
+ user=_get_acct(accounts, ix_accounts, 13),
236
+ sol_amount=sol_amount,
237
+ token_amount=token_amount,
238
+ fee_recipient=_get_acct(accounts, ix_accounts, 6),
239
+ is_buy=ix_name != "sell_v2",
240
+ is_created_buy=mint in created_mints,
241
+ ix_name=ix_name,
242
+ mayhem_mode=mint in mayhem_mints,
243
+ associated_bonding_curve=_get_acct(accounts, ix_accounts, 11),
244
+ token_program=_token_program_default(_get_acct(accounts, ix_accounts, 3)),
245
+ creator_vault=_get_acct(accounts, ix_accounts, 16),
246
+ ),
247
+ )
248
+
249
+
250
+ def parse_pumpfun_shred_ix(
251
+ data: bytes,
252
+ accounts: List[str],
253
+ ix_accounts: bytes,
254
+ program_id: str,
255
+ sig: str,
256
+ slot: int,
257
+ tx_index: int,
258
+ recv_us: int,
259
+ created_mints: Set[str],
260
+ mayhem_mints: Set[str],
261
+ ) -> DexEvent | None:
262
+ if program_id != PUMPFUN_PROGRAM_ID or len(data) < 8:
263
+ return None
264
+ disc = data[:8]
265
+ if disc == _DISC_BUY:
266
+ return parse_pumpfun_buy(
267
+ data, accounts, ix_accounts, sig, slot, tx_index, recv_us, created_mints, mayhem_mints
268
+ )
269
+ if disc == _DISC_SELL:
270
+ return parse_pumpfun_sell(data, accounts, ix_accounts, sig, slot, tx_index, recv_us)
271
+ if disc == _DISC_BUY_EXACT_SOL_IN:
272
+ return parse_pumpfun_buy_exact_sol_in(
273
+ data, accounts, ix_accounts, sig, slot, tx_index, recv_us, created_mints, mayhem_mints
274
+ )
275
+ if disc == _DISC_BUY_V2:
276
+ return parse_pumpfun_trade_v2(
277
+ "buy_v2", data, accounts, ix_accounts, sig, slot, tx_index, recv_us, created_mints, mayhem_mints
278
+ )
279
+ if disc == _DISC_BUY_EXACT_QUOTE_IN_V2:
280
+ return parse_pumpfun_trade_v2(
281
+ "buy_exact_quote_in_v2",
282
+ data,
283
+ accounts,
284
+ ix_accounts,
285
+ sig,
286
+ slot,
287
+ tx_index,
288
+ recv_us,
289
+ created_mints,
290
+ mayhem_mints,
291
+ )
292
+ if disc == _DISC_SELL_V2:
293
+ return parse_pumpfun_trade_v2(
294
+ "sell_v2", data, accounts, ix_accounts, sig, slot, tx_index, recv_us, created_mints, mayhem_mints
295
+ )
296
+ return parse_pumpfun_instruction(data, accounts, sig, slot, tx_index, None, recv_us)