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