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.
- sol_parser/__init__.py +34 -0
- sol_parser/account_dispatcher.py +98 -4
- sol_parser/account_fillers/meteora.py +10 -2
- sol_parser/account_fillers/raydium.py +9 -4
- sol_parser/account_fillers/raydium_launchlab.py +54 -0
- sol_parser/accounts/__init__.py +52 -2
- sol_parser/block_meta.py +29 -0
- sol_parser/dex_parsers.py +662 -69
- sol_parser/entries_decode.py +6 -80
- sol_parser/event_type_parity.py +4 -0
- sol_parser/event_types.py +269 -3
- sol_parser/grpc/subscribe_builder.py +8 -0
- sol_parser/grpc_client.py +114 -20
- sol_parser/grpc_instruction_parser.py +85 -26
- sol_parser/grpc_types.py +31 -0
- sol_parser/inner_instruction_parser.py +5 -7
- sol_parser/instructions.py +259 -53
- sol_parser/json_util.py +15 -1
- sol_parser/liquidity_snapshot.py +101 -0
- sol_parser/log_instr_dedup.py +56 -12
- sol_parser/merger.py +73 -85
- sol_parser/parser.py +11 -0
- sol_parser/pumpfun_fee_enrich.py +2 -0
- sol_parser/rpc_parser.py +30 -4
- 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.7.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/METADATA +44 -9
- {sol_parser_sdk_python-0.5.7.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/RECORD +32 -26
- {sol_parser_sdk_python-0.5.7.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/WHEEL +1 -1
- {sol_parser_sdk_python-0.5.7.dist-info → sol_parser_sdk_python-0.5.9.dist-info}/entry_points.txt +0 -0
|
@@ -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)
|