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
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"""Native instruction-level route analysis, aligned with Rust parser 0.7.7.
|
|
2
|
+
|
|
3
|
+
Accepts JSON-encoded RPC transactions. This is execution evidence, not a quote
|
|
4
|
+
or automatic route selector. Unknown invocations and failed intent remain visible.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from dataclasses import dataclass, field, asdict
|
|
9
|
+
from typing import Any, Iterable, Optional
|
|
10
|
+
import base58
|
|
11
|
+
import base64
|
|
12
|
+
from .wire_transaction import decode_wire_transaction
|
|
13
|
+
|
|
14
|
+
ZERO = "11111111111111111111111111111111"
|
|
15
|
+
TOKEN = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
|
16
|
+
TOKEN_2022 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
17
|
+
WSOL = "So11111111111111111111111111111111111111112"
|
|
18
|
+
PROGRAMS = {
|
|
19
|
+
'PumpFun': '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
|
|
20
|
+
"MeteoraDammV2": "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG",
|
|
21
|
+
"RaydiumClmm": "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK",
|
|
22
|
+
"RaydiumCpmm": "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C",
|
|
23
|
+
"RaydiumAmmV4": "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8",
|
|
24
|
+
"LaunchLab": "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj",
|
|
25
|
+
"OrcaWhirlpool": "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc",
|
|
26
|
+
"MeteoraDlmm": "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo",
|
|
27
|
+
"PumpSwap": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
|
|
28
|
+
}
|
|
29
|
+
STANDARD = "4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7"
|
|
30
|
+
REWARD = "6BwHHDg3u1854jC8PDLXvR4spTcLNaoBxLJNGC4nTESt"
|
|
31
|
+
U64_MAX = (1 << 64) - 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def stonkfun_mode_from_platform_config(key: str) -> Optional[str]:
|
|
35
|
+
return "Standard" if key == STANDARD else "Reward" if key == REWARD else None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class InstructionPosition:
|
|
40
|
+
outer_index: int
|
|
41
|
+
inner_index: Optional[int]
|
|
42
|
+
stack_height: Optional[int]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class RouteSwapLeg:
|
|
47
|
+
position: InstructionPosition
|
|
48
|
+
program: str
|
|
49
|
+
protocol: str
|
|
50
|
+
pool: str
|
|
51
|
+
trader: str
|
|
52
|
+
input_account: str
|
|
53
|
+
output_account: str
|
|
54
|
+
input_mint: Optional[str]
|
|
55
|
+
output_mint: Optional[str]
|
|
56
|
+
amount_specified_is_input: bool
|
|
57
|
+
specified_amount: int
|
|
58
|
+
other_amount_threshold: int
|
|
59
|
+
actual_input_amount: Optional[int] = None
|
|
60
|
+
actual_output_amount: Optional[int] = None
|
|
61
|
+
stonkfun_mode: Optional[str] = None
|
|
62
|
+
stonkfun_graduated: bool = False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class RouteTokenTransfer:
|
|
67
|
+
position: InstructionPosition
|
|
68
|
+
program: str
|
|
69
|
+
source: str
|
|
70
|
+
destination: str
|
|
71
|
+
mint: Optional[str]
|
|
72
|
+
amount: int
|
|
73
|
+
withheld_fee: Optional[int]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class RouteUnknownInvocation:
|
|
78
|
+
position: InstructionPosition
|
|
79
|
+
program: str
|
|
80
|
+
has_token_transfers: bool
|
|
81
|
+
has_known_swap_descendants: bool
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class RouteNativeTokenAction:
|
|
86
|
+
position: InstructionPosition
|
|
87
|
+
account: str
|
|
88
|
+
action: Any
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class TransactionRoute:
|
|
93
|
+
signature: str
|
|
94
|
+
succeeded: bool
|
|
95
|
+
legs: list[RouteSwapLeg] = field(default_factory=list)
|
|
96
|
+
transfers: list[RouteTokenTransfer] = field(default_factory=list)
|
|
97
|
+
native_token_actions: list[RouteNativeTokenAction] = field(default_factory=list)
|
|
98
|
+
unknown_invocations: list[RouteUnknownInvocation] = field(default_factory=list)
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict:
|
|
101
|
+
return asdict(self)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class _Invocation:
|
|
106
|
+
position: InstructionPosition
|
|
107
|
+
program: str
|
|
108
|
+
accounts: list[str]
|
|
109
|
+
data: bytes
|
|
110
|
+
|
|
111
|
+
def a(self, i: int) -> str:
|
|
112
|
+
return self.accounts[i] if 0 <= i < len(self.accounts) else ZERO
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _disc(values: list[int]) -> bytes:
|
|
116
|
+
return bytes(values)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _pumpfun_native_debit(ix, children, leg):
|
|
120
|
+
# Only immediate protocol payments count; exclude rent and wrapping.
|
|
121
|
+
if leg.protocol!='PumpFun' or len(ix.accounts)<17 or ix.a(2)!=WSOL or leg.input_account!=leg.trader:
|
|
122
|
+
return None
|
|
123
|
+
recipients={ix.a(i) for i in (6,8,10,16)}
|
|
124
|
+
depth=ix.position.stack_height
|
|
125
|
+
if depth is None:return None
|
|
126
|
+
total=0;paid_pool=False
|
|
127
|
+
for child in children:
|
|
128
|
+
if child.program!=ZERO or len(child.data)!=12 or child.data[:4]!=bytes([2,0,0,0]) or len(child.accounts)<2 or child.a(0)!=leg.trader:
|
|
129
|
+
continue
|
|
130
|
+
if child.position.stack_height!=depth+1:return None
|
|
131
|
+
if child.a(1) not in recipients:return None
|
|
132
|
+
paid_pool=paid_pool or child.a(1)==leg.pool
|
|
133
|
+
total+=int.from_bytes(child.data[4:12],'little')
|
|
134
|
+
if total>U64_MAX:return None
|
|
135
|
+
return total if paid_pool else None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
SWAP = _disc([248, 198, 158, 145, 225, 117, 135, 200])
|
|
139
|
+
SWAP_V2 = _disc([43, 4, 237, 11, 26, 201, 30, 98])
|
|
140
|
+
CP_IN = _disc([143, 190, 90, 218, 196, 30, 51, 222])
|
|
141
|
+
CP_OUT = _disc([55, 217, 98, 86, 163, 74, 180, 173])
|
|
142
|
+
DL_IN = {SWAP, _disc([65, 75, 63, 76, 235, 91, 91, 136])}
|
|
143
|
+
DL_OUT = {
|
|
144
|
+
_disc([250, 73, 101, 33, 38, 207, 75, 184]),
|
|
145
|
+
_disc([43, 215, 247, 132, 137, 60, 243, 81]),
|
|
146
|
+
}
|
|
147
|
+
LAB = {
|
|
148
|
+
_disc([250, 234, 13, 123, 213, 156, 19, 236]): (True, True),
|
|
149
|
+
_disc([24, 211, 116, 40, 105, 3, 153, 56]): (True, False),
|
|
150
|
+
_disc([149, 39, 222, 155, 211, 124, 152, 26]): (False, True),
|
|
151
|
+
_disc([95, 200, 71, 34, 8, 9, 11, 166]): (False, False),
|
|
152
|
+
}
|
|
153
|
+
PUMP = {
|
|
154
|
+
_disc([198, 46, 21, 82, 180, 217, 232, 112]): (True, True),
|
|
155
|
+
_disc([102, 6, 61, 18, 1, 218, 235, 234]): (True, False),
|
|
156
|
+
_disc([51, 230, 133, 164, 1, 127, 131, 173]): (False, True),
|
|
157
|
+
}
|
|
158
|
+
CURVE_V2={
|
|
159
|
+
_disc([194,171,28,70,104,77,91,47]):(True,True),
|
|
160
|
+
_disc([184,23,238,97,103,197,211,61]):(True,False),
|
|
161
|
+
_disc([93,246,130,60,231,233,64,178]):(False,True),
|
|
162
|
+
}
|
|
163
|
+
CURVE_LEGACY={
|
|
164
|
+
_disc([56,252,116,8,158,223,205,95]):(True,True),
|
|
165
|
+
_disc([102,6,61,18,1,218,235,234]):(True,False),
|
|
166
|
+
_disc([51,230,133,164,1,127,131,173]):(False,True),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _swap(
|
|
171
|
+
ix: _Invocation, mints: dict[str, str], graduated: set[str]
|
|
172
|
+
) -> Optional[RouteSwapLeg]:
|
|
173
|
+
d, a, n = ix.data, ix.a, len(ix.accounts)
|
|
174
|
+
disc, pair, mode = d[:8], None, None
|
|
175
|
+
protocol = next((p for p, key in PROGRAMS.items() if key == ix.program), None)
|
|
176
|
+
exact, offset, invert = True, 8, False
|
|
177
|
+
if (
|
|
178
|
+
protocol == "RaydiumClmm"
|
|
179
|
+
and disc in (SWAP, SWAP_V2)
|
|
180
|
+
and n >= 10
|
|
181
|
+
and len(d) >= 41
|
|
182
|
+
):
|
|
183
|
+
pool, user, source, dest = a(2), a(0), a(3), a(4)
|
|
184
|
+
exact = d[40] != 0
|
|
185
|
+
if disc == SWAP_V2 and n >= 13:
|
|
186
|
+
pair = (a(11), a(12))
|
|
187
|
+
elif protocol == "OrcaWhirlpool" and disc in (SWAP, SWAP_V2) and len(d) >= 42:
|
|
188
|
+
v2, direction = disc == SWAP_V2, d[41] != 0
|
|
189
|
+
if n < (15 if v2 else 11):
|
|
190
|
+
return None
|
|
191
|
+
pool, user = a(4 if v2 else 2), a(3 if v2 else 1)
|
|
192
|
+
x, y = (7, 9) if v2 else (3, 5)
|
|
193
|
+
source, dest, exact = (
|
|
194
|
+
a(x if direction else y),
|
|
195
|
+
a(y if direction else x),
|
|
196
|
+
d[40] != 0,
|
|
197
|
+
)
|
|
198
|
+
if v2:
|
|
199
|
+
pair = (a(5), a(6)) if direction else (a(6), a(5))
|
|
200
|
+
elif protocol == "RaydiumCpmm" and disc in (CP_IN, CP_OUT) and n >= 13:
|
|
201
|
+
pool, user, source, dest = a(3), a(0), a(4), a(5)
|
|
202
|
+
pair, exact = (a(10), a(11)), disc == CP_IN
|
|
203
|
+
invert = not exact
|
|
204
|
+
elif protocol == "MeteoraDammV2" and disc in (SWAP, _disc([65,75,63,76,235,91,91,136])) and n >= 11:
|
|
205
|
+
if len(d) < 24:
|
|
206
|
+
return None
|
|
207
|
+
if disc != SWAP:
|
|
208
|
+
if len(d) < 25 or d[24] > 2:
|
|
209
|
+
return None
|
|
210
|
+
exact = d[24] != 2
|
|
211
|
+
pool, user, source, dest = a(1), a(8), a(2), a(3)
|
|
212
|
+
# Pool mint order does not determine direction; use user-account evidence.
|
|
213
|
+
elif protocol == "MeteoraDlmm" and disc in DL_IN | DL_OUT and n >= 11:
|
|
214
|
+
pool, user, source, dest = a(0), a(10), a(4), a(5)
|
|
215
|
+
exact, invert = disc in DL_IN, disc in DL_OUT
|
|
216
|
+
elif protocol in ("LaunchLab", "PumpSwap") and disc in (
|
|
217
|
+
LAB if protocol == "LaunchLab" else PUMP
|
|
218
|
+
):
|
|
219
|
+
buy, exact = (LAB if protocol == "LaunchLab" else PUMP)[disc]
|
|
220
|
+
if n < (18 if protocol == "LaunchLab" else 21):
|
|
221
|
+
return None
|
|
222
|
+
source, dest = a(6 if buy else 5), a(5 if buy else 6)
|
|
223
|
+
if protocol == "LaunchLab":
|
|
224
|
+
pool, user = a(4), a(0)
|
|
225
|
+
pair, mode = (
|
|
226
|
+
(a(10), a(9)) if buy else (a(9), a(10))
|
|
227
|
+
), stonkfun_mode_from_platform_config(a(3))
|
|
228
|
+
else:
|
|
229
|
+
pool, user = a(0), a(1)
|
|
230
|
+
pair = (a(4), a(3)) if buy else (a(3), a(4))
|
|
231
|
+
elif protocol=='PumpFun' and disc in CURVE_V2 and n>=16:
|
|
232
|
+
buy,exact=CURVE_V2[disc]
|
|
233
|
+
pool,user,source,dest=a(10),a(13),a(15 if buy else 14),a(14 if buy else 15)
|
|
234
|
+
pair=(a(2),a(1)) if buy else (a(1),a(2))
|
|
235
|
+
if a(2)==WSOL:
|
|
236
|
+
if buy: source=user
|
|
237
|
+
else: dest=user
|
|
238
|
+
elif protocol=='PumpFun' and disc in CURVE_LEGACY and n>=12:
|
|
239
|
+
buy,exact=CURVE_LEGACY[disc]
|
|
240
|
+
pool,user,source,dest=a(3),a(6),a(6 if buy else 5),a(5 if buy else 6)
|
|
241
|
+
pair=(WSOL,a(2)) if buy else (a(2),WSOL)
|
|
242
|
+
elif protocol == "RaydiumAmmV4" and len(d) >= 17 and d[0] in (9, 11, 16, 17):
|
|
243
|
+
modern = d[0] in (16, 17)
|
|
244
|
+
if n < (8 if modern else 17):
|
|
245
|
+
return None
|
|
246
|
+
pool, user = a(1), a(7 if modern else n - 1)
|
|
247
|
+
source, dest = (a(5), a(6)) if modern else (a(n - 3), a(n - 2))
|
|
248
|
+
offset, exact = 1, d[0] in (9, 16)
|
|
249
|
+
invert = not exact
|
|
250
|
+
else:
|
|
251
|
+
return None
|
|
252
|
+
if len(d) < offset + 16:
|
|
253
|
+
return None
|
|
254
|
+
first, second = int.from_bytes(d[offset : offset + 8], "little"), int.from_bytes(
|
|
255
|
+
d[offset + 8 : offset + 16], "little"
|
|
256
|
+
)
|
|
257
|
+
source_mint = mints.get(source) or (pair[0] if pair and pair[0] != ZERO else None)
|
|
258
|
+
dest_mint = mints.get(dest) or (pair[1] if pair and pair[1] != ZERO else None)
|
|
259
|
+
return RouteSwapLeg(
|
|
260
|
+
ix.position,
|
|
261
|
+
ix.program,
|
|
262
|
+
protocol,
|
|
263
|
+
pool,
|
|
264
|
+
user,
|
|
265
|
+
source,
|
|
266
|
+
dest,
|
|
267
|
+
source_mint,
|
|
268
|
+
dest_mint,
|
|
269
|
+
exact,
|
|
270
|
+
second if invert else first,
|
|
271
|
+
first if invert else second,
|
|
272
|
+
stonkfun_mode=mode,
|
|
273
|
+
stonkfun_graduated=protocol == "RaydiumCpmm" and pool in graduated,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _checked(ix: _Invocation) -> bool:
|
|
278
|
+
return (
|
|
279
|
+
ix.program in (TOKEN, TOKEN_2022)
|
|
280
|
+
and len(ix.data) >= 10
|
|
281
|
+
and ix.data[0] == 12
|
|
282
|
+
and len(ix.accounts) >= 4
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _with_fee(ix: _Invocation) -> bool:
|
|
287
|
+
return (
|
|
288
|
+
ix.program == TOKEN_2022
|
|
289
|
+
and len(ix.data) >= 19
|
|
290
|
+
and ix.data[:2] == bytes([26, 1])
|
|
291
|
+
and len(ix.accounts) >= 4
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _transfer(ix: _Invocation, mints: dict[str, str]) -> Optional[RouteTokenTransfer]:
|
|
296
|
+
if ix.program not in (TOKEN, TOKEN_2022) or not ix.data:
|
|
297
|
+
return None
|
|
298
|
+
if _checked(ix):
|
|
299
|
+
dest, offset, fee = 2, 1, 0 if ix.program == TOKEN else None
|
|
300
|
+
elif _with_fee(ix):
|
|
301
|
+
dest, offset, fee = 2, 2, int.from_bytes(ix.data[11:19], "little")
|
|
302
|
+
elif ix.data[0] == 3 and len(ix.data) >= 9 and len(ix.accounts) >= 3:
|
|
303
|
+
dest, offset, fee = 1, 1, 0
|
|
304
|
+
else:
|
|
305
|
+
return None
|
|
306
|
+
return RouteTokenTransfer(
|
|
307
|
+
ix.position,
|
|
308
|
+
ix.program,
|
|
309
|
+
ix.a(0),
|
|
310
|
+
ix.a(dest),
|
|
311
|
+
mints.get(ix.a(0)),
|
|
312
|
+
int.from_bytes(ix.data[offset : offset + 8], "little"),
|
|
313
|
+
fee,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _descendant(parent: InstructionPosition, child: InstructionPosition) -> bool:
|
|
318
|
+
if parent.outer_index != child.outer_index:
|
|
319
|
+
return False
|
|
320
|
+
if parent.inner_index is None:
|
|
321
|
+
return child.inner_index is not None
|
|
322
|
+
return (
|
|
323
|
+
child.inner_index is not None
|
|
324
|
+
and child.inner_index > parent.inner_index
|
|
325
|
+
and parent.stack_height is not None
|
|
326
|
+
and child.stack_height is not None
|
|
327
|
+
and child.stack_height > parent.stack_height
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def analyze_rpc_transaction_routes(
|
|
332
|
+
transaction: dict, graduated_stonkfun_pools: Iterable[str] = ()
|
|
333
|
+
) -> TransactionRoute:
|
|
334
|
+
tx = transaction.get("result", transaction)
|
|
335
|
+
if not isinstance(tx, dict) or not isinstance(tx.get("meta"), dict):
|
|
336
|
+
raise ValueError("Transaction metadata missing")
|
|
337
|
+
if "err" not in tx["meta"]:
|
|
338
|
+
raise ValueError("Transaction execution status missing")
|
|
339
|
+
body = tx["transaction"]
|
|
340
|
+
if isinstance(body, list):
|
|
341
|
+
if len(body) != 2 or body[1] != "base64":
|
|
342
|
+
raise ValueError("Expected base64 wire encoding")
|
|
343
|
+
body, _ = decode_wire_transaction(base64.b64decode(body[0], validate=True))
|
|
344
|
+
message, meta = body["message"], tx["meta"]
|
|
345
|
+
if not isinstance(message, dict):
|
|
346
|
+
raise ValueError("Expected JSON-encoded compiled transaction")
|
|
347
|
+
keys = [k if isinstance(k, str) else k["pubkey"] for k in message["accountKeys"]]
|
|
348
|
+
loaded = meta.get("loadedAddresses") or {}
|
|
349
|
+
keys += loaded.get("writable", []) + loaded.get("readonly", [])
|
|
350
|
+
|
|
351
|
+
def key(i: int) -> str:
|
|
352
|
+
if type(i) is not int or not 0 <= i < len(keys):
|
|
353
|
+
raise ValueError("Invalid compiled account index")
|
|
354
|
+
return keys[i]
|
|
355
|
+
|
|
356
|
+
invocations: list[_Invocation] = []
|
|
357
|
+
groups = meta.get("innerInstructions") or []
|
|
358
|
+
if not isinstance(groups, list):
|
|
359
|
+
raise ValueError("Invalid compiled instruction groups")
|
|
360
|
+
seen = set()
|
|
361
|
+
for group in groups:
|
|
362
|
+
if not isinstance(group, dict):
|
|
363
|
+
raise ValueError("Invalid compiled instruction group")
|
|
364
|
+
index = group.get("index")
|
|
365
|
+
if type(index) is not int or not 0 <= index < len(message["instructions"]) or index in seen or not isinstance(group.get("instructions"), list):
|
|
366
|
+
raise ValueError("Invalid compiled instruction group")
|
|
367
|
+
seen.add(index)
|
|
368
|
+
for ix in group["instructions"]:
|
|
369
|
+
if not isinstance(ix, dict) or (ix.get("stackHeight") is not None and (type(ix["stackHeight"]) is not int or ix["stackHeight"] < 2)):
|
|
370
|
+
raise ValueError("Invalid compiled stack height")
|
|
371
|
+
for i, outer in enumerate(message["instructions"]):
|
|
372
|
+
batch = [(outer, InstructionPosition(i, None, 1))]
|
|
373
|
+
for group in groups:
|
|
374
|
+
if group["index"] == i:
|
|
375
|
+
batch += [
|
|
376
|
+
(ix, InstructionPosition(i, j, ix.get("stackHeight")))
|
|
377
|
+
for j, ix in enumerate(group["instructions"])
|
|
378
|
+
]
|
|
379
|
+
for ix, position in batch:
|
|
380
|
+
if "programIdIndex" not in ix:
|
|
381
|
+
raise ValueError("Expected compiled instructions, not jsonParsed")
|
|
382
|
+
data = (
|
|
383
|
+
base58.b58decode(ix["data"])
|
|
384
|
+
if isinstance(ix["data"], str)
|
|
385
|
+
else bytes(ix["data"])
|
|
386
|
+
)
|
|
387
|
+
invocations.append(
|
|
388
|
+
_Invocation(
|
|
389
|
+
position,
|
|
390
|
+
key(ix["programIdIndex"]),
|
|
391
|
+
[key(k) for k in ix["accounts"]],
|
|
392
|
+
data,
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
mints: dict[str, str] = {}
|
|
396
|
+
for balance in (meta.get("preTokenBalances") or []) + (
|
|
397
|
+
meta.get("postTokenBalances") or []
|
|
398
|
+
):
|
|
399
|
+
# Reject malformed mints rather than inventing pool identity from a key.
|
|
400
|
+
if len(base58.b58decode(balance["mint"])) == 32:
|
|
401
|
+
mints[key(balance["accountIndex"])] = balance["mint"]
|
|
402
|
+
for ix in invocations:
|
|
403
|
+
if _checked(ix) or _with_fee(ix):
|
|
404
|
+
mints[ix.a(0)] = mints[ix.a(2)] = ix.a(1)
|
|
405
|
+
if (
|
|
406
|
+
ix.program in (TOKEN, TOKEN_2022)
|
|
407
|
+
and len(ix.accounts) >= 2
|
|
408
|
+
and (
|
|
409
|
+
(ix.data == bytes([1]))
|
|
410
|
+
or (len(ix.data) == 33 and ix.data[0] in (16, 18))
|
|
411
|
+
)
|
|
412
|
+
):
|
|
413
|
+
mints[ix.a(0)] = ix.a(1)
|
|
414
|
+
while True:
|
|
415
|
+
before = len(mints)
|
|
416
|
+
for ix in invocations:
|
|
417
|
+
if (
|
|
418
|
+
ix.program in (TOKEN, TOKEN_2022)
|
|
419
|
+
and len(ix.data) >= 9
|
|
420
|
+
and ix.data[0] == 3
|
|
421
|
+
and len(ix.accounts) >= 3
|
|
422
|
+
):
|
|
423
|
+
mint = mints.get(ix.a(0)) or mints.get(ix.a(1))
|
|
424
|
+
if mint:
|
|
425
|
+
mints.setdefault(ix.a(0), mint)
|
|
426
|
+
mints.setdefault(ix.a(1), mint)
|
|
427
|
+
if len(mints) == before:
|
|
428
|
+
break
|
|
429
|
+
mints.pop(ZERO, None)
|
|
430
|
+
signatures = body.get("signatures", [])
|
|
431
|
+
route = TransactionRoute(
|
|
432
|
+
signatures[0] if signatures else base58.b58encode(bytes(64)).decode(),
|
|
433
|
+
meta.get("err") is None,
|
|
434
|
+
)
|
|
435
|
+
graduated = set(graduated_stonkfun_pools)
|
|
436
|
+
for i, ix in enumerate(invocations):
|
|
437
|
+
own = _transfer(ix, mints)
|
|
438
|
+
if own:
|
|
439
|
+
route.transfers.append(own)
|
|
440
|
+
children = []
|
|
441
|
+
for child in invocations[i + 1 :]:
|
|
442
|
+
if not _descendant(ix.position, child.position):
|
|
443
|
+
break
|
|
444
|
+
children.append(child)
|
|
445
|
+
nested = [t for c in children if (t := _transfer(c, mints)) is not None]
|
|
446
|
+
leg = _swap(ix, mints, graduated)
|
|
447
|
+
if leg:
|
|
448
|
+
if route.succeeded:
|
|
449
|
+
inputs = [t.amount for t in nested if t.source == leg.input_account]
|
|
450
|
+
outputs = [
|
|
451
|
+
(
|
|
452
|
+
None
|
|
453
|
+
if t.withheld_fee is None or t.withheld_fee > t.amount
|
|
454
|
+
else t.amount - t.withheld_fee
|
|
455
|
+
)
|
|
456
|
+
for t in nested
|
|
457
|
+
if t.destination == leg.output_account
|
|
458
|
+
]
|
|
459
|
+
if inputs and sum(inputs) <= U64_MAX:
|
|
460
|
+
leg.actual_input_amount = sum(inputs)
|
|
461
|
+
if leg.protocol=='PumpFun' and leg.input_mint==WSOL and leg.input_account==leg.trader:
|
|
462
|
+
leg.actual_input_amount=_pumpfun_native_debit(ix,children,leg)
|
|
463
|
+
if (
|
|
464
|
+
outputs
|
|
465
|
+
and all(x is not None for x in outputs)
|
|
466
|
+
and sum(outputs) <= U64_MAX
|
|
467
|
+
):
|
|
468
|
+
leg.actual_output_amount = sum(outputs)
|
|
469
|
+
route.legs.append(leg)
|
|
470
|
+
elif ix.program not in (
|
|
471
|
+
TOKEN,
|
|
472
|
+
TOKEN_2022,
|
|
473
|
+
ZERO,
|
|
474
|
+
"ComputeBudget111111111111111111111111111111",
|
|
475
|
+
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
|
|
476
|
+
"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
|
|
477
|
+
):
|
|
478
|
+
route.unknown_invocations.append(
|
|
479
|
+
RouteUnknownInvocation(
|
|
480
|
+
ix.position,
|
|
481
|
+
ix.program,
|
|
482
|
+
bool(nested),
|
|
483
|
+
any(_swap(c, mints, graduated) for c in children),
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
target, action = None, None
|
|
487
|
+
if (
|
|
488
|
+
ix.program == ZERO
|
|
489
|
+
and len(ix.data) == 12
|
|
490
|
+
and ix.data[:4] == bytes([2, 0, 0, 0])
|
|
491
|
+
and len(ix.accounts) >= 2
|
|
492
|
+
):
|
|
493
|
+
target, action = ix.a(1), {
|
|
494
|
+
"Fund": {
|
|
495
|
+
"source": ix.a(0),
|
|
496
|
+
"lamports": int.from_bytes(ix.data[4:12], "little"),
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
elif ix.program in (TOKEN, TOKEN_2022):
|
|
500
|
+
if ix.data == bytes([17]) and ix.accounts:
|
|
501
|
+
target, action = ix.a(0), "SyncNative"
|
|
502
|
+
elif ix.data == bytes([9]) and len(ix.accounts) >= 3:
|
|
503
|
+
target, action = ix.a(0), {
|
|
504
|
+
"Close": {"destination": ix.a(1), "authority": ix.a(2)}
|
|
505
|
+
}
|
|
506
|
+
if target and mints.get(target) == WSOL:
|
|
507
|
+
route.native_token_actions.append(
|
|
508
|
+
RouteNativeTokenAction(ix.position, target, action)
|
|
509
|
+
)
|
|
510
|
+
return route
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Native canonical Solana Legacy, V0 and V1 wire decoding (SIMD-0385)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import base58
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _Reader:
|
|
8
|
+
def __init__(self, data: bytes, offset: int = 0):
|
|
9
|
+
self.data, self.pos = data, offset
|
|
10
|
+
|
|
11
|
+
def take(self, n: int) -> bytes:
|
|
12
|
+
if n < 0 or self.pos + n > len(self.data):
|
|
13
|
+
raise ValueError("Truncated transaction")
|
|
14
|
+
value = self.data[self.pos : self.pos + n]
|
|
15
|
+
self.pos += n
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
def uint(self, n: int = 1) -> int:
|
|
19
|
+
return int.from_bytes(self.take(n), "little")
|
|
20
|
+
|
|
21
|
+
def short(self) -> int:
|
|
22
|
+
value = 0
|
|
23
|
+
for i in range(3):
|
|
24
|
+
b = self.uint()
|
|
25
|
+
if i == 2 and b > 3:
|
|
26
|
+
raise ValueError("Invalid short-u16")
|
|
27
|
+
value |= (b & 127) << (7 * i)
|
|
28
|
+
if not b & 128:
|
|
29
|
+
if i and b == 0:
|
|
30
|
+
raise ValueError("Noncanonical short-u16")
|
|
31
|
+
return value
|
|
32
|
+
raise ValueError("Invalid short-u16")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def decode_wire_transaction(
|
|
36
|
+
data: bytes, offset: int = 0, require_complete: bool = True
|
|
37
|
+
) -> tuple[dict, int]:
|
|
38
|
+
if not isinstance(offset, int) or offset < 0 or offset >= len(data):
|
|
39
|
+
raise ValueError("Invalid wire offset")
|
|
40
|
+
r = _Reader(data, offset)
|
|
41
|
+
version, signatures, config, lookups = "legacy", [], None, []
|
|
42
|
+
if r.data[r.pos : r.pos + 1] == bytes([129]):
|
|
43
|
+
r.take(1)
|
|
44
|
+
version = 1
|
|
45
|
+
header = r.take(3)
|
|
46
|
+
mask = r.uint(4)
|
|
47
|
+
blockhash = r.take(32)
|
|
48
|
+
if mask & ~31 or mask & 3 not in (0, 3):
|
|
49
|
+
raise ValueError("Invalid V1 config mask")
|
|
50
|
+
count, keys_count = r.uint(), r.uint()
|
|
51
|
+
if count > 64 or keys_count > 64 or header[0] > 12:
|
|
52
|
+
raise ValueError("V1 transaction limit exceeded")
|
|
53
|
+
keys = [r.take(32) for _ in range(keys_count)]
|
|
54
|
+
config = {}
|
|
55
|
+
for bit, key, size in [
|
|
56
|
+
(3, "priorityFee", 8),
|
|
57
|
+
(4, "computeUnitLimit", 4),
|
|
58
|
+
(8, "loadedAccountsDataSizeLimit", 4),
|
|
59
|
+
(16, "heapSize", 4),
|
|
60
|
+
]:
|
|
61
|
+
if mask & bit:
|
|
62
|
+
config[key] = r.uint(size)
|
|
63
|
+
if "heapSize" in config and (
|
|
64
|
+
not 32768 <= config["heapSize"] <= 262144 or config["heapSize"] % 1024
|
|
65
|
+
):
|
|
66
|
+
raise ValueError("Invalid heap size")
|
|
67
|
+
headers = [(r.uint(), r.uint(), r.uint(2)) for _ in range(count)]
|
|
68
|
+
instructions = [
|
|
69
|
+
{
|
|
70
|
+
"programIdIndex": program,
|
|
71
|
+
"accounts": list(r.take(accounts)),
|
|
72
|
+
"data": base58.b58encode(r.take(length)).decode(),
|
|
73
|
+
}
|
|
74
|
+
for program, accounts, length in headers
|
|
75
|
+
]
|
|
76
|
+
signatures = [r.take(64) for _ in range(header[0])]
|
|
77
|
+
if r.pos - offset > 4096:
|
|
78
|
+
raise ValueError("V1 transaction exceeds 4096 bytes")
|
|
79
|
+
else:
|
|
80
|
+
signature_count = r.short()
|
|
81
|
+
if signature_count > 127:
|
|
82
|
+
raise ValueError("Too many signatures")
|
|
83
|
+
signatures = [r.take(64) for _ in range(signature_count)]
|
|
84
|
+
first = r.uint()
|
|
85
|
+
if first & 128:
|
|
86
|
+
if first != 128:
|
|
87
|
+
raise ValueError("Unknown transaction version")
|
|
88
|
+
version = 0
|
|
89
|
+
header = r.take(3)
|
|
90
|
+
else:
|
|
91
|
+
header = bytes([first]) + r.take(2)
|
|
92
|
+
keys_count = r.short()
|
|
93
|
+
if keys_count > 256:
|
|
94
|
+
raise ValueError("Too many account keys")
|
|
95
|
+
keys = [r.take(32) for _ in range(keys_count)]
|
|
96
|
+
blockhash = r.take(32)
|
|
97
|
+
instructions = []
|
|
98
|
+
for _ in range(r.short()):
|
|
99
|
+
program = r.uint()
|
|
100
|
+
accounts = list(r.take(r.short()))
|
|
101
|
+
payload = r.take(r.short())
|
|
102
|
+
instructions.append(
|
|
103
|
+
{
|
|
104
|
+
"programIdIndex": program,
|
|
105
|
+
"accounts": accounts,
|
|
106
|
+
"data": base58.b58encode(payload).decode(),
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
if version == 0:
|
|
110
|
+
for _ in range(r.short()):
|
|
111
|
+
key = r.take(32)
|
|
112
|
+
writable = list(r.take(r.short()))
|
|
113
|
+
readonly = list(r.take(r.short()))
|
|
114
|
+
lookups.append(
|
|
115
|
+
{
|
|
116
|
+
"accountKey": base58.b58encode(key).decode(),
|
|
117
|
+
"writableIndexes": writable,
|
|
118
|
+
"readonlyIndexes": readonly,
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
if len(signatures) != header[0]:
|
|
122
|
+
raise ValueError("Signature count does not match header")
|
|
123
|
+
if (
|
|
124
|
+
header[0] > len(keys)
|
|
125
|
+
or header[1] > header[0]
|
|
126
|
+
or header[2] > len(keys) - header[0]
|
|
127
|
+
):
|
|
128
|
+
raise ValueError("Invalid message header")
|
|
129
|
+
if version == 1:
|
|
130
|
+
if header[1] >= header[0]:
|
|
131
|
+
raise ValueError("V1 requires a writable fee payer")
|
|
132
|
+
if len(set(keys)) != len(keys):
|
|
133
|
+
raise ValueError("Duplicate V1 accounts")
|
|
134
|
+
for ix in instructions:
|
|
135
|
+
if (
|
|
136
|
+
ix["programIdIndex"] == 0
|
|
137
|
+
or ix["programIdIndex"] >= len(keys)
|
|
138
|
+
or any(i >= len(keys) for i in ix["accounts"])
|
|
139
|
+
):
|
|
140
|
+
raise ValueError("Invalid V1 instruction index")
|
|
141
|
+
account_count = len(keys) + sum(
|
|
142
|
+
len(l["writableIndexes"]) + len(l["readonlyIndexes"]) for l in lookups
|
|
143
|
+
)
|
|
144
|
+
if account_count > 256:
|
|
145
|
+
raise ValueError("Too many resolved account keys")
|
|
146
|
+
if any(ix["programIdIndex"] >= len(keys) or any(i >= account_count for i in ix["accounts"]) for ix in instructions):
|
|
147
|
+
raise ValueError("Invalid instruction index")
|
|
148
|
+
if require_complete and r.pos != len(data):
|
|
149
|
+
raise ValueError("Trailing transaction bytes")
|
|
150
|
+
message = {
|
|
151
|
+
"header": dict(
|
|
152
|
+
zip(
|
|
153
|
+
(
|
|
154
|
+
"numRequiredSignatures",
|
|
155
|
+
"numReadonlySignedAccounts",
|
|
156
|
+
"numReadonlyUnsignedAccounts",
|
|
157
|
+
),
|
|
158
|
+
header,
|
|
159
|
+
)
|
|
160
|
+
),
|
|
161
|
+
"accountKeys": [base58.b58encode(key).decode() for key in keys],
|
|
162
|
+
"recentBlockhash": base58.b58encode(blockhash).decode(),
|
|
163
|
+
"instructions": instructions,
|
|
164
|
+
"addressTableLookups": lookups,
|
|
165
|
+
}
|
|
166
|
+
if config is not None:
|
|
167
|
+
message["config"] = config
|
|
168
|
+
return {
|
|
169
|
+
"signatures": [base58.b58encode(s).decode() for s in signatures],
|
|
170
|
+
"message": message,
|
|
171
|
+
"version": version,
|
|
172
|
+
}, r.pos - offset
|