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,870 @@
|
|
|
1
|
+
"""Yellowstone gRPC 客户端实现
|
|
2
|
+
|
|
3
|
+
参考实现:
|
|
4
|
+
- https://github.com/chainstacklabs/grpc-geyser-tutorial
|
|
5
|
+
- https://github.com/rpcpool/yellowstone-grpc
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import Dict, List, Optional, Callable, Any, AsyncGenerator, Tuple
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
import base58
|
|
18
|
+
import grpc
|
|
19
|
+
from grpc import aio
|
|
20
|
+
|
|
21
|
+
from .grpc_types import (
|
|
22
|
+
ClientConfig,
|
|
23
|
+
TransactionFilter,
|
|
24
|
+
SubscribeCallbacks,
|
|
25
|
+
SubscribeUpdate,
|
|
26
|
+
SubscribeUpdateAccount,
|
|
27
|
+
SubscribeUpdateAccountInfo,
|
|
28
|
+
SubscribeUpdateSlot,
|
|
29
|
+
SubscribeUpdateTransaction,
|
|
30
|
+
SubscribeUpdateTransactionInfo,
|
|
31
|
+
SubscribeUpdateBlock,
|
|
32
|
+
SubscribeUpdateBlockMeta,
|
|
33
|
+
SubscribeUpdatePing,
|
|
34
|
+
SubscribeUpdatePong,
|
|
35
|
+
CommitmentLevel,
|
|
36
|
+
SlotStatus,
|
|
37
|
+
GetLatestBlockhashRequest,
|
|
38
|
+
GetLatestBlockhashResponse,
|
|
39
|
+
GetBlockHeightRequest,
|
|
40
|
+
GetBlockHeightResponse,
|
|
41
|
+
GetSlotRequest,
|
|
42
|
+
GetSlotResponse,
|
|
43
|
+
GetVersionRequest,
|
|
44
|
+
GetVersionResponse,
|
|
45
|
+
IsBlockhashValidRequest,
|
|
46
|
+
IsBlockhashValidResponse,
|
|
47
|
+
PingRequest,
|
|
48
|
+
PongResponse,
|
|
49
|
+
SubscribeReplayInfoRequest,
|
|
50
|
+
SubscribeReplayInfoResponse,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# 尝试导入生成的 protobuf 代码
|
|
54
|
+
try:
|
|
55
|
+
from . import geyser_pb2
|
|
56
|
+
from . import geyser_pb2_grpc
|
|
57
|
+
HAS_PROTO = True
|
|
58
|
+
except ImportError:
|
|
59
|
+
HAS_PROTO = False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def normalize_grpc_endpoint(endpoint: str, default_tls_from_config: bool) -> Tuple[str, bool]:
|
|
63
|
+
"""将 ``https://host:443`` / ``http://host`` 转为 gRPC 可用的 ``host:port``,并决定是否走 TLS。
|
|
64
|
+
|
|
65
|
+
``grpc.aio.secure_channel`` / ``insecure_channel`` 的 *target* 不能包含 scheme,否则会出现
|
|
66
|
+
DNS 报错 ``Misformatted domain name``。
|
|
67
|
+
"""
|
|
68
|
+
s = (endpoint or "").strip()
|
|
69
|
+
if not s:
|
|
70
|
+
return s, default_tls_from_config
|
|
71
|
+
if "://" not in s:
|
|
72
|
+
return s, default_tls_from_config
|
|
73
|
+
parsed = urlparse(s)
|
|
74
|
+
host = parsed.hostname or ""
|
|
75
|
+
if not host:
|
|
76
|
+
return s, default_tls_from_config
|
|
77
|
+
port = parsed.port
|
|
78
|
+
if port is not None:
|
|
79
|
+
target = f"{host}:{port}"
|
|
80
|
+
else:
|
|
81
|
+
if parsed.scheme == "https":
|
|
82
|
+
target = f"{host}:443"
|
|
83
|
+
elif parsed.scheme == "http":
|
|
84
|
+
target = f"{host}:80"
|
|
85
|
+
else:
|
|
86
|
+
target = host
|
|
87
|
+
use_tls = parsed.scheme == "https"
|
|
88
|
+
return target, use_tls
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Subscription:
|
|
93
|
+
"""订阅句柄"""
|
|
94
|
+
id: str
|
|
95
|
+
filter: TransactionFilter
|
|
96
|
+
cancel: Callable[[], None]
|
|
97
|
+
callbacks: SubscribeCallbacks
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class YellowstoneGrpc:
|
|
101
|
+
"""Yellowstone gRPC 客户端"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, endpoint: str, config: Optional[ClientConfig] = None):
|
|
104
|
+
self.endpoint = endpoint
|
|
105
|
+
self.config = config or ClientConfig.default()
|
|
106
|
+
self._x_token: Optional[str] = None
|
|
107
|
+
self._connected = False
|
|
108
|
+
self._subscribers: Dict[str, Subscription] = {}
|
|
109
|
+
self._channel: Optional[aio.Channel] = None
|
|
110
|
+
self._client: Optional[Any] = None
|
|
111
|
+
self._lock = asyncio.Lock()
|
|
112
|
+
self._dex_event_queue: Optional[asyncio.Queue] = None
|
|
113
|
+
self._dex_event_filter: Optional[Any] = None
|
|
114
|
+
self._dex_cancel_event: Optional[asyncio.Event] = None
|
|
115
|
+
self._dex_task: Optional[asyncio.Task] = None
|
|
116
|
+
self._dex_request_queue: Optional[asyncio.Queue] = None
|
|
117
|
+
self._dex_current_req: Optional[Any] = None
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def new(cls, endpoint: str, token: Optional[str] = None) -> YellowstoneGrpc:
|
|
121
|
+
"""对齐 Rust ``YellowstoneGrpc::new``。"""
|
|
122
|
+
from .parser import warmup_parser
|
|
123
|
+
|
|
124
|
+
warmup_parser()
|
|
125
|
+
inst = cls(endpoint)
|
|
126
|
+
if token:
|
|
127
|
+
inst.set_x_token(token)
|
|
128
|
+
return inst
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def new_with_config(
|
|
132
|
+
cls, endpoint: str, token: Optional[str], config: ClientConfig
|
|
133
|
+
) -> YellowstoneGrpc:
|
|
134
|
+
"""对齐 Rust ``YellowstoneGrpc::new_with_config``。"""
|
|
135
|
+
from .parser import warmup_parser
|
|
136
|
+
|
|
137
|
+
warmup_parser()
|
|
138
|
+
inst = cls(endpoint, config)
|
|
139
|
+
if token:
|
|
140
|
+
inst.set_x_token(token)
|
|
141
|
+
return inst
|
|
142
|
+
|
|
143
|
+
async def subscribe_dex_events(
|
|
144
|
+
self,
|
|
145
|
+
transaction_filters: List[TransactionFilter],
|
|
146
|
+
account_filters: List[Any],
|
|
147
|
+
event_type_filter: Optional[Any] = None,
|
|
148
|
+
) -> asyncio.Queue:
|
|
149
|
+
"""订阅 DEX 事件并返回低延迟 ``asyncio.Queue``。
|
|
150
|
+
|
|
151
|
+
与 Rust ``YellowstoneGrpc::subscribe_dex_events`` 语义对齐:方法负责启动后台流,
|
|
152
|
+
将解析后的 ``DexEvent`` 推入队列;调用方从返回队列消费事件。
|
|
153
|
+
"""
|
|
154
|
+
if not self._connected:
|
|
155
|
+
await self.connect()
|
|
156
|
+
if not self._client:
|
|
157
|
+
raise RuntimeError("Client not connected")
|
|
158
|
+
|
|
159
|
+
from .grpc.subscribe_builder import build_subscribe_request
|
|
160
|
+
|
|
161
|
+
req = build_subscribe_request(transaction_filters, account_filters)
|
|
162
|
+
queue: asyncio.Queue = asyncio.Queue(maxsize=max(1, int(self.config.buffer_size or 100_000)))
|
|
163
|
+
|
|
164
|
+
if self._dex_cancel_event is not None:
|
|
165
|
+
self._dex_cancel_event.set()
|
|
166
|
+
if self._dex_task is not None and not self._dex_task.done():
|
|
167
|
+
self._dex_task.cancel()
|
|
168
|
+
|
|
169
|
+
cancel_event = asyncio.Event()
|
|
170
|
+
request_queue: asyncio.Queue = asyncio.Queue(maxsize=100)
|
|
171
|
+
self._dex_event_queue = queue
|
|
172
|
+
self._dex_event_filter = event_type_filter
|
|
173
|
+
self._dex_cancel_event = cancel_event
|
|
174
|
+
self._dex_request_queue = request_queue
|
|
175
|
+
self._dex_current_req = req
|
|
176
|
+
self._dex_task = asyncio.create_task(
|
|
177
|
+
self._handle_dex_stream(req, queue, cancel_event, event_type_filter, request_queue)
|
|
178
|
+
)
|
|
179
|
+
return queue
|
|
180
|
+
|
|
181
|
+
async def update_subscription(
|
|
182
|
+
self,
|
|
183
|
+
transaction_filters: List[TransactionFilter],
|
|
184
|
+
account_filters: List[Any],
|
|
185
|
+
) -> None:
|
|
186
|
+
"""动态更新 DEX 订阅。
|
|
187
|
+
|
|
188
|
+
Python gRPC runtime 不暴露当前双向流的发送端给外部 API;这里保留原队列,
|
|
189
|
+
取消旧后台流并用新过滤器立即重建,调用方无需替换消费队列。
|
|
190
|
+
"""
|
|
191
|
+
if self._dex_event_queue is None:
|
|
192
|
+
raise RuntimeError("No active DEX subscription")
|
|
193
|
+
if not self._connected:
|
|
194
|
+
await self.connect()
|
|
195
|
+
if not self._client:
|
|
196
|
+
raise RuntimeError("Client not connected")
|
|
197
|
+
|
|
198
|
+
from .grpc.subscribe_builder import build_subscribe_request
|
|
199
|
+
|
|
200
|
+
req = build_subscribe_request(transaction_filters, account_filters)
|
|
201
|
+
self._dex_current_req = req
|
|
202
|
+
if self._dex_request_queue is None:
|
|
203
|
+
raise RuntimeError("No active DEX subscription")
|
|
204
|
+
await self._dex_request_queue.put(req)
|
|
205
|
+
|
|
206
|
+
def set_x_token(self, token: str) -> None:
|
|
207
|
+
"""设置 X-Token 认证"""
|
|
208
|
+
self._x_token = token
|
|
209
|
+
|
|
210
|
+
def _get_channel_options(self) -> list:
|
|
211
|
+
"""获取 gRPC 通道选项
|
|
212
|
+
|
|
213
|
+
参考: https://github.com/chainstacklabs/grpc-geyser-tutorial
|
|
214
|
+
"""
|
|
215
|
+
return [
|
|
216
|
+
('grpc.keepalive_time_ms', self.config.keep_alive_interval_ms),
|
|
217
|
+
('grpc.keepalive_timeout_ms', self.config.keep_alive_timeout_ms),
|
|
218
|
+
('grpc.keepalive_permit_without_calls', True),
|
|
219
|
+
('grpc.http2.min_time_between_pings_ms', 10000),
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
def _create_auth_credentials(self):
|
|
223
|
+
"""创建认证凭证
|
|
224
|
+
|
|
225
|
+
参考: https://github.com/chainstacklabs/grpc-geyser-tutorial/main.py
|
|
226
|
+
"""
|
|
227
|
+
if not self._x_token:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
def auth_callback(context, callback):
|
|
231
|
+
callback((('x-token', self._x_token),), None)
|
|
232
|
+
|
|
233
|
+
return grpc.metadata_call_credentials(auth_callback)
|
|
234
|
+
|
|
235
|
+
def _get_metadata(self) -> Optional[list]:
|
|
236
|
+
"""获取认证元数据(用于流式调用)"""
|
|
237
|
+
if self._x_token:
|
|
238
|
+
return [('x-token', self._x_token)]
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
async def connect(self) -> None:
|
|
242
|
+
"""连接到 gRPC 服务器
|
|
243
|
+
|
|
244
|
+
参考实现:
|
|
245
|
+
- https://github.com/chainstacklabs/grpc-geyser-tutorial/main.py
|
|
246
|
+
- https://github.com/rpcpool/yellowstone-grpc/examples/python
|
|
247
|
+
"""
|
|
248
|
+
if self._connected:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
if not HAS_PROTO:
|
|
252
|
+
raise ImportError(
|
|
253
|
+
"YellowstoneGrpc.connect: 需要 protobuf 生成的代码。\n"
|
|
254
|
+
"请执行以下步骤:\n"
|
|
255
|
+
"1. 克隆 https://github.com/rpcpool/yellowstone-grpc\n"
|
|
256
|
+
"2. 使用 protoc 生成 Python 代码:\n"
|
|
257
|
+
" python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. geyser.proto\n"
|
|
258
|
+
"3. 将生成的 geyser_pb2.py 和 geyser_pb2_grpc.py 放入 sol_parser 目录"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
async with self._lock:
|
|
262
|
+
if self._connected:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
channel_options = self._get_channel_options()
|
|
266
|
+
|
|
267
|
+
target, use_tls = normalize_grpc_endpoint(
|
|
268
|
+
self.endpoint, self.config.enable_tls
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if use_tls:
|
|
272
|
+
# 创建 SSL 凭证
|
|
273
|
+
ssl_creds = grpc.ssl_channel_credentials()
|
|
274
|
+
|
|
275
|
+
# 添加认证
|
|
276
|
+
auth_creds = self._create_auth_credentials()
|
|
277
|
+
if auth_creds:
|
|
278
|
+
composite_creds = grpc.composite_channel_credentials(ssl_creds, auth_creds)
|
|
279
|
+
else:
|
|
280
|
+
composite_creds = ssl_creds
|
|
281
|
+
|
|
282
|
+
self._channel = aio.secure_channel(
|
|
283
|
+
target,
|
|
284
|
+
composite_creds,
|
|
285
|
+
options=channel_options
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
self._channel = aio.insecure_channel(
|
|
289
|
+
target,
|
|
290
|
+
options=channel_options
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
self._client = geyser_pb2_grpc.GeyserStub(self._channel)
|
|
294
|
+
self._connected = True
|
|
295
|
+
|
|
296
|
+
async def disconnect(self) -> None:
|
|
297
|
+
"""断开连接"""
|
|
298
|
+
if not self._connected:
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
async with self._lock:
|
|
302
|
+
# 取消所有订阅
|
|
303
|
+
for sub in list(self._subscribers.values()):
|
|
304
|
+
sub.cancel()
|
|
305
|
+
self._subscribers.clear()
|
|
306
|
+
if self._dex_cancel_event is not None:
|
|
307
|
+
self._dex_cancel_event.set()
|
|
308
|
+
if self._dex_task is not None and not self._dex_task.done():
|
|
309
|
+
self._dex_task.cancel()
|
|
310
|
+
self._dex_cancel_event = None
|
|
311
|
+
self._dex_task = None
|
|
312
|
+
self._dex_event_queue = None
|
|
313
|
+
self._dex_request_queue = None
|
|
314
|
+
self._dex_current_req = None
|
|
315
|
+
|
|
316
|
+
# 关闭通道
|
|
317
|
+
if self._channel:
|
|
318
|
+
await self._channel.close()
|
|
319
|
+
self._channel = None
|
|
320
|
+
|
|
321
|
+
self._client = None
|
|
322
|
+
self._connected = False
|
|
323
|
+
|
|
324
|
+
def _build_subscribe_request(self, filter: TransactionFilter) -> Any:
|
|
325
|
+
"""构建订阅请求"""
|
|
326
|
+
tx_filter = geyser_pb2.SubscribeRequestFilterTransactions(
|
|
327
|
+
account_include=filter.account_include,
|
|
328
|
+
account_exclude=filter.account_exclude,
|
|
329
|
+
account_required=filter.account_required,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if filter.vote is not None:
|
|
333
|
+
tx_filter.vote = filter.vote
|
|
334
|
+
if filter.failed is not None:
|
|
335
|
+
tx_filter.failed = filter.failed
|
|
336
|
+
if filter.signature:
|
|
337
|
+
tx_filter.signature = filter.signature
|
|
338
|
+
|
|
339
|
+
return geyser_pb2.SubscribeRequest(
|
|
340
|
+
transactions={"client": tx_filter}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def _convert_update(self, pb_update: Any) -> SubscribeUpdate:
|
|
344
|
+
"""转换 protobuf 更新到本地类型"""
|
|
345
|
+
update = SubscribeUpdate(filters=list(pb_update.filters))
|
|
346
|
+
if hasattr(pb_update, "created_at") and pb_update.HasField("created_at"):
|
|
347
|
+
ts = pb_update.created_at
|
|
348
|
+
update.created_at = int(ts.seconds) * 1_000_000 + int(ts.nanos) // 1_000
|
|
349
|
+
|
|
350
|
+
# 转换账户更新
|
|
351
|
+
if pb_update.HasField('account'):
|
|
352
|
+
acc = pb_update.account
|
|
353
|
+
update.account = SubscribeUpdateAccount(
|
|
354
|
+
slot=acc.slot,
|
|
355
|
+
is_startup=acc.is_startup
|
|
356
|
+
)
|
|
357
|
+
if acc.account:
|
|
358
|
+
update.account.account = SubscribeUpdateAccountInfo(
|
|
359
|
+
pubkey=bytes(acc.account.pubkey),
|
|
360
|
+
lamports=acc.account.lamports,
|
|
361
|
+
owner=bytes(acc.account.owner),
|
|
362
|
+
executable=acc.account.executable,
|
|
363
|
+
rent_epoch=acc.account.rent_epoch,
|
|
364
|
+
data=bytes(acc.account.data),
|
|
365
|
+
write_version=acc.account.write_version,
|
|
366
|
+
txn_signature=bytes(acc.account.txn_signature) if acc.account.txn_signature else None
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# 转换 slot 更新
|
|
370
|
+
if pb_update.HasField('slot'):
|
|
371
|
+
slot = pb_update.slot
|
|
372
|
+
update.slot = SubscribeUpdateSlot(
|
|
373
|
+
slot=slot.slot,
|
|
374
|
+
status=SlotStatus(slot.status)
|
|
375
|
+
)
|
|
376
|
+
if slot.HasField('parent'):
|
|
377
|
+
update.slot.parent = slot.parent
|
|
378
|
+
if slot.HasField('dead_error'):
|
|
379
|
+
update.slot.dead_error = slot.dead_error
|
|
380
|
+
|
|
381
|
+
# 转换交易更新
|
|
382
|
+
if pb_update.HasField('transaction'):
|
|
383
|
+
tx = pb_update.transaction
|
|
384
|
+
update.transaction = SubscribeUpdateTransaction(slot=tx.slot)
|
|
385
|
+
if tx.transaction:
|
|
386
|
+
# 直接从 proto 对象提取 log_messages,避免反序列化
|
|
387
|
+
log_msgs = list(tx.transaction.meta.log_messages) if tx.transaction.meta else []
|
|
388
|
+
update.transaction.transaction = SubscribeUpdateTransactionInfo(
|
|
389
|
+
signature=bytes(tx.transaction.signature),
|
|
390
|
+
is_vote=tx.transaction.is_vote,
|
|
391
|
+
transaction_raw=tx.transaction.transaction.SerializeToString() if tx.transaction.transaction else b"",
|
|
392
|
+
meta_raw=tx.transaction.meta.SerializeToString() if tx.transaction.meta else b"",
|
|
393
|
+
index=tx.transaction.index,
|
|
394
|
+
log_messages=log_msgs,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# 转换区块更新
|
|
398
|
+
if pb_update.HasField('block'):
|
|
399
|
+
block = pb_update.block
|
|
400
|
+
update.block = SubscribeUpdateBlock(
|
|
401
|
+
slot=block.slot,
|
|
402
|
+
blockhash=block.blockhash,
|
|
403
|
+
parent_slot=block.parent_slot,
|
|
404
|
+
parent_blockhash=block.parent_blockhash,
|
|
405
|
+
executed_transaction_count=block.executed_transaction_count
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# 转换区块元数据更新
|
|
409
|
+
if pb_update.HasField('block_meta'):
|
|
410
|
+
meta = pb_update.block_meta
|
|
411
|
+
update.block_meta = SubscribeUpdateBlockMeta(
|
|
412
|
+
slot=meta.slot,
|
|
413
|
+
blockhash=meta.blockhash,
|
|
414
|
+
parent_slot=meta.parent_slot,
|
|
415
|
+
parent_blockhash=meta.parent_blockhash,
|
|
416
|
+
executed_transaction_count=meta.executed_transaction_count
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# 转换 Ping
|
|
420
|
+
if pb_update.HasField('ping'):
|
|
421
|
+
update.ping = SubscribeUpdatePing()
|
|
422
|
+
|
|
423
|
+
# 转换 Pong
|
|
424
|
+
if pb_update.HasField('pong'):
|
|
425
|
+
update.pong = SubscribeUpdatePong(id=pb_update.pong.id)
|
|
426
|
+
|
|
427
|
+
return update
|
|
428
|
+
|
|
429
|
+
@staticmethod
|
|
430
|
+
def _queue_event_nowait(queue: asyncio.Queue, event: Any) -> None:
|
|
431
|
+
try:
|
|
432
|
+
queue.put_nowait(event)
|
|
433
|
+
except asyncio.QueueFull:
|
|
434
|
+
# 与 Rust 有界 ArrayQueue 的低延迟取舍一致:消费端落后时丢弃新事件,避免阻塞 gRPC 流。
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
async def _enqueue_transaction_dex_events(
|
|
438
|
+
self,
|
|
439
|
+
queue: asyncio.Queue,
|
|
440
|
+
info: SubscribeUpdateTransactionInfo,
|
|
441
|
+
slot: int,
|
|
442
|
+
event_type_filter: Optional[Any],
|
|
443
|
+
grpc_recv_us: int,
|
|
444
|
+
block_time_us: Optional[int],
|
|
445
|
+
order_dispatcher: Optional[Any] = None,
|
|
446
|
+
) -> None:
|
|
447
|
+
if not info:
|
|
448
|
+
return
|
|
449
|
+
signature = base58.b58encode(bytes(info.signature)).decode("ascii") if info.signature else ""
|
|
450
|
+
|
|
451
|
+
from .grpc_instruction_parser import (
|
|
452
|
+
enrich_dex_events_with_subscribe_tx_info,
|
|
453
|
+
parse_instructions_enhanced_from_subscribe_tx_info,
|
|
454
|
+
)
|
|
455
|
+
from .parser import parse_log_optimized
|
|
456
|
+
from .log_instr_dedup import dedupe_log_instruction_events
|
|
457
|
+
from .pumpfun_fee_enrich import enrich_pumpfun_same_tx_post_merge
|
|
458
|
+
from .grpc_types import EventType
|
|
459
|
+
|
|
460
|
+
instruction_events = parse_instructions_enhanced_from_subscribe_tx_info(
|
|
461
|
+
info,
|
|
462
|
+
slot,
|
|
463
|
+
event_type_filter,
|
|
464
|
+
block_time_us,
|
|
465
|
+
grpc_recv_us,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
log_events = []
|
|
469
|
+
is_created_buy = False
|
|
470
|
+
for log in info.log_messages:
|
|
471
|
+
ev = parse_log_optimized(
|
|
472
|
+
log,
|
|
473
|
+
signature,
|
|
474
|
+
slot,
|
|
475
|
+
int(info.index),
|
|
476
|
+
block_time_us,
|
|
477
|
+
grpc_recv_us,
|
|
478
|
+
event_type_filter,
|
|
479
|
+
is_created_buy,
|
|
480
|
+
"",
|
|
481
|
+
)
|
|
482
|
+
if ev is None:
|
|
483
|
+
continue
|
|
484
|
+
if ev.type in (EventType.PUMP_FUN_CREATE, EventType.PUMP_FUN_CREATE_V2):
|
|
485
|
+
is_created_buy = True
|
|
486
|
+
log_events.append(ev)
|
|
487
|
+
|
|
488
|
+
enrich_dex_events_with_subscribe_tx_info(instruction_events, info)
|
|
489
|
+
enrich_dex_events_with_subscribe_tx_info(log_events, info)
|
|
490
|
+
events = dedupe_log_instruction_events(log_events, instruction_events)
|
|
491
|
+
enrich_pumpfun_same_tx_post_merge(events)
|
|
492
|
+
if order_dispatcher is not None:
|
|
493
|
+
order_dispatcher.push_transaction_events(
|
|
494
|
+
events,
|
|
495
|
+
slot,
|
|
496
|
+
int(info.index),
|
|
497
|
+
lambda ev: self._queue_event_nowait(queue, ev),
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
for ev in events:
|
|
501
|
+
self._queue_event_nowait(queue, ev)
|
|
502
|
+
|
|
503
|
+
async def _enqueue_account_dex_event(
|
|
504
|
+
self,
|
|
505
|
+
queue: asyncio.Queue,
|
|
506
|
+
update: SubscribeUpdate,
|
|
507
|
+
event_type_filter: Optional[Any],
|
|
508
|
+
grpc_recv_us: int,
|
|
509
|
+
block_time_us: Optional[int],
|
|
510
|
+
) -> None:
|
|
511
|
+
if update.account is None or update.account.account is None:
|
|
512
|
+
return
|
|
513
|
+
acc = update.account.account
|
|
514
|
+
|
|
515
|
+
from .accounts import AccountData, parse_account_unified
|
|
516
|
+
from .grpc_types import EventMetadata
|
|
517
|
+
|
|
518
|
+
signature = (
|
|
519
|
+
base58.b58encode(bytes(acc.txn_signature)).decode("ascii")
|
|
520
|
+
if acc.txn_signature
|
|
521
|
+
else ""
|
|
522
|
+
)
|
|
523
|
+
account = AccountData(
|
|
524
|
+
pubkey=base58.b58encode(bytes(acc.pubkey)).decode("ascii"),
|
|
525
|
+
executable=bool(acc.executable),
|
|
526
|
+
lamports=int(acc.lamports),
|
|
527
|
+
owner=base58.b58encode(bytes(acc.owner)).decode("ascii"),
|
|
528
|
+
rent_epoch=int(acc.rent_epoch),
|
|
529
|
+
data=bytes(acc.data),
|
|
530
|
+
)
|
|
531
|
+
metadata = EventMetadata(
|
|
532
|
+
signature=signature,
|
|
533
|
+
slot=int(update.account.slot),
|
|
534
|
+
tx_index=0,
|
|
535
|
+
block_time_us=0 if block_time_us is None else block_time_us,
|
|
536
|
+
grpc_recv_us=grpc_recv_us,
|
|
537
|
+
)
|
|
538
|
+
ev = parse_account_unified(account, metadata, event_type_filter)
|
|
539
|
+
if ev is not None:
|
|
540
|
+
self._queue_event_nowait(queue, ev)
|
|
541
|
+
|
|
542
|
+
async def _handle_dex_stream(
|
|
543
|
+
self,
|
|
544
|
+
req: Any,
|
|
545
|
+
queue: asyncio.Queue,
|
|
546
|
+
cancel_event: asyncio.Event,
|
|
547
|
+
event_type_filter: Optional[Any],
|
|
548
|
+
request_queue: asyncio.Queue,
|
|
549
|
+
) -> None:
|
|
550
|
+
delay = 1.0
|
|
551
|
+
from .order_buffer import OrderDispatcher
|
|
552
|
+
|
|
553
|
+
order_dispatcher = OrderDispatcher(self.config)
|
|
554
|
+
|
|
555
|
+
async def flush_loop() -> None:
|
|
556
|
+
while not cancel_event.is_set():
|
|
557
|
+
await asyncio.sleep(order_dispatcher.interval_s)
|
|
558
|
+
order_dispatcher.flush_due(lambda ev: self._queue_event_nowait(queue, ev))
|
|
559
|
+
|
|
560
|
+
flush_task: Optional[asyncio.Task] = None
|
|
561
|
+
if order_dispatcher.needs_timer:
|
|
562
|
+
flush_task = asyncio.create_task(flush_loop())
|
|
563
|
+
|
|
564
|
+
try:
|
|
565
|
+
while not cancel_event.is_set():
|
|
566
|
+
outgoing: asyncio.Queue = asyncio.Queue()
|
|
567
|
+
|
|
568
|
+
async def request_iterator():
|
|
569
|
+
yield self._dex_current_req or req
|
|
570
|
+
while True:
|
|
571
|
+
if cancel_event.is_set():
|
|
572
|
+
return
|
|
573
|
+
ping_req = await outgoing.get()
|
|
574
|
+
if ping_req is None:
|
|
575
|
+
return
|
|
576
|
+
yield ping_req
|
|
577
|
+
|
|
578
|
+
async def request_pump() -> None:
|
|
579
|
+
while not cancel_event.is_set():
|
|
580
|
+
next_req = await request_queue.get()
|
|
581
|
+
self._dex_current_req = next_req
|
|
582
|
+
await outgoing.put(next_req)
|
|
583
|
+
|
|
584
|
+
pump_task = asyncio.create_task(request_pump())
|
|
585
|
+
try:
|
|
586
|
+
metadata = self._get_metadata()
|
|
587
|
+
async for pb_update in self._client.Subscribe(request_iterator(), metadata=metadata):
|
|
588
|
+
if cancel_event.is_set():
|
|
589
|
+
break
|
|
590
|
+
if pb_update.HasField("ping"):
|
|
591
|
+
await outgoing.put(
|
|
592
|
+
geyser_pb2.SubscribeRequest(
|
|
593
|
+
ping=geyser_pb2.SubscribeRequestPing(id=1)
|
|
594
|
+
)
|
|
595
|
+
)
|
|
596
|
+
continue
|
|
597
|
+
|
|
598
|
+
grpc_recv_us = int(time.time() * 1_000_000)
|
|
599
|
+
update = self._convert_update(pb_update)
|
|
600
|
+
block_time_us = update.created_at
|
|
601
|
+
if update.transaction and update.transaction.transaction:
|
|
602
|
+
await self._enqueue_transaction_dex_events(
|
|
603
|
+
queue,
|
|
604
|
+
update.transaction.transaction,
|
|
605
|
+
int(update.transaction.slot),
|
|
606
|
+
event_type_filter,
|
|
607
|
+
grpc_recv_us,
|
|
608
|
+
block_time_us,
|
|
609
|
+
order_dispatcher,
|
|
610
|
+
)
|
|
611
|
+
if update.account:
|
|
612
|
+
await self._enqueue_account_dex_event(
|
|
613
|
+
queue,
|
|
614
|
+
update,
|
|
615
|
+
event_type_filter,
|
|
616
|
+
grpc_recv_us,
|
|
617
|
+
block_time_us,
|
|
618
|
+
)
|
|
619
|
+
delay = 1.0
|
|
620
|
+
except asyncio.CancelledError:
|
|
621
|
+
break
|
|
622
|
+
except Exception:
|
|
623
|
+
order_dispatcher.flush_all(lambda ev: self._queue_event_nowait(queue, ev))
|
|
624
|
+
if cancel_event.is_set():
|
|
625
|
+
break
|
|
626
|
+
await asyncio.sleep(delay)
|
|
627
|
+
delay = min(delay * 2.0, 60.0)
|
|
628
|
+
finally:
|
|
629
|
+
pump_task.cancel()
|
|
630
|
+
await outgoing.put(None)
|
|
631
|
+
finally:
|
|
632
|
+
if flush_task is not None:
|
|
633
|
+
flush_task.cancel()
|
|
634
|
+
order_dispatcher.flush_all(lambda ev: self._queue_event_nowait(queue, ev))
|
|
635
|
+
|
|
636
|
+
async def subscribe_transactions(
|
|
637
|
+
self,
|
|
638
|
+
filter: TransactionFilter,
|
|
639
|
+
callbacks: SubscribeCallbacks,
|
|
640
|
+
) -> Subscription:
|
|
641
|
+
"""订阅交易"""
|
|
642
|
+
if not self._connected or not self._client:
|
|
643
|
+
raise RuntimeError("Client not connected, call connect() first")
|
|
644
|
+
|
|
645
|
+
if not HAS_PROTO:
|
|
646
|
+
raise ImportError(
|
|
647
|
+
"YellowstoneGrpc.subscribe_transactions: 需要 protobuf 生成的代码。"
|
|
648
|
+
"请从 https://github.com/rpcpool/yellowstone-grpc 获取 proto 文件并生成 Python 代码。"
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
sub_id = str(uuid.uuid4())
|
|
652
|
+
|
|
653
|
+
# 创建取消事件
|
|
654
|
+
cancel_event = asyncio.Event()
|
|
655
|
+
|
|
656
|
+
def cancel():
|
|
657
|
+
cancel_event.set()
|
|
658
|
+
|
|
659
|
+
sub = Subscription(
|
|
660
|
+
id=sub_id,
|
|
661
|
+
filter=filter,
|
|
662
|
+
cancel=cancel,
|
|
663
|
+
callbacks=callbacks,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
self._subscribers[sub_id] = sub
|
|
667
|
+
|
|
668
|
+
# 构建订阅请求
|
|
669
|
+
req = self._build_subscribe_request(filter)
|
|
670
|
+
|
|
671
|
+
# 启动处理任务
|
|
672
|
+
asyncio.create_task(self._handle_stream(sub, req, cancel_event))
|
|
673
|
+
|
|
674
|
+
return sub
|
|
675
|
+
|
|
676
|
+
async def _handle_stream(
|
|
677
|
+
self, sub: Subscription, req: Any, cancel_event: asyncio.Event
|
|
678
|
+
) -> None:
|
|
679
|
+
"""处理流式响应。
|
|
680
|
+
|
|
681
|
+
Geyser 会周期性下发 ``SubscribeUpdate.ping``;必须在同一 Subscribe 双向流上回写
|
|
682
|
+
``SubscribeRequest.ping``(与 Rust / TypeScript / Go 一致),否则公共节点或 LB 可能断开。
|
|
683
|
+
"""
|
|
684
|
+
outgoing: asyncio.Queue = asyncio.Queue()
|
|
685
|
+
|
|
686
|
+
async def request_iterator():
|
|
687
|
+
yield req
|
|
688
|
+
while True:
|
|
689
|
+
if cancel_event.is_set():
|
|
690
|
+
return
|
|
691
|
+
ping_req = await outgoing.get()
|
|
692
|
+
if ping_req is None:
|
|
693
|
+
return
|
|
694
|
+
yield ping_req
|
|
695
|
+
|
|
696
|
+
try:
|
|
697
|
+
metadata = self._get_metadata()
|
|
698
|
+
async for update in self._client.Subscribe(request_iterator(), metadata=metadata):
|
|
699
|
+
if cancel_event.is_set():
|
|
700
|
+
break
|
|
701
|
+
if update.HasField("ping"):
|
|
702
|
+
await outgoing.put(
|
|
703
|
+
geyser_pb2.SubscribeRequest(
|
|
704
|
+
ping=geyser_pb2.SubscribeRequestPing(id=1)
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
continue
|
|
708
|
+
if sub.callbacks.on_update:
|
|
709
|
+
converted = self._convert_update(update)
|
|
710
|
+
sub.callbacks.on_update(converted)
|
|
711
|
+
except Exception as e:
|
|
712
|
+
if sub.callbacks.on_error:
|
|
713
|
+
sub.callbacks.on_error(e)
|
|
714
|
+
finally:
|
|
715
|
+
await outgoing.put(None)
|
|
716
|
+
self._subscribers.pop(sub.id, None)
|
|
717
|
+
if sub.callbacks.on_end:
|
|
718
|
+
sub.callbacks.on_end()
|
|
719
|
+
|
|
720
|
+
async def unsubscribe(self, sub_id: str) -> None:
|
|
721
|
+
"""取消订阅"""
|
|
722
|
+
sub = self._subscribers.pop(sub_id, None)
|
|
723
|
+
if sub is None:
|
|
724
|
+
raise ValueError(f"Subscription {sub_id} not found")
|
|
725
|
+
sub.cancel()
|
|
726
|
+
|
|
727
|
+
def is_connected(self) -> bool:
|
|
728
|
+
"""检查是否已连接"""
|
|
729
|
+
return self._connected
|
|
730
|
+
|
|
731
|
+
def get_config(self) -> ClientConfig:
|
|
732
|
+
"""获取客户端配置"""
|
|
733
|
+
return self.config
|
|
734
|
+
|
|
735
|
+
async def get_latest_blockhash(
|
|
736
|
+
self, commitment: Optional[CommitmentLevel] = None
|
|
737
|
+
) -> GetLatestBlockhashResponse:
|
|
738
|
+
"""获取最新区块哈希"""
|
|
739
|
+
if not self._connected or not self._client:
|
|
740
|
+
raise RuntimeError("Client not connected")
|
|
741
|
+
|
|
742
|
+
if not HAS_PROTO:
|
|
743
|
+
raise ImportError("需要 protobuf 生成的代码")
|
|
744
|
+
|
|
745
|
+
req = geyser_pb2.GetLatestBlockhashRequest()
|
|
746
|
+
if commitment is not None:
|
|
747
|
+
req.commitment = commitment.value
|
|
748
|
+
|
|
749
|
+
metadata = self._get_metadata()
|
|
750
|
+
resp = await self._client.get_latest_blockhash(req, metadata=metadata)
|
|
751
|
+
|
|
752
|
+
return GetLatestBlockhashResponse(
|
|
753
|
+
slot=resp.slot,
|
|
754
|
+
blockhash=resp.blockhash,
|
|
755
|
+
last_valid_block_height=resp.last_valid_block_height
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
async def get_block_height(
|
|
759
|
+
self, commitment: Optional[CommitmentLevel] = None
|
|
760
|
+
) -> GetBlockHeightResponse:
|
|
761
|
+
"""获取区块高度"""
|
|
762
|
+
if not self._connected or not self._client:
|
|
763
|
+
raise RuntimeError("Client not connected")
|
|
764
|
+
|
|
765
|
+
if not HAS_PROTO:
|
|
766
|
+
raise ImportError("需要 protobuf 生成的代码")
|
|
767
|
+
|
|
768
|
+
req = geyser_pb2.GetBlockHeightRequest()
|
|
769
|
+
if commitment is not None:
|
|
770
|
+
req.commitment = commitment.value
|
|
771
|
+
|
|
772
|
+
metadata = self._get_metadata()
|
|
773
|
+
resp = await self._client.get_block_height(req, metadata=metadata)
|
|
774
|
+
|
|
775
|
+
return GetBlockHeightResponse(block_height=resp.block_height)
|
|
776
|
+
|
|
777
|
+
async def get_slot(
|
|
778
|
+
self, commitment: Optional[CommitmentLevel] = None
|
|
779
|
+
) -> GetSlotResponse:
|
|
780
|
+
"""获取当前 Slot"""
|
|
781
|
+
if not self._connected or not self._client:
|
|
782
|
+
raise RuntimeError("Client not connected")
|
|
783
|
+
|
|
784
|
+
if not HAS_PROTO:
|
|
785
|
+
raise ImportError("需要 protobuf 生成的代码")
|
|
786
|
+
|
|
787
|
+
req = geyser_pb2.GetSlotRequest()
|
|
788
|
+
if commitment is not None:
|
|
789
|
+
req.commitment = commitment.value
|
|
790
|
+
|
|
791
|
+
metadata = self._get_metadata()
|
|
792
|
+
resp = await self._client.get_slot(req, metadata=metadata)
|
|
793
|
+
|
|
794
|
+
return GetSlotResponse(slot=resp.slot)
|
|
795
|
+
|
|
796
|
+
async def get_version(self) -> GetVersionResponse:
|
|
797
|
+
"""获取服务器版本"""
|
|
798
|
+
if not self._connected or not self._client:
|
|
799
|
+
raise RuntimeError("Client not connected")
|
|
800
|
+
|
|
801
|
+
if not HAS_PROTO:
|
|
802
|
+
raise ImportError("需要 protobuf 生成的代码")
|
|
803
|
+
|
|
804
|
+
req = geyser_pb2.GetVersionRequest()
|
|
805
|
+
metadata = self._get_metadata()
|
|
806
|
+
resp = await self._client.get_version(req, metadata=metadata)
|
|
807
|
+
|
|
808
|
+
return GetVersionResponse(version=resp.version)
|
|
809
|
+
|
|
810
|
+
async def is_blockhash_valid(
|
|
811
|
+
self, blockhash: str, commitment: Optional[CommitmentLevel] = None
|
|
812
|
+
) -> IsBlockhashValidResponse:
|
|
813
|
+
"""验证区块哈希是否有效"""
|
|
814
|
+
if not self._connected or not self._client:
|
|
815
|
+
raise RuntimeError("Client not connected")
|
|
816
|
+
|
|
817
|
+
if not HAS_PROTO:
|
|
818
|
+
raise ImportError("需要 protobuf 生成的代码")
|
|
819
|
+
|
|
820
|
+
req = geyser_pb2.IsBlockhashValidRequest(blockhash=blockhash)
|
|
821
|
+
if commitment is not None:
|
|
822
|
+
req.commitment = commitment.value
|
|
823
|
+
|
|
824
|
+
metadata = self._get_metadata()
|
|
825
|
+
resp = await self._client.is_blockhash_valid(req, metadata=metadata)
|
|
826
|
+
|
|
827
|
+
return IsBlockhashValidResponse(slot=resp.slot, valid=resp.valid)
|
|
828
|
+
|
|
829
|
+
async def ping(self, count: int) -> PongResponse:
|
|
830
|
+
"""发送 Ping 请求"""
|
|
831
|
+
if not self._connected or not self._client:
|
|
832
|
+
raise RuntimeError("Client not connected")
|
|
833
|
+
|
|
834
|
+
if not HAS_PROTO:
|
|
835
|
+
raise ImportError("需要 protobuf 生成的代码")
|
|
836
|
+
|
|
837
|
+
req = geyser_pb2.PingRequest(count=count)
|
|
838
|
+
metadata = self._get_metadata()
|
|
839
|
+
resp = await self._client.ping(req, metadata=metadata)
|
|
840
|
+
|
|
841
|
+
return PongResponse(count=resp.count)
|
|
842
|
+
|
|
843
|
+
async def subscribe_replay_info(self) -> SubscribeReplayInfoResponse:
|
|
844
|
+
"""订阅重放信息"""
|
|
845
|
+
if not self._connected or not self._client:
|
|
846
|
+
raise RuntimeError("Client not connected")
|
|
847
|
+
|
|
848
|
+
if not HAS_PROTO:
|
|
849
|
+
raise ImportError("需要 protobuf 生成的代码")
|
|
850
|
+
|
|
851
|
+
req = geyser_pb2.SubscribeReplayInfoRequest()
|
|
852
|
+
metadata = self._get_metadata()
|
|
853
|
+
resp = await self._client.subscribe_replay_info(req, metadata=metadata)
|
|
854
|
+
|
|
855
|
+
result = SubscribeReplayInfoResponse()
|
|
856
|
+
if resp.HasField('first_available'):
|
|
857
|
+
result.first_available = resp.first_available
|
|
858
|
+
|
|
859
|
+
return result
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def parse_commitment_level(s: str) -> CommitmentLevel:
|
|
863
|
+
"""解析承诺级别字符串"""
|
|
864
|
+
s_lower = s.lower()
|
|
865
|
+
if s_lower == "confirmed":
|
|
866
|
+
return CommitmentLevel.CONFIRMED
|
|
867
|
+
elif s_lower == "finalized":
|
|
868
|
+
return CommitmentLevel.FINALIZED
|
|
869
|
+
else:
|
|
870
|
+
return CommitmentLevel.PROCESSED
|