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/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
@@ -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
- return _get_account_safe(accounts, index - shift if index >= 5 else index)
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 == 9 else 0,
1725
- "minimum_amount_out": second if instr_type == 9 else 0,
1726
- "max_amount_in": first if instr_type == 11 else 0,
1727
- "amount_out": second if instr_type == 11 else 0,
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) < 139:
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
- return None
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)