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,171 @@
|
|
|
1
|
+
"""OrderMode buffers for low-latency DEX subscriptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Callable, Dict, List, Tuple
|
|
8
|
+
|
|
9
|
+
from .event_types import DexEvent
|
|
10
|
+
from .grpc_types import ClientConfig, OrderMode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class _Batch:
|
|
15
|
+
slot: int
|
|
16
|
+
tx_index: int
|
|
17
|
+
seq: int
|
|
18
|
+
events: List[DexEvent]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _event_slot_tx(events: List[DexEvent], fallback_slot: int, fallback_tx_index: int) -> Tuple[int, int]:
|
|
22
|
+
if not events:
|
|
23
|
+
return fallback_slot, fallback_tx_index
|
|
24
|
+
meta = getattr(events[0].data, "metadata", None)
|
|
25
|
+
slot = int(getattr(meta, "slot", fallback_slot) or fallback_slot)
|
|
26
|
+
tx_index = int(getattr(meta, "tx_index", fallback_tx_index) or fallback_tx_index)
|
|
27
|
+
return slot, tx_index
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OrderDispatcher:
|
|
31
|
+
def __init__(self, config: ClientConfig):
|
|
32
|
+
self.mode = config.order_mode
|
|
33
|
+
self.timeout_s = max(0.001, float(config.order_timeout_ms or 100) / 1000.0)
|
|
34
|
+
self.micro_batch_s = max(0.000001, float(config.micro_batch_us or 100) / 1_000_000.0)
|
|
35
|
+
self.slots: Dict[int, List[_Batch]] = {}
|
|
36
|
+
self.watermarks: Dict[int, int] = {}
|
|
37
|
+
self.micro_batch: List[_Batch] = []
|
|
38
|
+
self.micro_start = 0.0
|
|
39
|
+
self.last_flush = time.monotonic()
|
|
40
|
+
self.current_slot = 0
|
|
41
|
+
self.seq = 0
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def needs_timer(self) -> bool:
|
|
45
|
+
return self.mode != OrderMode.UNORDERED
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def interval_s(self) -> float:
|
|
49
|
+
if self.mode == OrderMode.MICRO_BATCH:
|
|
50
|
+
return self.micro_batch_s
|
|
51
|
+
return max(0.001, self.timeout_s / 2.0)
|
|
52
|
+
|
|
53
|
+
def push_transaction_events(
|
|
54
|
+
self,
|
|
55
|
+
events: List[DexEvent],
|
|
56
|
+
fallback_slot: int,
|
|
57
|
+
fallback_tx_index: int,
|
|
58
|
+
emit: Callable[[DexEvent], None],
|
|
59
|
+
) -> None:
|
|
60
|
+
if not events:
|
|
61
|
+
return
|
|
62
|
+
slot, tx_index = _event_slot_tx(events, fallback_slot, fallback_tx_index)
|
|
63
|
+
batch = _Batch(slot=slot, tx_index=tx_index, seq=self.seq, events=events)
|
|
64
|
+
self.seq += 1
|
|
65
|
+
|
|
66
|
+
if self.mode == OrderMode.UNORDERED:
|
|
67
|
+
self._emit_batch(batch, emit)
|
|
68
|
+
elif self.mode == OrderMode.ORDERED:
|
|
69
|
+
self._push_ordered(batch, emit)
|
|
70
|
+
elif self.mode == OrderMode.STREAMING_ORDERED:
|
|
71
|
+
self._push_streaming(batch, emit)
|
|
72
|
+
elif self.mode == OrderMode.MICRO_BATCH:
|
|
73
|
+
self._push_micro_batch(batch, emit)
|
|
74
|
+
else:
|
|
75
|
+
self._emit_batch(batch, emit)
|
|
76
|
+
|
|
77
|
+
def flush_due(self, emit: Callable[[DexEvent], None]) -> None:
|
|
78
|
+
now = time.monotonic()
|
|
79
|
+
if self.mode in (OrderMode.ORDERED, OrderMode.STREAMING_ORDERED):
|
|
80
|
+
if self.slots and now - self.last_flush > self.timeout_s:
|
|
81
|
+
self._flush_all_slots(emit)
|
|
82
|
+
if self.mode == OrderMode.MICRO_BATCH:
|
|
83
|
+
if self.micro_batch and now - self.micro_start >= self.micro_batch_s:
|
|
84
|
+
self._flush_micro_batch(emit)
|
|
85
|
+
|
|
86
|
+
def flush_all(self, emit: Callable[[DexEvent], None]) -> None:
|
|
87
|
+
self._flush_all_slots(emit)
|
|
88
|
+
self._flush_micro_batch(emit)
|
|
89
|
+
|
|
90
|
+
def _push_ordered(self, batch: _Batch, emit: Callable[[DexEvent], None]) -> None:
|
|
91
|
+
if batch.slot > self.current_slot and self.current_slot > 0:
|
|
92
|
+
self._flush_before(batch.slot, emit)
|
|
93
|
+
if batch.slot > self.current_slot:
|
|
94
|
+
self.current_slot = batch.slot
|
|
95
|
+
self.slots.setdefault(batch.slot, []).append(batch)
|
|
96
|
+
|
|
97
|
+
def _push_streaming(self, batch: _Batch, emit: Callable[[DexEvent], None]) -> None:
|
|
98
|
+
if batch.slot > self.current_slot and self.current_slot > 0:
|
|
99
|
+
self._flush_before(batch.slot, emit)
|
|
100
|
+
for slot in list(self.watermarks):
|
|
101
|
+
if slot < batch.slot:
|
|
102
|
+
self.watermarks.pop(slot, None)
|
|
103
|
+
if batch.slot > self.current_slot:
|
|
104
|
+
self.current_slot = batch.slot
|
|
105
|
+
|
|
106
|
+
expected = self.watermarks.get(batch.slot, 0)
|
|
107
|
+
if batch.tx_index == expected:
|
|
108
|
+
self._emit_batch(batch, emit)
|
|
109
|
+
watermark = expected + 1
|
|
110
|
+
buffered = self.slots.get(batch.slot, [])
|
|
111
|
+
buffered.sort(key=_batch_key)
|
|
112
|
+
while True:
|
|
113
|
+
pos = next((i for i, item in enumerate(buffered) if item.tx_index == watermark), -1)
|
|
114
|
+
if pos < 0:
|
|
115
|
+
break
|
|
116
|
+
self._emit_batch(buffered.pop(pos), emit)
|
|
117
|
+
watermark += 1
|
|
118
|
+
if buffered:
|
|
119
|
+
self.slots[batch.slot] = buffered
|
|
120
|
+
else:
|
|
121
|
+
self.slots.pop(batch.slot, None)
|
|
122
|
+
self.watermarks[batch.slot] = watermark
|
|
123
|
+
self.last_flush = time.monotonic()
|
|
124
|
+
elif batch.tx_index > expected:
|
|
125
|
+
self.slots.setdefault(batch.slot, []).append(batch)
|
|
126
|
+
|
|
127
|
+
def _push_micro_batch(self, batch: _Batch, emit: Callable[[DexEvent], None]) -> None:
|
|
128
|
+
now = time.monotonic()
|
|
129
|
+
if not self.micro_batch:
|
|
130
|
+
self.micro_start = now
|
|
131
|
+
self.micro_batch.append(batch)
|
|
132
|
+
if now - self.micro_start >= self.micro_batch_s:
|
|
133
|
+
self._flush_micro_batch(emit)
|
|
134
|
+
|
|
135
|
+
def _flush_before(self, slot: int, emit: Callable[[DexEvent], None]) -> None:
|
|
136
|
+
for s in sorted(k for k in self.slots if k < slot):
|
|
137
|
+
batches = self.slots.pop(s)
|
|
138
|
+
batches.sort(key=_batch_key)
|
|
139
|
+
for batch in batches:
|
|
140
|
+
self._emit_batch(batch, emit)
|
|
141
|
+
self.watermarks.pop(s, None)
|
|
142
|
+
self.last_flush = time.monotonic()
|
|
143
|
+
|
|
144
|
+
def _flush_all_slots(self, emit: Callable[[DexEvent], None]) -> None:
|
|
145
|
+
for s in sorted(self.slots):
|
|
146
|
+
batches = self.slots[s]
|
|
147
|
+
batches.sort(key=_batch_key)
|
|
148
|
+
for batch in batches:
|
|
149
|
+
self._emit_batch(batch, emit)
|
|
150
|
+
self.slots.clear()
|
|
151
|
+
self.watermarks.clear()
|
|
152
|
+
self.last_flush = time.monotonic()
|
|
153
|
+
|
|
154
|
+
def _flush_micro_batch(self, emit: Callable[[DexEvent], None]) -> None:
|
|
155
|
+
if not self.micro_batch:
|
|
156
|
+
return
|
|
157
|
+
self.micro_batch.sort(key=_batch_key)
|
|
158
|
+
for batch in self.micro_batch:
|
|
159
|
+
self._emit_batch(batch, emit)
|
|
160
|
+
self.micro_batch = []
|
|
161
|
+
self.micro_start = 0.0
|
|
162
|
+
self.last_flush = time.monotonic()
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _emit_batch(batch: _Batch, emit: Callable[[DexEvent], None]) -> None:
|
|
166
|
+
for event in batch.events:
|
|
167
|
+
emit(event)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _batch_key(batch: _Batch) -> Tuple[int, int, int]:
|
|
171
|
+
return (batch.slot, batch.tx_index, batch.seq)
|
sol_parser/parser.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base58
|
|
4
|
+
import base64
|
|
5
|
+
import struct
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from .dex_parsers import (
|
|
10
|
+
DexEvent,
|
|
11
|
+
apply_event_type_filter,
|
|
12
|
+
dispatch_program_data,
|
|
13
|
+
event_type_for_discriminator,
|
|
14
|
+
filter_allows_unknown_log_event,
|
|
15
|
+
parse_trade_from_data,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .grpc_types import SubscribeUpdateTransactionInfo
|
|
20
|
+
from .pumpfun_fee_enrich import enrich_pumpfun_same_tx_post_merge
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _disc8(bs: bytes) -> int:
|
|
24
|
+
return struct.unpack("<Q", bs)[0]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def decode_program_data_line(log: str) -> Optional[bytes]:
|
|
28
|
+
p = "Program data: "
|
|
29
|
+
i = log.find(p)
|
|
30
|
+
if i < 0:
|
|
31
|
+
return None
|
|
32
|
+
s = log[i + len(p) :].strip()
|
|
33
|
+
if len(s) > 2700:
|
|
34
|
+
return None
|
|
35
|
+
try:
|
|
36
|
+
raw = base64.standard_b64decode(s)
|
|
37
|
+
except Exception:
|
|
38
|
+
return None
|
|
39
|
+
if len(raw) < 8 or len(raw) > 2048:
|
|
40
|
+
return None
|
|
41
|
+
return raw
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _meta(
|
|
45
|
+
sig: str,
|
|
46
|
+
slot: int,
|
|
47
|
+
tx_idx: int,
|
|
48
|
+
block_us: Optional[int],
|
|
49
|
+
grpc_us: int,
|
|
50
|
+
recent_blockhash: str = "",
|
|
51
|
+
) -> dict:
|
|
52
|
+
m: dict = {
|
|
53
|
+
"signature": sig,
|
|
54
|
+
"slot": slot,
|
|
55
|
+
"tx_index": tx_idx,
|
|
56
|
+
"block_time_us": 0 if block_us is None else block_us,
|
|
57
|
+
"grpc_recv_us": grpc_us,
|
|
58
|
+
}
|
|
59
|
+
if recent_blockhash:
|
|
60
|
+
m["recent_blockhash"] = recent_blockhash
|
|
61
|
+
return m
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_log_optimized(
|
|
65
|
+
log: str,
|
|
66
|
+
signature: str,
|
|
67
|
+
slot: int,
|
|
68
|
+
tx_index: int = 0,
|
|
69
|
+
block_time_us: Optional[int] = None,
|
|
70
|
+
grpc_recv_us: Optional[int] = None,
|
|
71
|
+
event_type_filter: Any = None,
|
|
72
|
+
is_created_buy: bool = False,
|
|
73
|
+
recent_blockhash: str = "",
|
|
74
|
+
) -> Optional[DexEvent]:
|
|
75
|
+
"""单次 base64 decode 后按 discriminator 做 early filter,再按实际事件类型二次过滤。"""
|
|
76
|
+
grpc = int(time.time() * 1_000_000) if grpc_recv_us is None else grpc_recv_us
|
|
77
|
+
buf = decode_program_data_line(log)
|
|
78
|
+
if not buf:
|
|
79
|
+
return None
|
|
80
|
+
disc = _disc8(buf[:8])
|
|
81
|
+
if event_type_filter is not None:
|
|
82
|
+
event_type = event_type_for_discriminator(disc)
|
|
83
|
+
if event_type is not None:
|
|
84
|
+
if not event_type_filter.should_include(event_type):
|
|
85
|
+
return None
|
|
86
|
+
elif not filter_allows_unknown_log_event(event_type_filter):
|
|
87
|
+
return None
|
|
88
|
+
data = buf[8:]
|
|
89
|
+
meta = _meta(signature, slot, tx_index, block_time_us, grpc, recent_blockhash)
|
|
90
|
+
return apply_event_type_filter(
|
|
91
|
+
dispatch_program_data(disc, data, buf, meta, is_created_buy),
|
|
92
|
+
event_type_filter,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_log_unified(
|
|
97
|
+
log: str,
|
|
98
|
+
signature: str,
|
|
99
|
+
slot: int,
|
|
100
|
+
block_time_us: Optional[int] = None,
|
|
101
|
+
*,
|
|
102
|
+
tx_index: int = 0,
|
|
103
|
+
) -> Optional[DexEvent]:
|
|
104
|
+
grpc = int(time.time() * 1_000_000)
|
|
105
|
+
return parse_log_optimized(
|
|
106
|
+
log,
|
|
107
|
+
signature,
|
|
108
|
+
slot,
|
|
109
|
+
tx_index,
|
|
110
|
+
block_time_us,
|
|
111
|
+
grpc,
|
|
112
|
+
None,
|
|
113
|
+
False,
|
|
114
|
+
"",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def parse_transaction_events(
|
|
119
|
+
logs: List[str],
|
|
120
|
+
signature: str,
|
|
121
|
+
slot: int,
|
|
122
|
+
block_time_us: Optional[int] = None,
|
|
123
|
+
*,
|
|
124
|
+
subscribe_tx_info: Optional["SubscribeUpdateTransactionInfo"] = None,
|
|
125
|
+
tx_index: Optional[int] = None,
|
|
126
|
+
) -> List[DexEvent]:
|
|
127
|
+
"""对齐 Rust `parse_transaction_events` - 解析完整交易并返回所有 DEX 事件"""
|
|
128
|
+
return parse_logs_only(
|
|
129
|
+
logs,
|
|
130
|
+
signature,
|
|
131
|
+
slot,
|
|
132
|
+
block_time_us,
|
|
133
|
+
subscribe_tx_info=subscribe_tx_info,
|
|
134
|
+
tx_index=tx_index,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def parse_logs_only(
|
|
139
|
+
logs: List[str],
|
|
140
|
+
signature: str,
|
|
141
|
+
slot: int,
|
|
142
|
+
block_time_us: Optional[int] = None,
|
|
143
|
+
*,
|
|
144
|
+
subscribe_tx_info: Optional["SubscribeUpdateTransactionInfo"] = None,
|
|
145
|
+
tx_index: Optional[int] = None,
|
|
146
|
+
) -> List[DexEvent]:
|
|
147
|
+
"""解析日志中的 Program data 事件。
|
|
148
|
+
|
|
149
|
+
若传入 ``subscribe_tx_info`` 且含 ``transaction_raw`` / ``meta_raw``(Yellowstone 订阅),
|
|
150
|
+
会在解析后调用 :func:`grpc_instruction_parser.enrich_dex_events_with_subscribe_tx_info`
|
|
151
|
+
从指令账户补全 bonding_curve、creator_vault 等字段(与 Rust gRPC 路径一致)。
|
|
152
|
+
|
|
153
|
+
``tx_index`` 为区块内交易序号(与 gRPC ``SubscribeUpdateTransactionInfo.index`` 一致)。
|
|
154
|
+
未显式传入且提供了 ``subscribe_tx_info`` 时,使用 ``subscribe_tx_info.index``。
|
|
155
|
+
"""
|
|
156
|
+
resolved_tx_index = 0
|
|
157
|
+
if tx_index is not None:
|
|
158
|
+
resolved_tx_index = int(tx_index)
|
|
159
|
+
elif subscribe_tx_info is not None:
|
|
160
|
+
resolved_tx_index = int(getattr(subscribe_tx_info, "index", 0) or 0)
|
|
161
|
+
out: List[DexEvent] = []
|
|
162
|
+
for log in logs:
|
|
163
|
+
ev = parse_log_unified(log, signature, slot, block_time_us, tx_index=resolved_tx_index)
|
|
164
|
+
if ev:
|
|
165
|
+
out.append(ev)
|
|
166
|
+
enrich_pumpfun_same_tx_post_merge(out)
|
|
167
|
+
if subscribe_tx_info is not None:
|
|
168
|
+
from .grpc_instruction_parser import enrich_dex_events_with_subscribe_tx_info
|
|
169
|
+
|
|
170
|
+
enrich_dex_events_with_subscribe_tx_info(out, subscribe_tx_info)
|
|
171
|
+
return out
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def parse_transaction_events_streaming(
|
|
175
|
+
logs: List[str],
|
|
176
|
+
signature: str,
|
|
177
|
+
slot: int,
|
|
178
|
+
block_time_us: Optional[int],
|
|
179
|
+
callback: Callable[[DexEvent], None],
|
|
180
|
+
*,
|
|
181
|
+
tx_index: int = 0,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""对齐 Rust `parse_transaction_events_streaming`"""
|
|
184
|
+
parse_logs_streaming(logs, signature, slot, block_time_us, callback, tx_index=tx_index)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def parse_logs_streaming(
|
|
188
|
+
logs: List[str],
|
|
189
|
+
signature: str,
|
|
190
|
+
slot: int,
|
|
191
|
+
block_time_us: Optional[int],
|
|
192
|
+
callback: Callable[[DexEvent], None],
|
|
193
|
+
*,
|
|
194
|
+
tx_index: int = 0,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""对齐 Rust `parse_logs_streaming` - 流式解析,每解析出一个事件立即回调"""
|
|
197
|
+
for log in logs:
|
|
198
|
+
ev = parse_log_unified(log, signature, slot, block_time_us, tx_index=tx_index)
|
|
199
|
+
if ev:
|
|
200
|
+
callback(ev)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class EventListener:
|
|
204
|
+
"""对齐 Rust `EventListener` trait"""
|
|
205
|
+
|
|
206
|
+
def on_dex_event(self, event: DexEvent) -> None:
|
|
207
|
+
raise NotImplementedError
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def parse_transaction_with_listener(
|
|
211
|
+
logs: List[str],
|
|
212
|
+
signature: str,
|
|
213
|
+
slot: int,
|
|
214
|
+
block_time_us: Optional[int],
|
|
215
|
+
listener: EventListener,
|
|
216
|
+
*,
|
|
217
|
+
subscribe_tx_info: Optional["SubscribeUpdateTransactionInfo"] = None,
|
|
218
|
+
tx_index: Optional[int] = None,
|
|
219
|
+
) -> None:
|
|
220
|
+
"""对齐 Rust `parse_transaction_with_listener`"""
|
|
221
|
+
events = parse_logs_only(
|
|
222
|
+
logs,
|
|
223
|
+
signature,
|
|
224
|
+
slot,
|
|
225
|
+
block_time_us,
|
|
226
|
+
subscribe_tx_info=subscribe_tx_info,
|
|
227
|
+
tx_index=tx_index,
|
|
228
|
+
)
|
|
229
|
+
for ev in events:
|
|
230
|
+
listener.on_dex_event(ev)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class StreamingEventListener:
|
|
234
|
+
"""对齐 Rust `StreamingEventListener` trait"""
|
|
235
|
+
|
|
236
|
+
def on_dex_event_streaming(self, event: DexEvent) -> None:
|
|
237
|
+
raise NotImplementedError
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def parse_transaction_with_streaming_listener(
|
|
241
|
+
logs: List[str],
|
|
242
|
+
signature: str,
|
|
243
|
+
slot: int,
|
|
244
|
+
block_time_us: Optional[int],
|
|
245
|
+
listener: StreamingEventListener,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""对齐 Rust `parse_transaction_with_streaming_listener`"""
|
|
248
|
+
|
|
249
|
+
def callback(ev: DexEvent) -> None:
|
|
250
|
+
listener.on_dex_event_streaming(ev)
|
|
251
|
+
|
|
252
|
+
parse_logs_streaming(logs, signature, slot, block_time_us, callback)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def parse_log(
|
|
256
|
+
log: str,
|
|
257
|
+
signature: str,
|
|
258
|
+
slot: int,
|
|
259
|
+
tx_index: int,
|
|
260
|
+
block_time_us: Optional[int],
|
|
261
|
+
grpc_recv_us: int,
|
|
262
|
+
is_created_buy: bool,
|
|
263
|
+
recent_blockhash: str = "",
|
|
264
|
+
) -> Optional[DexEvent]:
|
|
265
|
+
"""对齐 Rust `parse_log` - 带完整 gRPC 元数据字段的日志解析"""
|
|
266
|
+
return parse_log_optimized(
|
|
267
|
+
log,
|
|
268
|
+
signature,
|
|
269
|
+
slot,
|
|
270
|
+
tx_index,
|
|
271
|
+
block_time_us,
|
|
272
|
+
grpc_recv_us,
|
|
273
|
+
None,
|
|
274
|
+
is_created_buy,
|
|
275
|
+
recent_blockhash,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def warmup_parser() -> None:
|
|
280
|
+
decode_program_data_line("Program data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
__all__ = [
|
|
284
|
+
"DexEvent",
|
|
285
|
+
"decode_program_data_line",
|
|
286
|
+
"dispatch_program_data",
|
|
287
|
+
"parse_log",
|
|
288
|
+
"parse_log_unified",
|
|
289
|
+
"parse_log_optimized",
|
|
290
|
+
"parse_logs_only",
|
|
291
|
+
"parse_logs_streaming",
|
|
292
|
+
"parse_transaction_events",
|
|
293
|
+
"parse_transaction_events_streaming",
|
|
294
|
+
"parse_transaction_with_listener",
|
|
295
|
+
"parse_transaction_with_streaming_listener",
|
|
296
|
+
"EventListener",
|
|
297
|
+
"StreamingEventListener",
|
|
298
|
+
"parse_trade_from_data",
|
|
299
|
+
"warmup_parser",
|
|
300
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""同笔交易 PumpFun 后处理(对齐 Rust ``pumpfun_fee_enrich``)。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from .dex_parsers import Z
|
|
8
|
+
from .event_types import DexEvent, PumpFunCreateEvent, PumpFunCreateV2TokenEvent, PumpFunTradeEvent
|
|
9
|
+
from .grpc_types import EventType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _buy_like_mint_fee(ev: DexEvent) -> Optional[Tuple[str, str]]:
|
|
13
|
+
if not isinstance(ev.data, PumpFunTradeEvent):
|
|
14
|
+
return None
|
|
15
|
+
t = ev.data
|
|
16
|
+
if t.mint == Z or not t.mint:
|
|
17
|
+
return None
|
|
18
|
+
if ev.type == EventType.PUMP_FUN_TRADE:
|
|
19
|
+
if t.is_buy:
|
|
20
|
+
return (t.mint, t.fee_recipient)
|
|
21
|
+
return None
|
|
22
|
+
if ev.type in (EventType.PUMP_FUN_BUY, EventType.PUMP_FUN_BUY_EXACT_SOL_IN):
|
|
23
|
+
return (t.mint, t.fee_recipient)
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def enrich_create_v2_observed_fee_recipient(events: List[DexEvent]) -> None:
|
|
28
|
+
mint_to_fee: Dict[str, str] = {}
|
|
29
|
+
for e in events:
|
|
30
|
+
p = _buy_like_mint_fee(e)
|
|
31
|
+
if not p:
|
|
32
|
+
continue
|
|
33
|
+
mint, fee = p
|
|
34
|
+
if fee and fee != Z:
|
|
35
|
+
mint_to_fee.setdefault(mint, fee)
|
|
36
|
+
if not mint_to_fee:
|
|
37
|
+
return
|
|
38
|
+
for e in events:
|
|
39
|
+
if e.type != EventType.PUMP_FUN_CREATE_V2:
|
|
40
|
+
continue
|
|
41
|
+
if not isinstance(e.data, PumpFunCreateV2TokenEvent):
|
|
42
|
+
continue
|
|
43
|
+
c = e.data
|
|
44
|
+
if not c.observed_fee_recipient and c.mint in mint_to_fee:
|
|
45
|
+
c.observed_fee_recipient = mint_to_fee[c.mint]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def enrich_pumpfun_trades_from_create_instructions(events: List[DexEvent]) -> None:
|
|
49
|
+
flags: Dict[str, Tuple[bool, bool]] = {}
|
|
50
|
+
for e in events:
|
|
51
|
+
if e.type not in (EventType.PUMP_FUN_CREATE, EventType.PUMP_FUN_CREATE_V2):
|
|
52
|
+
continue
|
|
53
|
+
if not isinstance(e.data, (PumpFunCreateEvent, PumpFunCreateV2TokenEvent)):
|
|
54
|
+
continue
|
|
55
|
+
c = e.data
|
|
56
|
+
if c.mint and c.mint != Z:
|
|
57
|
+
flags.setdefault(c.mint, (c.is_cashback_enabled, c.is_mayhem_mode))
|
|
58
|
+
if not flags:
|
|
59
|
+
return
|
|
60
|
+
for e in events:
|
|
61
|
+
if not isinstance(e.data, PumpFunTradeEvent):
|
|
62
|
+
continue
|
|
63
|
+
t = e.data
|
|
64
|
+
if not t.mint or t.mint == Z or t.mint not in flags:
|
|
65
|
+
continue
|
|
66
|
+
cashback_enabled, mayhem_mode = flags[t.mint]
|
|
67
|
+
t.is_cashback_coin = t.is_cashback_coin or cashback_enabled
|
|
68
|
+
t.mayhem_mode = t.mayhem_mode or mayhem_mode
|
|
69
|
+
if cashback_enabled:
|
|
70
|
+
t.track_volume = True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def enrich_pumpfun_same_tx_post_merge(events: List[DexEvent]) -> None:
|
|
74
|
+
enrich_create_v2_observed_fee_recipient(events)
|
|
75
|
+
enrich_pumpfun_trades_from_create_instructions(events)
|