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/grpc_types.py
CHANGED
|
@@ -37,6 +37,8 @@ class EventType(str, Enum):
|
|
|
37
37
|
"""事件类型"""
|
|
38
38
|
# Block
|
|
39
39
|
BLOCK_META = "BlockMeta"
|
|
40
|
+
ACCOUNT_RAW_SNAPSHOT = "AccountRawSnapshot"
|
|
41
|
+
ACCOUNT_LIQUIDITY_SNAPSHOT = "AccountLiquiditySnapshot"
|
|
40
42
|
# RaydiumLaunchlab
|
|
41
43
|
RAYDIUM_LAUNCHLAB_TRADE = "RaydiumLaunchlabTrade"
|
|
42
44
|
RAYDIUM_LAUNCHLAB_POOL_CREATE = "RaydiumLaunchlabPoolCreate"
|
|
@@ -435,6 +437,18 @@ class EventMetadata:
|
|
|
435
437
|
recent_blockhash: str = ""
|
|
436
438
|
is_created_buy: bool = False
|
|
437
439
|
|
|
440
|
+
def __post_init__(self):
|
|
441
|
+
for name,signed in (("slot",False),("tx_index",False),("block_time_us",True),("grpc_recv_us",True)):
|
|
442
|
+
value=getattr(self,name)
|
|
443
|
+
if isinstance(value,str):
|
|
444
|
+
import re
|
|
445
|
+
if not re.fullmatch(r"-?\d+",value):raise ValueError(f"Invalid {name} integer")
|
|
446
|
+
value=int(value)
|
|
447
|
+
minimum=-(1<<63) if signed else 0
|
|
448
|
+
maximum=(1<<63)-1 if signed else (1<<64)-1
|
|
449
|
+
if type(value) is not int or not minimum<=value<=maximum:raise ValueError(f"{name} outside {'i64' if signed else 'u64'}")
|
|
450
|
+
setattr(self,name,value)
|
|
451
|
+
|
|
438
452
|
|
|
439
453
|
@dataclass
|
|
440
454
|
class ClientConfig:
|
|
@@ -747,6 +761,8 @@ class Protocol(str, Enum):
|
|
|
747
761
|
PUMP_SWAP = "PumpSwap"
|
|
748
762
|
PUMP_FEES = "PumpFees"
|
|
749
763
|
RAYDIUM_LAUNCHLAB = "RaydiumLaunchlab"
|
|
764
|
+
LAUNCH_LAB = "LaunchLab"
|
|
765
|
+
STONK_FUN = "StonkFun"
|
|
750
766
|
RAYDIUM_CPMM = "RaydiumCpmm"
|
|
751
767
|
RAYDIUM_CLMM = "RaydiumClmm"
|
|
752
768
|
RAYDIUM_AMM_V4 = "RaydiumAmmV4"
|
|
@@ -762,6 +778,8 @@ _PROTOCOL_PROGRAM_IDS: Dict[Protocol, List[str]] = {
|
|
|
762
778
|
Protocol.PUMP_FUN: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"],
|
|
763
779
|
Protocol.PUMP_SWAP: ["pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"],
|
|
764
780
|
Protocol.PUMP_FEES: ["pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"],
|
|
781
|
+
Protocol.LAUNCH_LAB: ["LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"],
|
|
782
|
+
Protocol.STONK_FUN: ["LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"],
|
|
765
783
|
Protocol.RAYDIUM_LAUNCHLAB: ["LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"],
|
|
766
784
|
Protocol.RAYDIUM_CPMM: ["CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C"],
|
|
767
785
|
Protocol.RAYDIUM_CLMM: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"],
|
|
@@ -981,6 +999,7 @@ class SubscribeUpdateBlockMeta:
|
|
|
981
999
|
parent_slot: int = 0
|
|
982
1000
|
parent_blockhash: str = ""
|
|
983
1001
|
executed_transaction_count: int = 0
|
|
1002
|
+
block_time: Optional[int] = None
|
|
984
1003
|
|
|
985
1004
|
|
|
986
1005
|
@dataclass
|
sol_parser/instructions.py
CHANGED
|
@@ -368,7 +368,7 @@ def normal_instruction_data_may_parse(program_id: str, instruction_data: bytes)
|
|
|
368
368
|
if not instruction_data:
|
|
369
369
|
return False
|
|
370
370
|
if program_id == RAYDIUM_AMM_V4_PROGRAM_ID:
|
|
371
|
-
return instruction_data[0] in (1, 3, 4, 7, 9, 11)
|
|
371
|
+
return instruction_data[0] in (1, 3, 4, 7, 9, 11, 16, 17)
|
|
372
372
|
if program_id == METEORA_DLMM_PROGRAM_ID:
|
|
373
373
|
disc = _disc8(instruction_data)
|
|
374
374
|
return disc in _DLMM_INSTRUCTION_DISCS if disc is not None else False
|
|
@@ -1710,21 +1710,27 @@ def parse_raydium_amm_v4_instruction(
|
|
|
1710
1710
|
instr_type = data[0]
|
|
1711
1711
|
meta = _make_meta(signature, slot, tx_index, block_time_us, grpc_recv_us)
|
|
1712
1712
|
|
|
1713
|
-
if instr_type in (9, 11): # SwapBaseIn / SwapBaseOut
|
|
1713
|
+
if instr_type in (9, 11, 16, 17): # SwapBaseIn / SwapBaseOut
|
|
1714
1714
|
if len(data) < 17:
|
|
1715
1715
|
return None
|
|
1716
|
+
modern = instr_type in (16,17)
|
|
1717
|
+
if modern and len(accounts)<8:return None
|
|
1716
1718
|
first, second = struct.unpack_from("<QQ", data, 1)
|
|
1717
1719
|
shift = 1 if len(accounts) == 17 else 0
|
|
1718
1720
|
def g(index: int) -> str:
|
|
1719
|
-
|
|
1721
|
+
if modern:
|
|
1722
|
+
index = {0:0,1:1,2:2,5:3,6:4,15:5,16:6,17:7}.get(index,-1)
|
|
1723
|
+
else:
|
|
1724
|
+
index = index-shift if index>=5 else index
|
|
1725
|
+
return _get_account_safe(accounts,index)
|
|
1720
1726
|
return legacy_dict_to_dex_event({"RaydiumAmmV4Swap": {
|
|
1721
1727
|
"metadata": meta,
|
|
1722
1728
|
"amm": _get_account_safe(accounts, 1),
|
|
1723
1729
|
"user_source_owner": g(17),
|
|
1724
|
-
"amount_in": first if instr_type
|
|
1725
|
-
"minimum_amount_out": second if instr_type
|
|
1726
|
-
"max_amount_in": first if instr_type
|
|
1727
|
-
"amount_out": second if instr_type
|
|
1730
|
+
"amount_in": first if instr_type in (9,16) else 0,
|
|
1731
|
+
"minimum_amount_out": second if instr_type in (9,16) else 0,
|
|
1732
|
+
"max_amount_in": first if instr_type in (11,17) else 0,
|
|
1733
|
+
"amount_out": second if instr_type in (11,17) else 0,
|
|
1728
1734
|
"token_program": g(0), "amm_authority": g(2), "amm_open_orders": g(3),
|
|
1729
1735
|
"pool_coin_token_account": g(5), "pool_pc_token_account": g(6),
|
|
1730
1736
|
"serum_program": g(7), "serum_market": g(8), "serum_bids": g(9),
|
|
@@ -1843,7 +1849,7 @@ def parse_raydium_launchlab_instruction(
|
|
|
1843
1849
|
payload = data[8:]
|
|
1844
1850
|
|
|
1845
1851
|
if discriminator == _DISC_RAYDIUM_LAUNCHLAB_TRADE:
|
|
1846
|
-
if len(payload)
|
|
1852
|
+
if len(payload) != 139 or payload[136] > 1 or payload[137] > 2 or payload[138] > 1:
|
|
1847
1853
|
return None
|
|
1848
1854
|
return legacy_dict_to_dex_event({"RaydiumLaunchlabTrade": {
|
|
1849
1855
|
"metadata": meta,
|
|
@@ -1854,6 +1860,19 @@ def parse_raydium_launchlab_instruction(
|
|
|
1854
1860
|
"is_buy": payload[136] == 0,
|
|
1855
1861
|
"trade_direction": "Buy" if payload[136] == 0 else "Sell",
|
|
1856
1862
|
"exact_in": payload[138] == 1,
|
|
1863
|
+
"pool_status": ("Fund","Migrate","Trade")[payload[137]],
|
|
1864
|
+
"total_base_sell": _u64_payload(payload,32),
|
|
1865
|
+
"virtual_base": _u64_payload(payload,40),
|
|
1866
|
+
"virtual_quote": _u64_payload(payload,48),
|
|
1867
|
+
"real_base_before": _u64_payload(payload,56),
|
|
1868
|
+
"real_quote_before": _u64_payload(payload,64),
|
|
1869
|
+
"real_base_after": _u64_payload(payload,72),
|
|
1870
|
+
"real_quote_after": _u64_payload(payload,80),
|
|
1871
|
+
"protocol_fee": _u64_payload(payload,104),
|
|
1872
|
+
"platform_fee": _u64_payload(payload,112),
|
|
1873
|
+
"creator_fee": _u64_payload(payload,120),
|
|
1874
|
+
"share_fee": _u64_payload(payload,128),
|
|
1875
|
+
|
|
1857
1876
|
}})
|
|
1858
1877
|
if discriminator == _DISC_RAYDIUM_LAUNCHLAB_POOL_CREATE:
|
|
1859
1878
|
if len(payload) < 97:
|
|
@@ -1897,6 +1916,20 @@ def parse_raydium_launchlab_instruction(
|
|
|
1897
1916
|
"is_buy": is_buy,
|
|
1898
1917
|
"trade_direction": "Buy" if is_buy else "Sell",
|
|
1899
1918
|
"exact_in": exact_in,
|
|
1919
|
+
"global_config": _get_account_safe(accounts,2),
|
|
1920
|
+
"platform_config": _get_account_safe(accounts,3),
|
|
1921
|
+
"user_base_token": _get_account_safe(accounts,5),
|
|
1922
|
+
"user_quote_token": _get_account_safe(accounts,6),
|
|
1923
|
+
"base_vault": _get_account_safe(accounts,7),
|
|
1924
|
+
"quote_vault": _get_account_safe(accounts,8),
|
|
1925
|
+
"base_mint": _get_account_safe(accounts,9),
|
|
1926
|
+
"quote_mint": _get_account_safe(accounts,10),
|
|
1927
|
+
"base_token_program": _get_account_safe(accounts,11),
|
|
1928
|
+
"quote_token_program": _get_account_safe(accounts,12),
|
|
1929
|
+
"system_program": _get_account_safe(accounts,15),
|
|
1930
|
+
"platform_associated_account": _get_account_safe(accounts,16),
|
|
1931
|
+
"creator_associated_account": _get_account_safe(accounts,17),
|
|
1932
|
+
|
|
1900
1933
|
}})
|
|
1901
1934
|
if discriminator in (
|
|
1902
1935
|
_IX_RAYDIUM_LAUNCHLAB_INITIALIZE,
|
|
@@ -1922,7 +1955,16 @@ def parse_raydium_launchlab_instruction(
|
|
|
1922
1955
|
_IX_RAYDIUM_LAUNCHLAB_MIGRATE_TO_AMM,
|
|
1923
1956
|
_IX_RAYDIUM_LAUNCHLAB_MIGRATE_TO_CPSWAP,
|
|
1924
1957
|
):
|
|
1925
|
-
|
|
1958
|
+
cp = discriminator == _IX_RAYDIUM_LAUNCHLAB_MIGRATE_TO_CPSWAP
|
|
1959
|
+
if len(accounts) < (28 if cp else 32) or (not cp and len(payload) < 9): return None
|
|
1960
|
+
return legacy_dict_to_dex_event({"RaydiumLaunchlabMigrateAmm": {
|
|
1961
|
+
"metadata": meta, "old_pool": _get_account_safe(accounts,17 if cp else 23),
|
|
1962
|
+
"new_pool": _get_account_safe(accounts,5 if cp else 13), "user": _get_account_safe(accounts,0),
|
|
1963
|
+
"liquidity_amount": 0, "liquidity_amount_known": False,
|
|
1964
|
+
"base_mint": _get_account_safe(accounts,1), "quote_mint": _get_account_safe(accounts,2),
|
|
1965
|
+
"platform_config": _get_account_safe(accounts,3) if cp else Z,
|
|
1966
|
+
"destination_program": _get_account_safe(accounts,4 if cp else 12),
|
|
1967
|
+
}})
|
|
1926
1968
|
|
|
1927
1969
|
return None
|
|
1928
1970
|
|
sol_parser/json_util.py
CHANGED
|
@@ -11,7 +11,7 @@ from .grpc_types import EventType
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def dex_event_json_dumps(obj: Any, **kwargs: Any) -> str:
|
|
14
|
-
return json.dumps(obj, default=str, **kwargs)
|
|
14
|
+
return json.dumps(_precise_metadata(dex_event_to_jsonable(obj)), default=str, **kwargs)
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def dex_event_to_jsonable(ev: Any) -> Any:
|
|
@@ -39,3 +39,17 @@ def format_dex_event_json(ev: Any, *, indent: int = 2, ensure_ascii: bool = Fals
|
|
|
39
39
|
indent=indent,
|
|
40
40
|
ensure_ascii=ensure_ascii,
|
|
41
41
|
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _precise_metadata(obj):
|
|
45
|
+
"""Match Node exact metadata JSON without changing floating latency metrics."""
|
|
46
|
+
if isinstance(obj,dict):
|
|
47
|
+
result={k:_precise_metadata(v) for k,v in obj.items()}
|
|
48
|
+
if "signature" in obj and all(k in obj for k in ("slot","tx_index","block_time_us","grpc_recv_us")):
|
|
49
|
+
from .grpc_types import EventMetadata
|
|
50
|
+
validated=EventMetadata(**{k:obj[k] for k in ("signature","slot","tx_index","block_time_us","grpc_recv_us")})
|
|
51
|
+
for k in ("slot","tx_index","block_time_us","grpc_recv_us"):result[k]=str(getattr(validated,k))
|
|
52
|
+
return result
|
|
53
|
+
if isinstance(obj,(tuple,list)):return [_precise_metadata(v) for v in obj]
|
|
54
|
+
if is_dataclass(obj):return _precise_metadata(asdict(obj))
|
|
55
|
+
return obj
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Native liquidity identity decoders and immutable raw subscription snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import base58
|
|
6
|
+
from .accounts import AccountData
|
|
7
|
+
from .grpc_types import EventMetadata
|
|
8
|
+
from .transaction_route import PROGRAMS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class RawAccountSnapshotEvent:
|
|
13
|
+
metadata: EventMetadata
|
|
14
|
+
account: AccountData
|
|
15
|
+
write_version: int
|
|
16
|
+
is_startup: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class LiquidityAccountSnapshotEvent:
|
|
21
|
+
metadata: EventMetadata
|
|
22
|
+
pubkey: str
|
|
23
|
+
owner: str
|
|
24
|
+
kind: dict | str
|
|
25
|
+
data: bytes
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_liquidity_account(account: AccountData, metadata: EventMetadata):
|
|
29
|
+
if account.lamports <= 0 or account.executable:
|
|
30
|
+
return None
|
|
31
|
+
d, owner = bytes(account.data), account.owner
|
|
32
|
+
disc = d[:8]
|
|
33
|
+
|
|
34
|
+
def key(o):
|
|
35
|
+
return base58.b58encode(d[o : o + 32]).decode()
|
|
36
|
+
|
|
37
|
+
def number(o, n, signed=False):
|
|
38
|
+
return int.from_bytes(d[o : o + n], "little", signed=signed)
|
|
39
|
+
|
|
40
|
+
kind = None
|
|
41
|
+
if owner == PROGRAMS["LaunchLab"]:
|
|
42
|
+
if disc == bytes([247, 237, 227, 245, 215, 195, 222, 70]) and len(d) >= 429:
|
|
43
|
+
kind = {
|
|
44
|
+
"LaunchLabPool": dict(
|
|
45
|
+
base_mint=key(205),
|
|
46
|
+
quote_mint=key(237),
|
|
47
|
+
global_config=key(141),
|
|
48
|
+
platform_config=key(173),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
elif disc == bytes([149, 8, 156, 202, 160, 252, 176, 217]) and len(d) >= 35:
|
|
52
|
+
kind = "LaunchLabGlobalConfig"
|
|
53
|
+
elif disc == bytes([160, 78, 128, 0, 248, 83, 230, 160]) and len(d) >= 728:
|
|
54
|
+
kind = "LaunchLabPlatformConfig"
|
|
55
|
+
elif owner == PROGRAMS["MeteoraDlmm"]:
|
|
56
|
+
if disc == bytes([33, 11, 49, 98, 181, 101, 177, 13]) and len(d) >= 904:
|
|
57
|
+
kind = {
|
|
58
|
+
"DlmmPool": dict(
|
|
59
|
+
token_x_mint=key(88),
|
|
60
|
+
token_y_mint=key(120),
|
|
61
|
+
active_id=number(76, 4, True),
|
|
62
|
+
bin_step=number(80, 2),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
elif disc == bytes([92, 142, 92, 220, 5, 148, 70, 181]) and len(d) >= 10136:
|
|
66
|
+
kind = {"DlmmBinArray": dict(pool=key(24), index=number(8, 8, True))}
|
|
67
|
+
elif disc == bytes([80, 111, 124, 113, 55, 237, 18, 5]) and len(d) >= 1576:
|
|
68
|
+
kind = {"DlmmBitmap": dict(pool=key(8))}
|
|
69
|
+
elif owner == PROGRAMS["OrcaWhirlpool"]:
|
|
70
|
+
if disc == bytes([17, 216, 246, 142, 225, 199, 218, 56]) and len(d) >= 148:
|
|
71
|
+
bitmap, offset = number(44, 16), 60
|
|
72
|
+
if bitmap >> 88:
|
|
73
|
+
return None
|
|
74
|
+
for i in range(88):
|
|
75
|
+
if (
|
|
76
|
+
offset >= len(d)
|
|
77
|
+
or d[offset] not in (0, 1)
|
|
78
|
+
or d[offset] != (bitmap >> i) & 1
|
|
79
|
+
):
|
|
80
|
+
return None
|
|
81
|
+
offset += 1 + (112 if d[offset] else 0)
|
|
82
|
+
if offset > len(d):
|
|
83
|
+
return None
|
|
84
|
+
kind = {
|
|
85
|
+
"OrcaDynamicTickArray": dict(
|
|
86
|
+
pool=key(12),
|
|
87
|
+
start_tick_index=number(8, 4, True),
|
|
88
|
+
tick_bitmap=bitmap,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
elif disc == bytes([139, 194, 131, 179, 140, 179, 229, 244]) and len(d) >= 254:
|
|
92
|
+
kind = {"OrcaAdaptiveOracle": dict(pool=key(8))}
|
|
93
|
+
elif (
|
|
94
|
+
owner == PROGRAMS["RaydiumClmm"]
|
|
95
|
+
and disc == bytes([60, 150, 36, 219, 97, 128, 139, 153])
|
|
96
|
+
and len(d) >= 1832
|
|
97
|
+
):
|
|
98
|
+
kind = {"ClmmBitmap": dict(pool=key(8))}
|
|
99
|
+
if kind is None:
|
|
100
|
+
return None
|
|
101
|
+
return LiquidityAccountSnapshotEvent(metadata, account.pubkey, owner, kind, d)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Explicit simulateTransaction evidence adapter; no RPC or wire reserialization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
import struct
|
|
6
|
+
import base58
|
|
7
|
+
from .wire_transaction import decode_wire_transaction
|
|
8
|
+
from .transaction_route import analyze_rpc_transaction_routes, TOKEN, TOKEN_2022, ZERO
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _unsigned(value, bits):
|
|
12
|
+
if type(value) not in (str, int) or not re.fullmatch(r"[0-9]+", str(value)):
|
|
13
|
+
raise ValueError("Invalid simulation integer")
|
|
14
|
+
n = int(value)
|
|
15
|
+
if not 0 <= n < 1 << bits:
|
|
16
|
+
raise ValueError("Simulation integer outside range")
|
|
17
|
+
return n
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def analyze_simulation_routes(wire, response, graduated_pools=()):
|
|
21
|
+
"""Original wire plus simulateTransaction response. SPL transfers and ATA setup.
|
|
22
|
+
|
|
23
|
+
Unsupported parsed CPI fail explicitly. Raw/compiled CPI are preserved.
|
|
24
|
+
V0 with ALT requires the existing compiled route API and loadedAddresses.
|
|
25
|
+
"""
|
|
26
|
+
tx, _ = decode_wire_transaction(wire)
|
|
27
|
+
if tx["message"]["addressTableLookups"]:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"V0 ALT addresses unavailable; use compiled route input with loadedAddresses"
|
|
30
|
+
)
|
|
31
|
+
if not isinstance(response, dict) or not isinstance(response.get("result"), dict):
|
|
32
|
+
raise ValueError("Simulation response missing execution metadata")
|
|
33
|
+
value = response["result"].get("value")
|
|
34
|
+
if (
|
|
35
|
+
response.get("error")
|
|
36
|
+
or not isinstance(value, dict)
|
|
37
|
+
or "err" not in value
|
|
38
|
+
or "innerInstructions" not in value
|
|
39
|
+
):
|
|
40
|
+
raise ValueError("Simulation response missing execution metadata")
|
|
41
|
+
groups = value["innerInstructions"]
|
|
42
|
+
if groups is None:
|
|
43
|
+
groups = []
|
|
44
|
+
if not isinstance(groups, list):
|
|
45
|
+
raise ValueError("Invalid simulation inner instructions")
|
|
46
|
+
keys = tx["message"]["accountKeys"]
|
|
47
|
+
|
|
48
|
+
def index(key):
|
|
49
|
+
if not isinstance(key, str) or key not in keys:
|
|
50
|
+
raise ValueError("Simulation account absent from transaction")
|
|
51
|
+
return keys.index(key)
|
|
52
|
+
|
|
53
|
+
def valid_index(n):
|
|
54
|
+
if type(n) is not int or not 0 <= n < len(keys):
|
|
55
|
+
raise ValueError("Invalid simulation account index")
|
|
56
|
+
return n
|
|
57
|
+
|
|
58
|
+
def instruction(ix):
|
|
59
|
+
if not isinstance(ix, dict):
|
|
60
|
+
raise ValueError("Invalid simulation instruction")
|
|
61
|
+
height = ix.get("stackHeight")
|
|
62
|
+
if height is not None and (type(height) is not int or height < 2):
|
|
63
|
+
raise ValueError("Invalid simulation stack height")
|
|
64
|
+
if "programIdIndex" in ix:
|
|
65
|
+
if (
|
|
66
|
+
"parsed" in ix
|
|
67
|
+
or not isinstance(ix.get("data"), str)
|
|
68
|
+
or not isinstance(ix.get("accounts"), list)
|
|
69
|
+
):
|
|
70
|
+
raise ValueError("Invalid compiled simulation instruction")
|
|
71
|
+
base58.b58decode(ix["data"])
|
|
72
|
+
return dict(
|
|
73
|
+
programIdIndex=valid_index(ix["programIdIndex"]),
|
|
74
|
+
accounts=[valid_index(i) for i in ix["accounts"]],
|
|
75
|
+
data=ix["data"],
|
|
76
|
+
stackHeight=height,
|
|
77
|
+
)
|
|
78
|
+
program = index(ix.get("programId"))
|
|
79
|
+
if "parsed" not in ix:
|
|
80
|
+
if not isinstance(ix.get("data"), str) or not isinstance(
|
|
81
|
+
ix.get("accounts"), list
|
|
82
|
+
):
|
|
83
|
+
raise ValueError("Invalid raw simulation instruction")
|
|
84
|
+
base58.b58decode(ix["data"])
|
|
85
|
+
return dict(
|
|
86
|
+
programIdIndex=program,
|
|
87
|
+
accounts=[index(k) for k in ix["accounts"]],
|
|
88
|
+
data=ix["data"],
|
|
89
|
+
stackHeight=height,
|
|
90
|
+
)
|
|
91
|
+
parsed = ix["parsed"]
|
|
92
|
+
if not isinstance(parsed, dict) or not isinstance(parsed.get("info"), dict):
|
|
93
|
+
raise ValueError("Unsupported parsed simulation instruction")
|
|
94
|
+
setup = _account_setup(ix["programId"], parsed.get("type"), parsed["info"])
|
|
95
|
+
if setup is not None:
|
|
96
|
+
accounts, data = setup
|
|
97
|
+
return dict(
|
|
98
|
+
programIdIndex=program,
|
|
99
|
+
accounts=[index(k) for k in accounts],
|
|
100
|
+
data=base58.b58encode(data).decode(),
|
|
101
|
+
stackHeight=height,
|
|
102
|
+
)
|
|
103
|
+
if ix["programId"] not in (TOKEN, TOKEN_2022):
|
|
104
|
+
raise ValueError("Unsupported parsed simulation program")
|
|
105
|
+
kind, info = ix["parsed"].get("type"), ix["parsed"].get("info")
|
|
106
|
+
if not isinstance(info, dict) or kind not in (
|
|
107
|
+
"transfer",
|
|
108
|
+
"transferChecked",
|
|
109
|
+
"transferCheckedWithFee",
|
|
110
|
+
):
|
|
111
|
+
raise ValueError("Unsupported parsed simulation instruction")
|
|
112
|
+
authority = info.get("authority", info.get("multisigAuthority"))
|
|
113
|
+
signers = info.get("signers", [])
|
|
114
|
+
if not isinstance(signers, list):
|
|
115
|
+
raise ValueError("Invalid simulation signers")
|
|
116
|
+
if kind == "transfer":
|
|
117
|
+
accounts = [
|
|
118
|
+
info.get("source"),
|
|
119
|
+
info.get("destination"),
|
|
120
|
+
authority,
|
|
121
|
+
] + signers
|
|
122
|
+
data = struct.pack("<BQ", 3, _unsigned(info.get("amount"), 64))
|
|
123
|
+
else:
|
|
124
|
+
with_fee = kind == "transferCheckedWithFee"
|
|
125
|
+
if with_fee and ix["programId"] != TOKEN_2022:
|
|
126
|
+
raise ValueError("Transfer fee requires Token-2022")
|
|
127
|
+
amount = info.get("tokenAmount")
|
|
128
|
+
if not isinstance(amount, dict):
|
|
129
|
+
raise ValueError("Invalid simulation token amount")
|
|
130
|
+
n, decimals = _unsigned(amount.get("amount"), 64), _unsigned(
|
|
131
|
+
amount.get("decimals"), 8
|
|
132
|
+
)
|
|
133
|
+
accounts = [
|
|
134
|
+
info.get("source"),
|
|
135
|
+
info.get("mint"),
|
|
136
|
+
info.get("destination"),
|
|
137
|
+
authority,
|
|
138
|
+
] + signers
|
|
139
|
+
data = (
|
|
140
|
+
struct.pack(
|
|
141
|
+
"<BBQBQ", 26, 1, n, decimals, _unsigned(info.get("feeAmount"), 64)
|
|
142
|
+
)
|
|
143
|
+
if with_fee
|
|
144
|
+
else struct.pack("<BQB", 12, n, decimals)
|
|
145
|
+
)
|
|
146
|
+
return dict(
|
|
147
|
+
programIdIndex=program,
|
|
148
|
+
accounts=[index(k) for k in accounts],
|
|
149
|
+
data=base58.b58encode(data).decode(),
|
|
150
|
+
stackHeight=height,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
converted, seen = [], set()
|
|
154
|
+
for group in groups:
|
|
155
|
+
if not isinstance(group, dict):
|
|
156
|
+
raise ValueError("Invalid simulation instruction group")
|
|
157
|
+
outer = group.get("index")
|
|
158
|
+
if (
|
|
159
|
+
type(outer) is not int
|
|
160
|
+
or not 0 <= outer < len(tx["message"]["instructions"])
|
|
161
|
+
or outer in seen
|
|
162
|
+
or not isinstance(group.get("instructions"), list)
|
|
163
|
+
):
|
|
164
|
+
raise ValueError("Invalid simulation instruction group")
|
|
165
|
+
seen.add(outer)
|
|
166
|
+
converted.append(
|
|
167
|
+
dict(
|
|
168
|
+
index=outer,
|
|
169
|
+
instructions=[instruction(ix) for ix in group["instructions"]],
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
return analyze_rpc_transaction_routes(
|
|
173
|
+
dict(transaction=tx, meta={**value, "innerInstructions": converted}),
|
|
174
|
+
graduated_pools,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _account_setup(program, kind, info):
|
|
179
|
+
def pubkey(key):
|
|
180
|
+
if not isinstance(key, str):
|
|
181
|
+
raise ValueError("Invalid simulation public key")
|
|
182
|
+
data = base58.b58decode(key)
|
|
183
|
+
if len(data) != 32:
|
|
184
|
+
raise ValueError("Invalid simulation public key")
|
|
185
|
+
return data
|
|
186
|
+
|
|
187
|
+
if program == ZERO and kind == "createAccount":
|
|
188
|
+
return [info.get("source"), info.get("newAccount")], struct.pack(
|
|
189
|
+
"<IQQ",
|
|
190
|
+
0,
|
|
191
|
+
_unsigned(info.get("lamports"), 64),
|
|
192
|
+
_unsigned(info.get("space"), 64),
|
|
193
|
+
) + pubkey(info.get("owner"))
|
|
194
|
+
if program == ZERO and kind in ("transfer", "allocate"):
|
|
195
|
+
return ([info.get("source"), info.get("destination")] if kind == "transfer" else [info.get("account")]), struct.pack("<IQ", 2 if kind == "transfer" else 8, _unsigned(info.get("lamports" if kind == "transfer" else "space"), 64))
|
|
196
|
+
if program == ZERO and kind == "assign":
|
|
197
|
+
return [info.get("account")], struct.pack("<I", 1) + pubkey(info.get("owner"))
|
|
198
|
+
if program not in (TOKEN, TOKEN_2022):
|
|
199
|
+
return None
|
|
200
|
+
if kind in ("mintTo", "mintToChecked", "burn", "burnChecked"):
|
|
201
|
+
minting = kind.startswith("mintTo")
|
|
202
|
+
checked = kind.endswith("Checked")
|
|
203
|
+
authority_key = "mintAuthority" if minting else "authority"
|
|
204
|
+
multisig_key = "multisigMintAuthority" if minting else "multisigAuthority"
|
|
205
|
+
authority = info.get(authority_key)
|
|
206
|
+
if authority is None:
|
|
207
|
+
authority = info.get(multisig_key)
|
|
208
|
+
signers = info.get("signers", [])
|
|
209
|
+
if not isinstance(signers, list):
|
|
210
|
+
raise ValueError("Invalid simulation signers")
|
|
211
|
+
amount = info.get("tokenAmount") if checked else info
|
|
212
|
+
if not isinstance(amount, dict):
|
|
213
|
+
raise ValueError("Invalid simulation token amount")
|
|
214
|
+
data = struct.pack("<BQ", (14 if minting else 15) if checked else (7 if minting else 8),
|
|
215
|
+
_unsigned(amount.get("amount"), 64))
|
|
216
|
+
if checked:
|
|
217
|
+
data += bytes([_unsigned(amount.get("decimals"), 8)])
|
|
218
|
+
accounts = [info.get("mint"), info.get("account")] if minting else [info.get("account"), info.get("mint")]
|
|
219
|
+
return accounts + [authority] + signers, data
|
|
220
|
+
if kind == "initializeImmutableOwner":
|
|
221
|
+
return [info.get("account")], bytes([22])
|
|
222
|
+
if kind == "initializeAccount3":
|
|
223
|
+
return [info.get("account"), info.get("mint")], bytes([18]) + pubkey(
|
|
224
|
+
info.get("owner")
|
|
225
|
+
)
|
|
226
|
+
if kind == "getAccountDataSize":
|
|
227
|
+
extensions = info.get("extensionTypes")
|
|
228
|
+
if not isinstance(extensions, list):
|
|
229
|
+
raise ValueError("Invalid simulation extension types")
|
|
230
|
+
if any(ext != "immutableOwner" for ext in extensions):
|
|
231
|
+
raise ValueError("Unsupported simulation extension type")
|
|
232
|
+
return [info.get("mint")], bytes([21]) + struct.pack("<H", 7) * len(extensions)
|
|
233
|
+
return None
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Caller-owned, replay-safe provenance for successfully migrated StonkFun pools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
import base64
|
|
6
|
+
import base58
|
|
7
|
+
from .transaction_route import (
|
|
8
|
+
PROGRAMS,
|
|
9
|
+
ZERO,
|
|
10
|
+
stonkfun_mode_from_platform_config,
|
|
11
|
+
analyze_rpc_transaction_routes,
|
|
12
|
+
)
|
|
13
|
+
from .wire_transaction import decode_wire_transaction
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class StonkFunGraduatedPool:
|
|
18
|
+
curve_pool: str
|
|
19
|
+
pool: str
|
|
20
|
+
base_mint: str
|
|
21
|
+
quote_mint: str
|
|
22
|
+
platform_config: str
|
|
23
|
+
migration_signature: str
|
|
24
|
+
migration_slot: int
|
|
25
|
+
|
|
26
|
+
def mode(self):
|
|
27
|
+
return stonkfun_mode_from_platform_config(self.platform_config)
|
|
28
|
+
|
|
29
|
+
def validate(self):
|
|
30
|
+
if (
|
|
31
|
+
self.mode() is None
|
|
32
|
+
or self.curve_pool == self.pool
|
|
33
|
+
or self.base_mint == self.quote_mint
|
|
34
|
+
):
|
|
35
|
+
raise ValueError("Invalid StonkFun registry identity")
|
|
36
|
+
for key in (self.curve_pool, self.pool, self.base_mint, self.quote_mint):
|
|
37
|
+
if key == ZERO or len(base58.b58decode(key)) != 32:
|
|
38
|
+
raise ValueError("Invalid StonkFun registry key")
|
|
39
|
+
if (
|
|
40
|
+
len(base58.b58decode(self.migration_signature)) != 64
|
|
41
|
+
or type(self.migration_slot) is not int
|
|
42
|
+
or not 0 <= self.migration_slot < 1 << 64
|
|
43
|
+
):
|
|
44
|
+
raise ValueError("Invalid migration provenance")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StonkFunPoolRegistry:
|
|
48
|
+
def __init__(self, pools=()):
|
|
49
|
+
self._pools = {}
|
|
50
|
+
for entry in pools:
|
|
51
|
+
entry.validate()
|
|
52
|
+
if entry.pool in self._pools:
|
|
53
|
+
raise ValueError("Duplicate StonkFun registry entry")
|
|
54
|
+
self._pools[entry.pool] = entry
|
|
55
|
+
|
|
56
|
+
def get(self, pool):
|
|
57
|
+
return self._pools.get(pool)
|
|
58
|
+
|
|
59
|
+
def pools_for_base_mint(self, mint):
|
|
60
|
+
return [
|
|
61
|
+
self._pools[k]
|
|
62
|
+
for k in sorted(self._pools)
|
|
63
|
+
if self._pools[k].base_mint == mint
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def verified_cpmm_pools(self):
|
|
67
|
+
return sorted(self._pools)
|
|
68
|
+
|
|
69
|
+
def to_dict(self):
|
|
70
|
+
return {"pools": [asdict(self._pools[k]) for k in sorted(self._pools)]}
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_dict(cls, data):
|
|
74
|
+
return cls(StonkFunGraduatedPool(**p) for p in data["pools"])
|
|
75
|
+
|
|
76
|
+
def observe_migrations(self, entries, succeeded):
|
|
77
|
+
if not succeeded:
|
|
78
|
+
return 0
|
|
79
|
+
pending = {}
|
|
80
|
+
for entry in entries:
|
|
81
|
+
entry.validate()
|
|
82
|
+
prior = pending.get(entry.pool) or self._pools.get(entry.pool)
|
|
83
|
+
if prior is not None and prior != entry:
|
|
84
|
+
raise ValueError("Conflicting migration provenance")
|
|
85
|
+
pending[entry.pool] = entry
|
|
86
|
+
count = sum(k not in self._pools for k in pending)
|
|
87
|
+
self._pools.update(pending)
|
|
88
|
+
return count
|
|
89
|
+
|
|
90
|
+
def observe_rpc_transaction(self, transaction):
|
|
91
|
+
tx = transaction.get("result", transaction)
|
|
92
|
+
if not isinstance(tx.get("meta"), dict):
|
|
93
|
+
raise ValueError("Transaction metadata missing")
|
|
94
|
+
if "err" not in tx["meta"]:
|
|
95
|
+
raise ValueError("Transaction execution status missing")
|
|
96
|
+
if tx["meta"]["err"] is not None:
|
|
97
|
+
return 0
|
|
98
|
+
analyze_rpc_transaction_routes(transaction)
|
|
99
|
+
body = tx["transaction"]
|
|
100
|
+
if isinstance(body, list):
|
|
101
|
+
if body[1] != "base64":
|
|
102
|
+
raise ValueError("Expected base64 encoding")
|
|
103
|
+
body, _ = decode_wire_transaction(base64.b64decode(body[0], validate=True))
|
|
104
|
+
message, loaded = body["message"], tx["meta"].get("loadedAddresses") or {}
|
|
105
|
+
keys = (
|
|
106
|
+
[k if isinstance(k, str) else k["pubkey"] for k in message["accountKeys"]]
|
|
107
|
+
+ loaded.get("writable", [])
|
|
108
|
+
+ loaded.get("readonly", [])
|
|
109
|
+
)
|
|
110
|
+
instructions = list(message["instructions"])
|
|
111
|
+
for group in tx["meta"].get("innerInstructions") or []:
|
|
112
|
+
instructions.extend(group["instructions"])
|
|
113
|
+
entries = []
|
|
114
|
+
for ix in instructions:
|
|
115
|
+
data = (
|
|
116
|
+
base58.b58decode(ix["data"])
|
|
117
|
+
if isinstance(ix["data"], str)
|
|
118
|
+
else bytes(ix["data"])
|
|
119
|
+
)
|
|
120
|
+
if (
|
|
121
|
+
keys[ix["programIdIndex"]] != PROGRAMS["LaunchLab"]
|
|
122
|
+
or data[:8] != bytes([136, 92, 200, 103, 28, 218, 144, 140])
|
|
123
|
+
or len(ix["accounts"]) < 28
|
|
124
|
+
):
|
|
125
|
+
continue
|
|
126
|
+
a = [keys[k] if 0 <= k < len(keys) else ZERO for k in ix["accounts"]]
|
|
127
|
+
if (
|
|
128
|
+
a[4] != PROGRAMS["RaydiumCpmm"]
|
|
129
|
+
or stonkfun_mode_from_platform_config(a[3]) is None
|
|
130
|
+
):
|
|
131
|
+
continue
|
|
132
|
+
if ZERO in (a[17], a[5], a[1], a[2]) or a[17] == a[5] or a[1] == a[2]:
|
|
133
|
+
continue
|
|
134
|
+
entries.append(
|
|
135
|
+
StonkFunGraduatedPool(
|
|
136
|
+
a[17], a[5], a[1], a[2], a[3], body["signatures"][0], tx["slot"]
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return self.observe_migrations(entries, True)
|