sol-parser-sdk-python 0.5.7__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.
@@ -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)