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,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)
|