sol-parser-sdk-python 0.5.8__py3-none-any.whl → 0.5.9__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 +24 -0
- sol_parser/account_dispatcher.py +19 -4
- sol_parser/account_fillers/raydium.py +6 -2
- sol_parser/account_fillers/raydium_launchlab.py +4 -0
- sol_parser/accounts/__init__.py +5 -0
- sol_parser/block_meta.py +29 -0
- sol_parser/dex_parsers.py +14 -1
- sol_parser/entries_decode.py +6 -80
- sol_parser/event_types.py +48 -0
- sol_parser/grpc/subscribe_builder.py +8 -0
- sol_parser/grpc_client.py +114 -20
- sol_parser/grpc_types.py +19 -0
- sol_parser/instructions.py +51 -9
- sol_parser/json_util.py +15 -1
- sol_parser/liquidity_snapshot.py +101 -0
- sol_parser/simulation_route.py +233 -0
- sol_parser/stonkfun_registry.py +139 -0
- sol_parser/transaction_route.py +510 -0
- sol_parser/wire_transaction.py +172 -0
- {sol_parser_sdk_python-0.5.8.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/METADATA +20 -2
- {sol_parser_sdk_python-0.5.8.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/RECORD +23 -17
- {sol_parser_sdk_python-0.5.8.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/WHEEL +1 -1
- {sol_parser_sdk_python-0.5.8.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/entry_points.txt +0 -0
sol_parser/__init__.py
CHANGED
|
@@ -466,3 +466,27 @@ __all__ = [
|
|
|
466
466
|
"ShredStreamClient",
|
|
467
467
|
"ShredStreamConfig",
|
|
468
468
|
]
|
|
469
|
+
|
|
470
|
+
# Native route and subscription state APIs aligned with Rust 0.7.7.
|
|
471
|
+
from .transaction_route import (analyze_rpc_transaction_routes, TransactionRoute, RouteSwapLeg,
|
|
472
|
+
RouteTokenTransfer, RouteUnknownInvocation, RouteNativeTokenAction, InstructionPosition,
|
|
473
|
+
stonkfun_mode_from_platform_config)
|
|
474
|
+
from .stonkfun_registry import StonkFunPoolRegistry, StonkFunGraduatedPool
|
|
475
|
+
from .liquidity_snapshot import RawAccountSnapshotEvent, LiquidityAccountSnapshotEvent, parse_liquidity_account
|
|
476
|
+
from .wire_transaction import decode_wire_transaction
|
|
477
|
+
|
|
478
|
+
from .simulation_route import analyze_simulation_routes
|
|
479
|
+
|
|
480
|
+
__all__.append("analyze_simulation_routes")
|
|
481
|
+
|
|
482
|
+
from .block_meta import BlockMetaEvent, parse_block_meta_update
|
|
483
|
+
from .grpc.subscribe_builder import build_subscribe_request_with_event_filter
|
|
484
|
+
|
|
485
|
+
from .event_types import (
|
|
486
|
+
LaunchLabPoolCreateEvent, StonkFunPoolCreateEvent,
|
|
487
|
+
LaunchLabTradeEvent, StonkFunTradeEvent,
|
|
488
|
+
)
|
|
489
|
+
__all__ += ["LaunchLabPoolCreateEvent", "StonkFunPoolCreateEvent", "LaunchLabTradeEvent", "StonkFunTradeEvent"]
|
|
490
|
+
|
|
491
|
+
from .grpc_client import DexEventQueue, GrpcStreamStatus
|
|
492
|
+
__all__ += ["DexEventQueue", "GrpcStreamStatus"]
|
sol_parser/account_dispatcher.py
CHANGED
|
@@ -82,11 +82,26 @@ def fill_accounts_with_owned_keys(
|
|
|
82
82
|
w = [bytes(x) for x in meta_pb.loaded_writable_addresses]
|
|
83
83
|
r = [bytes(x) for x in meta_pb.loaded_readonly_addresses]
|
|
84
84
|
|
|
85
|
-
def run(program_b58: str, filler) -> None:
|
|
85
|
+
def run(program_b58: str, filler, anchor=None, unambiguous=False) -> None:
|
|
86
86
|
pid = base58.b58decode(program_b58)
|
|
87
87
|
inv = invokes.get(pid)
|
|
88
88
|
if not inv:
|
|
89
89
|
return
|
|
90
|
+
if anchor is not None or unambiguous:
|
|
91
|
+
pool, index = anchor or (None,1)
|
|
92
|
+
has_anchor=pool and pool != "11111111111111111111111111111111"
|
|
93
|
+
if not has_anchor and not unambiguous:return
|
|
94
|
+
selected = None
|
|
95
|
+
for invocation in inv:
|
|
96
|
+
candidate = get_instruction_account_getter(meta_pb, transaction_pb, static_keys, w, r, invocation)
|
|
97
|
+
if candidate is None or (has_anchor and candidate(index) != pool):
|
|
98
|
+
continue
|
|
99
|
+
if selected is not None and any(selected(i) != candidate(i) for i in range(18)):
|
|
100
|
+
return # Same pool, conflicting wallet/account context: do not guess.
|
|
101
|
+
selected = candidate
|
|
102
|
+
if selected is not None:
|
|
103
|
+
filler(selected)
|
|
104
|
+
return
|
|
90
105
|
ix = find_instruction_invoke(inv, meta_pb, transaction_pb)
|
|
91
106
|
if ix is None:
|
|
92
107
|
return
|
|
@@ -146,7 +161,7 @@ def fill_accounts_with_owned_keys(
|
|
|
146
161
|
elif et == EventType.RAYDIUM_CPMM_INITIALIZE:
|
|
147
162
|
run(RAYDIUM_CPMM_PROGRAM_ID, lambda g: raydium.fill_cpmm_initialize_accounts(data, g))
|
|
148
163
|
elif et == EventType.RAYDIUM_AMM_V4_SWAP:
|
|
149
|
-
run(RAYDIUM_AMM_V4_PROGRAM_ID, lambda g: raydium.fill_amm_v4_swap_accounts(data, g))
|
|
164
|
+
run(RAYDIUM_AMM_V4_PROGRAM_ID, lambda g: raydium.fill_amm_v4_swap_accounts(data, g), (data.amm,1), True)
|
|
150
165
|
elif et == EventType.RAYDIUM_AMM_V4_DEPOSIT:
|
|
151
166
|
run(RAYDIUM_AMM_V4_PROGRAM_ID, lambda g: raydium.fill_amm_v4_deposit_accounts(data, g))
|
|
152
167
|
elif et == EventType.RAYDIUM_AMM_V4_WITHDRAW:
|
|
@@ -182,9 +197,9 @@ def fill_accounts_with_owned_keys(
|
|
|
182
197
|
elif et == EventType.METEORA_DLMM_REMOVE_LIQUIDITY:
|
|
183
198
|
run(METEORA_DLMM_PROGRAM_ID, lambda g: meteora.fill_dlmm_remove_liquidity_accounts(data, g))
|
|
184
199
|
elif et == EventType.RAYDIUM_LAUNCHLAB_TRADE:
|
|
185
|
-
run(RAYDIUM_LAUNCHLAB_PROGRAM_ID, lambda g: raydium_launchlab.fill_trade_accounts(data, g))
|
|
200
|
+
run(RAYDIUM_LAUNCHLAB_PROGRAM_ID, lambda g: raydium_launchlab.fill_trade_accounts(data, g), (data.pool_state,4))
|
|
186
201
|
elif et == EventType.RAYDIUM_LAUNCHLAB_POOL_CREATE:
|
|
187
|
-
run(RAYDIUM_LAUNCHLAB_PROGRAM_ID, lambda g: raydium_launchlab.fill_pool_create_accounts(data, g))
|
|
202
|
+
run(RAYDIUM_LAUNCHLAB_PROGRAM_ID, lambda g: raydium_launchlab.fill_pool_create_accounts(data, g), (data.pool_state,5))
|
|
188
203
|
|
|
189
204
|
|
|
190
205
|
def fill_data(
|
|
@@ -100,8 +100,12 @@ def fill_cpmm_initialize_accounts(e: RaydiumCpmmInitializeEvent, get: AccountGet
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
def fill_amm_v4_swap_accounts(e: RaydiumAmmV4SwapEvent, get: AccountGetter) -> None:
|
|
103
|
-
if _empty(e.amm):
|
|
104
|
-
|
|
103
|
+
if _empty(e.amm):e.amm=get(1)
|
|
104
|
+
modern=_empty(get(8)) and not _empty(get(7))
|
|
105
|
+
if modern:
|
|
106
|
+
for field,index in [('token_program',0),('amm_authority',2),('pool_coin_token_account',3),('pool_pc_token_account',4),('user_source_token_account',5),('user_destination_token_account',6),('user_source_owner',7)]:
|
|
107
|
+
if _empty(getattr(e,field)):setattr(e,field,get(index))
|
|
108
|
+
elif _empty(e.user_source_owner):e.user_source_owner=get(16) if _empty(get(17)) else get(17)
|
|
105
109
|
|
|
106
110
|
|
|
107
111
|
def fill_amm_v4_deposit_accounts(e: RaydiumAmmV4DepositEvent, get: AccountGetter) -> None:
|
|
@@ -43,6 +43,10 @@ def fill_trade_accounts(e: RaydiumLaunchlabTradeEvent, get: AccountGetter) -> No
|
|
|
43
43
|
if _empty(e.quote_token_program):
|
|
44
44
|
e.quote_token_program = get(12)
|
|
45
45
|
|
|
46
|
+
for name, index in [("system_program", 15), ("platform_associated_account", 16), ("creator_associated_account", 17)]:
|
|
47
|
+
if _empty(getattr(e,name,"")):
|
|
48
|
+
setattr(e,name,get(index))
|
|
49
|
+
|
|
46
50
|
|
|
47
51
|
def fill_pool_create_accounts(e: RaydiumLaunchlabPoolCreateEvent, get: AccountGetter) -> None:
|
|
48
52
|
if _empty(e.pool_state):
|
sol_parser/accounts/__init__.py
CHANGED
|
@@ -206,6 +206,11 @@ def parse_account_unified(
|
|
|
206
206
|
event_type_filter: Optional[EventTypeFilter] = None,
|
|
207
207
|
) -> Optional[DexEvent]:
|
|
208
208
|
"""对齐 Rust ``parse_account_unified``(含 ``Option<EventTypeFilter>`` 语义)。"""
|
|
209
|
+
from ..liquidity_snapshot import parse_liquidity_account
|
|
210
|
+
if event_type_filter is None or event_type_filter.should_include(EventType.ACCOUNT_LIQUIDITY_SNAPSHOT):
|
|
211
|
+
snapshot = parse_liquidity_account(account, metadata)
|
|
212
|
+
if snapshot is not None:
|
|
213
|
+
return DexEvent(type=EventType.ACCOUNT_LIQUIDITY_SNAPSHOT, data=snapshot)
|
|
209
214
|
data = account.data
|
|
210
215
|
if not data:
|
|
211
216
|
return None
|
sol_parser/block_meta.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Local BlockMeta adapter, matching Rust's saturated timestamp conversion."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from .grpc_types import EventType, EventMetadata
|
|
5
|
+
from .event_types import DexEvent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class BlockMetaEvent:
|
|
10
|
+
metadata: EventMetadata
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_block_meta_update(meta, grpc_recv_us, fallback_block_us=None):
|
|
14
|
+
block_us = fallback_block_us or 0
|
|
15
|
+
if meta.block_time is not None:
|
|
16
|
+
block_us = max(-(1 << 63), min((1 << 63) - 1, meta.block_time * 1_000_000))
|
|
17
|
+
return DexEvent(
|
|
18
|
+
EventType.BLOCK_META,
|
|
19
|
+
BlockMetaEvent(
|
|
20
|
+
EventMetadata(
|
|
21
|
+
signature="1" * 64,
|
|
22
|
+
slot=meta.slot,
|
|
23
|
+
tx_index=0,
|
|
24
|
+
block_time_us=block_us,
|
|
25
|
+
grpc_recv_us=grpc_recv_us,
|
|
26
|
+
recent_blockhash=meta.blockhash,
|
|
27
|
+
)
|
|
28
|
+
),
|
|
29
|
+
)
|
sol_parser/dex_parsers.py
CHANGED
|
@@ -2727,7 +2727,7 @@ def parse_raydium_launchlab_from_discriminator(disc: int, data: bytes, meta: dic
|
|
|
2727
2727
|
|
|
2728
2728
|
|
|
2729
2729
|
def _parse_raydium_launchlab_trade(data: bytes, meta: dict) -> Optional[DexEvent]:
|
|
2730
|
-
if len(data)
|
|
2730
|
+
if len(data) != 139 or data[136] > 1 or data[137] > 2 or data[138] > 1:
|
|
2731
2731
|
return None
|
|
2732
2732
|
pool = _pub(data, 0)
|
|
2733
2733
|
ai = _u64le(data, 88)
|
|
@@ -2746,6 +2746,19 @@ def _parse_raydium_launchlab_trade(data: bytes, meta: dict) -> Optional[DexEvent
|
|
|
2746
2746
|
is_buy=is_buy,
|
|
2747
2747
|
trade_direction=d,
|
|
2748
2748
|
exact_in=ex_in,
|
|
2749
|
+
pool_status=("Fund","Migrate","Trade")[data[137]],
|
|
2750
|
+
total_base_sell=_u64le(data,32),
|
|
2751
|
+
virtual_base=_u64le(data,40),
|
|
2752
|
+
virtual_quote=_u64le(data,48),
|
|
2753
|
+
real_base_before=_u64le(data,56),
|
|
2754
|
+
real_quote_before=_u64le(data,64),
|
|
2755
|
+
real_base_after=_u64le(data,72),
|
|
2756
|
+
real_quote_after=_u64le(data,80),
|
|
2757
|
+
protocol_fee=_u64le(data,104),
|
|
2758
|
+
platform_fee=_u64le(data,112),
|
|
2759
|
+
creator_fee=_u64le(data,120),
|
|
2760
|
+
share_fee=_u64le(data,128),
|
|
2761
|
+
|
|
2749
2762
|
),
|
|
2750
2763
|
)
|
|
2751
2764
|
|
sol_parser/entries_decode.py
CHANGED
|
@@ -46,87 +46,13 @@ class _ParsedTx:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def parse_transaction_wire(buf: bytes, pos: int) -> Optional[_ParsedTx]:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
p = pos + sig_count_enc[1]
|
|
56
|
-
sig_count = sig_count_enc[0]
|
|
57
|
-
sigs_end = p + sig_count * 64
|
|
58
|
-
if sigs_end > len(buf):
|
|
59
|
-
return None
|
|
60
|
-
sigs: List[bytes] = []
|
|
61
|
-
for _ in range(sig_count):
|
|
62
|
-
sigs.append(buf[p : p + 64])
|
|
63
|
-
p += 64
|
|
64
|
-
if p >= len(buf):
|
|
65
|
-
return None
|
|
66
|
-
msg_first = buf[p]
|
|
67
|
-
is_v0 = msg_first >= 0x80
|
|
68
|
-
if is_v0:
|
|
69
|
-
p += 1
|
|
70
|
-
p += 3
|
|
71
|
-
if p > len(buf):
|
|
72
|
-
return None
|
|
73
|
-
acct_enc = decode_compact_u16(buf, p)
|
|
74
|
-
if not acct_enc:
|
|
75
|
-
return None
|
|
76
|
-
p += acct_enc[1]
|
|
77
|
-
p += acct_enc[0] * 32
|
|
78
|
-
if p > len(buf):
|
|
79
|
-
return None
|
|
80
|
-
ix_count_enc = decode_compact_u16(buf, p)
|
|
81
|
-
if not ix_count_enc:
|
|
49
|
+
from .wire_transaction import decode_wire_transaction
|
|
50
|
+
import base58
|
|
51
|
+
try:
|
|
52
|
+
transaction, length = decode_wire_transaction(buf, pos, require_complete=False)
|
|
53
|
+
return _ParsedTx(length, [base58.b58decode(s) for s in transaction['signatures']])
|
|
54
|
+
except (ValueError, IndexError):
|
|
82
55
|
return None
|
|
83
|
-
p += ix_count_enc[1]
|
|
84
|
-
ix_count = ix_count_enc[0]
|
|
85
|
-
for _ in range(ix_count):
|
|
86
|
-
p += 1
|
|
87
|
-
if p > len(buf):
|
|
88
|
-
return None
|
|
89
|
-
acct_len_enc = decode_compact_u16(buf, p)
|
|
90
|
-
if not acct_len_enc:
|
|
91
|
-
return None
|
|
92
|
-
p += acct_len_enc[1]
|
|
93
|
-
p += acct_len_enc[0]
|
|
94
|
-
if p > len(buf):
|
|
95
|
-
return None
|
|
96
|
-
data_len_enc = decode_compact_u16(buf, p)
|
|
97
|
-
if not data_len_enc:
|
|
98
|
-
return None
|
|
99
|
-
p += data_len_enc[1]
|
|
100
|
-
p += data_len_enc[0]
|
|
101
|
-
if p > len(buf):
|
|
102
|
-
return None
|
|
103
|
-
if is_v0:
|
|
104
|
-
if p >= len(buf):
|
|
105
|
-
return None
|
|
106
|
-
atl_count_enc = decode_compact_u16(buf, p)
|
|
107
|
-
if not atl_count_enc:
|
|
108
|
-
return None
|
|
109
|
-
p += atl_count_enc[1]
|
|
110
|
-
atl_count = atl_count_enc[0]
|
|
111
|
-
for _ in range(atl_count):
|
|
112
|
-
p += 32
|
|
113
|
-
if p > len(buf):
|
|
114
|
-
return None
|
|
115
|
-
w_len_enc = decode_compact_u16(buf, p)
|
|
116
|
-
if not w_len_enc:
|
|
117
|
-
return None
|
|
118
|
-
p += w_len_enc[1]
|
|
119
|
-
p += w_len_enc[0]
|
|
120
|
-
if p > len(buf):
|
|
121
|
-
return None
|
|
122
|
-
r_len_enc = decode_compact_u16(buf, p)
|
|
123
|
-
if not r_len_enc:
|
|
124
|
-
return None
|
|
125
|
-
p += r_len_enc[1]
|
|
126
|
-
p += r_len_enc[0]
|
|
127
|
-
if p > len(buf):
|
|
128
|
-
return None
|
|
129
|
-
return _ParsedTx(tx_len=p - start, signatures=sigs)
|
|
130
56
|
|
|
131
57
|
|
|
132
58
|
class _BatchDecoder:
|
sol_parser/event_types.py
CHANGED
|
@@ -1410,6 +1410,28 @@ class RaydiumLaunchlabTradeEvent(DexEventBase):
|
|
|
1410
1410
|
base_token_program: str = ""
|
|
1411
1411
|
quote_token_program: str = ""
|
|
1412
1412
|
|
|
1413
|
+
total_base_sell: int = 0
|
|
1414
|
+
virtual_base: int = 0
|
|
1415
|
+
virtual_quote: int = 0
|
|
1416
|
+
real_base_before: int = 0
|
|
1417
|
+
real_quote_before: int = 0
|
|
1418
|
+
real_base_after: int = 0
|
|
1419
|
+
real_quote_after: int = 0
|
|
1420
|
+
protocol_fee: int = 0
|
|
1421
|
+
platform_fee: int = 0
|
|
1422
|
+
creator_fee: int = 0
|
|
1423
|
+
share_fee: int = 0
|
|
1424
|
+
pool_status: str = "Fund"
|
|
1425
|
+
system_program: str = ""
|
|
1426
|
+
platform_associated_account: str = ""
|
|
1427
|
+
creator_associated_account: str = ""
|
|
1428
|
+
|
|
1429
|
+
def stonkfun_mode(self):
|
|
1430
|
+
from .transaction_route import stonkfun_mode_from_platform_config
|
|
1431
|
+
return stonkfun_mode_from_platform_config(self.platform_config)
|
|
1432
|
+
|
|
1433
|
+
def is_stonkfun(self):
|
|
1434
|
+
return self.stonkfun_mode() is not None
|
|
1413
1435
|
|
|
1414
1436
|
@dataclass
|
|
1415
1437
|
class RaydiumLaunchlabPoolCreateEvent(DexEventBase):
|
|
@@ -1428,6 +1450,14 @@ class RaydiumLaunchlabPoolCreateEvent(DexEventBase):
|
|
|
1428
1450
|
quote_token_program: str = ""
|
|
1429
1451
|
|
|
1430
1452
|
|
|
1453
|
+
|
|
1454
|
+
def stonkfun_mode(self):
|
|
1455
|
+
from .transaction_route import stonkfun_mode_from_platform_config
|
|
1456
|
+
return stonkfun_mode_from_platform_config(self.platform_config)
|
|
1457
|
+
|
|
1458
|
+
def is_stonkfun(self):
|
|
1459
|
+
return self.stonkfun_mode() is not None
|
|
1460
|
+
|
|
1431
1461
|
@dataclass
|
|
1432
1462
|
class RaydiumLaunchlabMigrateAmmEvent(DexEventBase):
|
|
1433
1463
|
"""RaydiumLaunchlab 迁移 AMM 事件"""
|
|
@@ -1436,6 +1466,18 @@ class RaydiumLaunchlabMigrateAmmEvent(DexEventBase):
|
|
|
1436
1466
|
user: str = ""
|
|
1437
1467
|
liquidity_amount: int = 0
|
|
1438
1468
|
|
|
1469
|
+
liquidity_amount_known: bool = False
|
|
1470
|
+
base_mint: str = ""
|
|
1471
|
+
quote_mint: str = ""
|
|
1472
|
+
platform_config: str = ""
|
|
1473
|
+
destination_program: str = ""
|
|
1474
|
+
|
|
1475
|
+
def stonkfun_mode(self):
|
|
1476
|
+
from .transaction_route import stonkfun_mode_from_platform_config
|
|
1477
|
+
return stonkfun_mode_from_platform_config(self.platform_config)
|
|
1478
|
+
|
|
1479
|
+
def is_stonkfun(self):
|
|
1480
|
+
return self.stonkfun_mode() is not None
|
|
1439
1481
|
|
|
1440
1482
|
# ============================================================
|
|
1441
1483
|
# 类型联合
|
|
@@ -2379,3 +2421,9 @@ def legacy_dict_to_dex_event(d: dict) -> Optional[DexEvent]:
|
|
|
2379
2421
|
except ValueError:
|
|
2380
2422
|
return None
|
|
2381
2423
|
return DexEvent(type=et, data=typed)
|
|
2424
|
+
|
|
2425
|
+
# Same classes: preserve isinstance, serialization and platform attribution.
|
|
2426
|
+
LaunchLabPoolCreateEvent = RaydiumLaunchlabPoolCreateEvent
|
|
2427
|
+
StonkFunPoolCreateEvent = RaydiumLaunchlabPoolCreateEvent
|
|
2428
|
+
LaunchLabTradeEvent = RaydiumLaunchlabTradeEvent
|
|
2429
|
+
StonkFunTradeEvent = RaydiumLaunchlabTradeEvent
|
|
@@ -131,3 +131,11 @@ __all__ = [
|
|
|
131
131
|
"build_subscribe_request_with_commitment",
|
|
132
132
|
"build_subscribe_transaction_filters_named",
|
|
133
133
|
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def build_subscribe_request_with_event_filter(tx_filters, acc_filters, event_type_filter=None):
|
|
137
|
+
from ..grpc_types import EventType
|
|
138
|
+
request=build_subscribe_request(tx_filters,acc_filters)
|
|
139
|
+
if event_type_filter is not None and event_type_filter.should_include(EventType.BLOCK_META):
|
|
140
|
+
request.blocks_meta["block_meta"].CopyFrom(geyser_pb2.SubscribeRequestFilterBlocksMeta())
|
|
141
|
+
return request
|
sol_parser/grpc_client.py
CHANGED
|
@@ -11,7 +11,7 @@ import asyncio
|
|
|
11
11
|
import time
|
|
12
12
|
import uuid
|
|
13
13
|
from typing import Dict, List, Optional, Callable, Any, AsyncGenerator, Tuple
|
|
14
|
-
from dataclasses import dataclass
|
|
14
|
+
from dataclasses import dataclass, replace
|
|
15
15
|
from urllib.parse import urlparse
|
|
16
16
|
|
|
17
17
|
import base58
|
|
@@ -97,6 +97,42 @@ class Subscription:
|
|
|
97
97
|
callbacks: SubscribeCallbacks
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class GrpcStreamStatus:
|
|
102
|
+
state: str = "connecting"
|
|
103
|
+
continuity_broken: bool = False
|
|
104
|
+
reconnects: int = 0
|
|
105
|
+
dropped: int = 0
|
|
106
|
+
|
|
107
|
+
class DexEventQueue(asyncio.Queue):
|
|
108
|
+
"""Queue-compatible DEX subscription with observable transport failures."""
|
|
109
|
+
def __init__(self, maxsize):
|
|
110
|
+
super().__init__(maxsize=maxsize)
|
|
111
|
+
self.errors = asyncio.Queue(maxsize=maxsize)
|
|
112
|
+
self.states = asyncio.Queue(maxsize=maxsize)
|
|
113
|
+
self._status = GrpcStreamStatus()
|
|
114
|
+
self.states.put_nowait(self._status)
|
|
115
|
+
|
|
116
|
+
def status(self):
|
|
117
|
+
return self._status
|
|
118
|
+
|
|
119
|
+
def transition(self, state, *, continuity_broken=None, reconnect=False, dropped=False):
|
|
120
|
+
previous = self._status
|
|
121
|
+
self._status = replace(
|
|
122
|
+
self._status, state=state,
|
|
123
|
+
continuity_broken=self._status.continuity_broken if continuity_broken is None else continuity_broken,
|
|
124
|
+
reconnects=self._status.reconnects + int(reconnect),
|
|
125
|
+
dropped=self._status.dropped + int(dropped),
|
|
126
|
+
)
|
|
127
|
+
if self._status == previous: return
|
|
128
|
+
if self.states.full(): self.states.get_nowait()
|
|
129
|
+
self.states.put_nowait(self._status)
|
|
130
|
+
|
|
131
|
+
def report_error(self, error):
|
|
132
|
+
if self.errors.full(): self.errors.get_nowait()
|
|
133
|
+
self.errors.put_nowait(error)
|
|
134
|
+
|
|
135
|
+
|
|
100
136
|
class YellowstoneGrpc:
|
|
101
137
|
"""Yellowstone gRPC 客户端"""
|
|
102
138
|
|
|
@@ -109,6 +145,7 @@ class YellowstoneGrpc:
|
|
|
109
145
|
self._channel: Optional[aio.Channel] = None
|
|
110
146
|
self._client: Optional[Any] = None
|
|
111
147
|
self._lock = asyncio.Lock()
|
|
148
|
+
self._dex_lifecycle = asyncio.Lock()
|
|
112
149
|
self._dex_event_queue: Optional[asyncio.Queue] = None
|
|
113
150
|
self._dex_event_filter: Optional[Any] = None
|
|
114
151
|
self._dex_cancel_event: Optional[asyncio.Event] = None
|
|
@@ -140,7 +177,27 @@ class YellowstoneGrpc:
|
|
|
140
177
|
inst.set_x_token(token)
|
|
141
178
|
return inst
|
|
142
179
|
|
|
143
|
-
async def
|
|
180
|
+
async def stop(self):
|
|
181
|
+
async with self._dex_lifecycle:
|
|
182
|
+
await self._stop_dex_unlocked()
|
|
183
|
+
|
|
184
|
+
async def _stop_dex_unlocked(self):
|
|
185
|
+
if self._dex_cancel_event is not None: self._dex_cancel_event.set()
|
|
186
|
+
task = self._dex_task
|
|
187
|
+
if task is not None:
|
|
188
|
+
task.cancel()
|
|
189
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
190
|
+
self._dex_task = None
|
|
191
|
+
self._dex_cancel_event = None
|
|
192
|
+
self._dex_request_queue = None
|
|
193
|
+
self._dex_current_req = None
|
|
194
|
+
|
|
195
|
+
async def subscribe_dex_events(self, transaction_filters, account_filters, event_type_filter=None):
|
|
196
|
+
async with self._dex_lifecycle:
|
|
197
|
+
await self._stop_dex_unlocked()
|
|
198
|
+
return await self._subscribe_dex_events(transaction_filters, account_filters, event_type_filter)
|
|
199
|
+
|
|
200
|
+
async def _subscribe_dex_events(
|
|
144
201
|
self,
|
|
145
202
|
transaction_filters: List[TransactionFilter],
|
|
146
203
|
account_filters: List[Any],
|
|
@@ -156,10 +213,10 @@ class YellowstoneGrpc:
|
|
|
156
213
|
if not self._client:
|
|
157
214
|
raise RuntimeError("Client not connected")
|
|
158
215
|
|
|
159
|
-
from .grpc.subscribe_builder import
|
|
216
|
+
from .grpc.subscribe_builder import build_subscribe_request_with_event_filter
|
|
160
217
|
|
|
161
|
-
req =
|
|
162
|
-
queue: asyncio.Queue =
|
|
218
|
+
req = build_subscribe_request_with_event_filter(transaction_filters, account_filters, event_type_filter)
|
|
219
|
+
queue: asyncio.Queue = DexEventQueue(maxsize=max(1, int(self.config.buffer_size or 100_000)))
|
|
163
220
|
|
|
164
221
|
if self._dex_cancel_event is not None:
|
|
165
222
|
self._dex_cancel_event.set()
|
|
@@ -178,15 +235,19 @@ class YellowstoneGrpc:
|
|
|
178
235
|
)
|
|
179
236
|
return queue
|
|
180
237
|
|
|
181
|
-
async def update_subscription(
|
|
238
|
+
async def update_subscription(self, transaction_filters, account_filters):
|
|
239
|
+
async with self._dex_lifecycle:
|
|
240
|
+
return await self._update_subscription(transaction_filters, account_filters)
|
|
241
|
+
|
|
242
|
+
async def _update_subscription(
|
|
182
243
|
self,
|
|
183
244
|
transaction_filters: List[TransactionFilter],
|
|
184
245
|
account_filters: List[Any],
|
|
185
246
|
) -> None:
|
|
186
247
|
"""动态更新 DEX 订阅。
|
|
187
248
|
|
|
188
|
-
|
|
189
|
-
|
|
249
|
+
通过原双向流发送最新过滤器,保留事件消费队列。
|
|
250
|
+
重连后使用最新请求,调用方无需替换消费队列。
|
|
190
251
|
"""
|
|
191
252
|
if self._dex_event_queue is None:
|
|
192
253
|
raise RuntimeError("No active DEX subscription")
|
|
@@ -195,9 +256,9 @@ class YellowstoneGrpc:
|
|
|
195
256
|
if not self._client:
|
|
196
257
|
raise RuntimeError("Client not connected")
|
|
197
258
|
|
|
198
|
-
from .grpc.subscribe_builder import
|
|
259
|
+
from .grpc.subscribe_builder import build_subscribe_request_with_event_filter
|
|
199
260
|
|
|
200
|
-
req =
|
|
261
|
+
req = build_subscribe_request_with_event_filter(transaction_filters, account_filters, self._dex_event_filter)
|
|
201
262
|
self._dex_current_req = req
|
|
202
263
|
if self._dex_request_queue is None:
|
|
203
264
|
raise RuntimeError("No active DEX subscription")
|
|
@@ -294,6 +355,11 @@ class YellowstoneGrpc:
|
|
|
294
355
|
self._connected = True
|
|
295
356
|
|
|
296
357
|
async def disconnect(self) -> None:
|
|
358
|
+
async with self._dex_lifecycle:
|
|
359
|
+
await self._stop_dex_unlocked()
|
|
360
|
+
await self._disconnect()
|
|
361
|
+
|
|
362
|
+
async def _disconnect(self) -> None:
|
|
297
363
|
"""断开连接"""
|
|
298
364
|
if not self._connected:
|
|
299
365
|
return
|
|
@@ -413,7 +479,8 @@ class YellowstoneGrpc:
|
|
|
413
479
|
blockhash=meta.blockhash,
|
|
414
480
|
parent_slot=meta.parent_slot,
|
|
415
481
|
parent_blockhash=meta.parent_blockhash,
|
|
416
|
-
executed_transaction_count=meta.executed_transaction_count
|
|
482
|
+
executed_transaction_count=meta.executed_transaction_count,
|
|
483
|
+
block_time=meta.block_time.timestamp if meta.HasField("block_time") else None
|
|
417
484
|
)
|
|
418
485
|
|
|
419
486
|
# 转换 Ping
|
|
@@ -432,7 +499,8 @@ class YellowstoneGrpc:
|
|
|
432
499
|
queue.put_nowait(event)
|
|
433
500
|
except asyncio.QueueFull:
|
|
434
501
|
# 与 Rust 有界 ArrayQueue 的低延迟取舍一致:消费端落后时丢弃新事件,避免阻塞 gRPC 流。
|
|
435
|
-
|
|
502
|
+
if isinstance(queue, DexEventQueue):
|
|
503
|
+
queue.transition(queue.status().state, continuity_broken=True, dropped=True)
|
|
436
504
|
|
|
437
505
|
async def _enqueue_transaction_dex_events(
|
|
438
506
|
self,
|
|
@@ -554,6 +622,15 @@ class YellowstoneGrpc:
|
|
|
554
622
|
block_time_us=0 if block_time_us is None else block_time_us,
|
|
555
623
|
grpc_recv_us=grpc_recv_us,
|
|
556
624
|
)
|
|
625
|
+
# Raw snapshots are opt-in, including closure updates with empty bytes.
|
|
626
|
+
from .event_types import DexEvent
|
|
627
|
+
from .grpc_types import EventType
|
|
628
|
+
from .liquidity_snapshot import RawAccountSnapshotEvent
|
|
629
|
+
if EventType.ACCOUNT_RAW_SNAPSHOT in (getattr(event_type_filter, "include_only", None) or []):
|
|
630
|
+
self._queue_event_nowait(queue, DexEvent(
|
|
631
|
+
type=EventType.ACCOUNT_RAW_SNAPSHOT,
|
|
632
|
+
data=RawAccountSnapshotEvent(metadata, account, int(acc.write_version), bool(update.account.is_startup)),
|
|
633
|
+
))
|
|
557
634
|
ev = parse_account_unified(account, metadata, event_type_filter)
|
|
558
635
|
if ev is not None:
|
|
559
636
|
self._queue_event_nowait(queue, ev)
|
|
@@ -582,6 +659,7 @@ class YellowstoneGrpc:
|
|
|
582
659
|
|
|
583
660
|
try:
|
|
584
661
|
while not cancel_event.is_set():
|
|
662
|
+
if isinstance(queue, DexEventQueue): queue.transition("connecting")
|
|
585
663
|
outgoing: asyncio.Queue = asyncio.Queue()
|
|
586
664
|
|
|
587
665
|
async def request_iterator():
|
|
@@ -606,6 +684,7 @@ class YellowstoneGrpc:
|
|
|
606
684
|
async for pb_update in self._client.Subscribe(request_iterator(), metadata=metadata):
|
|
607
685
|
if cancel_event.is_set():
|
|
608
686
|
break
|
|
687
|
+
if isinstance(queue, DexEventQueue): queue.transition("connected")
|
|
609
688
|
if pb_update.HasField("ping"):
|
|
610
689
|
await outgoing.put(
|
|
611
690
|
geyser_pb2.SubscribeRequest(
|
|
@@ -627,6 +706,13 @@ class YellowstoneGrpc:
|
|
|
627
706
|
block_time_us,
|
|
628
707
|
order_dispatcher,
|
|
629
708
|
)
|
|
709
|
+
if update.block_meta:
|
|
710
|
+
from .block_meta import parse_block_meta_update
|
|
711
|
+
from .grpc_types import EventType
|
|
712
|
+
if event_type_filter is None or event_type_filter.should_include(EventType.BLOCK_META):
|
|
713
|
+
# Match transaction/account overflow semantics. A full
|
|
714
|
+
# consumer queue must not stall gRPC reads and pings.
|
|
715
|
+
self._queue_event_nowait(queue, parse_block_meta_update(update.block_meta,grpc_recv_us,block_time_us))
|
|
630
716
|
if update.account:
|
|
631
717
|
await self._enqueue_account_dex_event(
|
|
632
718
|
queue,
|
|
@@ -635,10 +721,15 @@ class YellowstoneGrpc:
|
|
|
635
721
|
grpc_recv_us,
|
|
636
722
|
block_time_us,
|
|
637
723
|
)
|
|
724
|
+
if not cancel_event.is_set():
|
|
725
|
+
raise ConnectionError("Yellowstone DEX stream ended")
|
|
638
726
|
delay = 1.0
|
|
639
727
|
except asyncio.CancelledError:
|
|
640
728
|
break
|
|
641
|
-
except Exception:
|
|
729
|
+
except Exception as error:
|
|
730
|
+
if isinstance(queue, DexEventQueue):
|
|
731
|
+
queue.report_error(error)
|
|
732
|
+
queue.transition("reconnecting", continuity_broken=True, reconnect=True)
|
|
642
733
|
order_dispatcher.flush_all(lambda ev: self._queue_event_nowait(queue, ev))
|
|
643
734
|
if cancel_event.is_set():
|
|
644
735
|
break
|
|
@@ -646,10 +737,13 @@ class YellowstoneGrpc:
|
|
|
646
737
|
delay = min(delay * 2.0, 60.0)
|
|
647
738
|
finally:
|
|
648
739
|
pump_task.cancel()
|
|
740
|
+
await asyncio.gather(pump_task, return_exceptions=True)
|
|
649
741
|
await outgoing.put(None)
|
|
650
742
|
finally:
|
|
651
743
|
if flush_task is not None:
|
|
652
744
|
flush_task.cancel()
|
|
745
|
+
await asyncio.gather(flush_task, return_exceptions=True)
|
|
746
|
+
if isinstance(queue, DexEventQueue): queue.transition("stopped", continuity_broken=True)
|
|
653
747
|
order_dispatcher.flush_all(lambda ev: self._queue_event_nowait(queue, ev))
|
|
654
748
|
|
|
655
749
|
async def subscribe_transactions(
|
|
@@ -766,7 +860,7 @@ class YellowstoneGrpc:
|
|
|
766
860
|
req.commitment = commitment.value
|
|
767
861
|
|
|
768
862
|
metadata = self._get_metadata()
|
|
769
|
-
resp = await self._client.
|
|
863
|
+
resp = await self._client.GetLatestBlockhash(req, metadata=metadata)
|
|
770
864
|
|
|
771
865
|
return GetLatestBlockhashResponse(
|
|
772
866
|
slot=resp.slot,
|
|
@@ -789,7 +883,7 @@ class YellowstoneGrpc:
|
|
|
789
883
|
req.commitment = commitment.value
|
|
790
884
|
|
|
791
885
|
metadata = self._get_metadata()
|
|
792
|
-
resp = await self._client.
|
|
886
|
+
resp = await self._client.GetBlockHeight(req, metadata=metadata)
|
|
793
887
|
|
|
794
888
|
return GetBlockHeightResponse(block_height=resp.block_height)
|
|
795
889
|
|
|
@@ -808,7 +902,7 @@ class YellowstoneGrpc:
|
|
|
808
902
|
req.commitment = commitment.value
|
|
809
903
|
|
|
810
904
|
metadata = self._get_metadata()
|
|
811
|
-
resp = await self._client.
|
|
905
|
+
resp = await self._client.GetSlot(req, metadata=metadata)
|
|
812
906
|
|
|
813
907
|
return GetSlotResponse(slot=resp.slot)
|
|
814
908
|
|
|
@@ -822,7 +916,7 @@ class YellowstoneGrpc:
|
|
|
822
916
|
|
|
823
917
|
req = geyser_pb2.GetVersionRequest()
|
|
824
918
|
metadata = self._get_metadata()
|
|
825
|
-
resp = await self._client.
|
|
919
|
+
resp = await self._client.GetVersion(req, metadata=metadata)
|
|
826
920
|
|
|
827
921
|
return GetVersionResponse(version=resp.version)
|
|
828
922
|
|
|
@@ -841,7 +935,7 @@ class YellowstoneGrpc:
|
|
|
841
935
|
req.commitment = commitment.value
|
|
842
936
|
|
|
843
937
|
metadata = self._get_metadata()
|
|
844
|
-
resp = await self._client.
|
|
938
|
+
resp = await self._client.IsBlockhashValid(req, metadata=metadata)
|
|
845
939
|
|
|
846
940
|
return IsBlockhashValidResponse(slot=resp.slot, valid=resp.valid)
|
|
847
941
|
|
|
@@ -855,7 +949,7 @@ class YellowstoneGrpc:
|
|
|
855
949
|
|
|
856
950
|
req = geyser_pb2.PingRequest(count=count)
|
|
857
951
|
metadata = self._get_metadata()
|
|
858
|
-
resp = await self._client.
|
|
952
|
+
resp = await self._client.Ping(req, metadata=metadata)
|
|
859
953
|
|
|
860
954
|
return PongResponse(count=resp.count)
|
|
861
955
|
|
|
@@ -869,7 +963,7 @@ class YellowstoneGrpc:
|
|
|
869
963
|
|
|
870
964
|
req = geyser_pb2.SubscribeReplayInfoRequest()
|
|
871
965
|
metadata = self._get_metadata()
|
|
872
|
-
resp = await self._client.
|
|
966
|
+
resp = await self._client.SubscribeReplayInfo(req, metadata=metadata)
|
|
873
967
|
|
|
874
968
|
result = SubscribeReplayInfoResponse()
|
|
875
969
|
if resp.HasField('first_available'):
|