checkpoint-sdk 0.2.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 apchhui
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: checkpoint-sdk
3
+ Version: 0.2.0
4
+ Summary: Program-agnostic Solana IDL transaction decoder and SDK
5
+ Author: apchhui
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/apchhui/checkpoint-sdk
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: requests>=2.31.0
12
+ Requires-Dist: solana>=0.30.0
13
+ Requires-Dist: solders>=0.19.0
14
+ Requires-Dist: base58>=2.1.1
15
+ Dynamic: license-file
16
+
17
+ # checkpoint-sdk
18
+
19
+ Solana SDK for:
20
+ - Real-time transaction decoding via Anchor IDL
21
+ - Program-agnostic decoding
22
+ - Batch RPC fetching
23
+ - Wallet utilities
24
+ - etc.
25
+
26
+ Wallet Manager is beta!!!
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install checkpoint-sdk
32
+ ```
33
+
34
+ ## Example usage
35
+
36
+ ```python
37
+ from checkpoint_sdk import Decoder
38
+ # from checkpoint_sdk.decoder import Decoder # also works
39
+
40
+ PROGRAM_ID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
41
+
42
+ decoder = Decoder([PROGRAM_ID])
43
+
44
+ result = decoder.decode("vdt/007m", PROGRAM_ID) # Example b64 data
45
+
46
+ print(result)
47
+ ```
48
+
49
+ If the built-in IDL auto-fetch 403s (it hits an undocumented Solscan
50
+ endpoint, which blocks non-browser requests fairly often), pass your own:
51
+
52
+ ```python
53
+ import json
54
+
55
+ idl = json.load(open("pump.json")) # from the program's own public source repo
56
+ decoder = Decoder([PROGRAM_ID], idls={PROGRAM_ID: idl})
57
+ ```
58
+
59
+ ## 0.2.0 changes
60
+
61
+ Fixed, without changing the public shape of anything except `WalletManager`:
62
+
63
+ - **`import checkpoint_sdk` no longer crashes on a fresh install.**
64
+ `checkpoint_sdk/__init__.py` unconditionally imported `WalletManager`,
65
+ which imported `solana.rpc.api.Client` - a module solana-py removed in
66
+ favor of the async-only client. `WalletManager` is now imported lazily, so
67
+ a problem in the wallet module can't break `Decoder`/`TransactionManager`.
68
+ - **`from checkpoint_sdk.decoder import Decoder` (this README's own example)
69
+ now actually works.** `decoder/__init__.py` (and `transaction/__init__.py`,
70
+ `wallet/__init__.py`) were empty.
71
+ - **`Decoder.IDLs` is an instance attribute now, not a class attribute.**
72
+ Previously every `Decoder` you created in the same process shared (and
73
+ kept appending to) the same list.
74
+ - **`Decoder(programs, idls={...})`** - pass pre-loaded IDLs directly instead
75
+ of subclassing to override `_fetch_idl()`. See the Solscan 403 note above.
76
+ - **`extract_program_data()` / `extract_all_program_data()` now track the
77
+ actual invoke/success call stack** instead of assuming a `Program data:`
78
+ line always precedes the next invoke of the same program - which breaks
79
+ on transactions with nested self-CPI event logging. Also fixed an
80
+ off-by-one that silently dropped a data line if it was the very last log
81
+ line.
82
+
83
+ **Breaking change:** `WalletManager`'s methods are `async def` now
84
+ (`get_balance`, `transfer`, `instant_transfer`, `airdrop`,
85
+ `get_recent_blockhash`, `estimate_transfer_fee`, `get_network_info` all need
86
+ `await`; there's also a new `await wallet.close()`). This wasn't optional -
87
+ the synchronous `solana.rpc.api.Client` it depended on no longer exists in
88
+ current solana-py, so there was no working synchronous behavior left to
89
+ preserve. Verified end-to-end against Solana devnet (client construction,
90
+ wallet creation, network info, balance checks, and the insufficient-balance
91
+ error path); the airdrop-then-send round trip specifically couldn't be
92
+ re-confirmed live in one sitting because the public devnet faucet was
93
+ rate-limited at the time - transaction construction/signing itself uses the
94
+ same `MessageV0` + `VersionedTransaction` pattern already verified in the
95
+ decoder/transaction paths.
96
+
97
+ ## Documentation
98
+
99
+ https://github.com/apchhui/checkpoint-python/tree/main/docs
@@ -0,0 +1,83 @@
1
+ # checkpoint-sdk
2
+
3
+ Solana SDK for:
4
+ - Real-time transaction decoding via Anchor IDL
5
+ - Program-agnostic decoding
6
+ - Batch RPC fetching
7
+ - Wallet utilities
8
+ - etc.
9
+
10
+ Wallet Manager is beta!!!
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install checkpoint-sdk
16
+ ```
17
+
18
+ ## Example usage
19
+
20
+ ```python
21
+ from checkpoint_sdk import Decoder
22
+ # from checkpoint_sdk.decoder import Decoder # also works
23
+
24
+ PROGRAM_ID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
25
+
26
+ decoder = Decoder([PROGRAM_ID])
27
+
28
+ result = decoder.decode("vdt/007m", PROGRAM_ID) # Example b64 data
29
+
30
+ print(result)
31
+ ```
32
+
33
+ If the built-in IDL auto-fetch 403s (it hits an undocumented Solscan
34
+ endpoint, which blocks non-browser requests fairly often), pass your own:
35
+
36
+ ```python
37
+ import json
38
+
39
+ idl = json.load(open("pump.json")) # from the program's own public source repo
40
+ decoder = Decoder([PROGRAM_ID], idls={PROGRAM_ID: idl})
41
+ ```
42
+
43
+ ## 0.2.0 changes
44
+
45
+ Fixed, without changing the public shape of anything except `WalletManager`:
46
+
47
+ - **`import checkpoint_sdk` no longer crashes on a fresh install.**
48
+ `checkpoint_sdk/__init__.py` unconditionally imported `WalletManager`,
49
+ which imported `solana.rpc.api.Client` - a module solana-py removed in
50
+ favor of the async-only client. `WalletManager` is now imported lazily, so
51
+ a problem in the wallet module can't break `Decoder`/`TransactionManager`.
52
+ - **`from checkpoint_sdk.decoder import Decoder` (this README's own example)
53
+ now actually works.** `decoder/__init__.py` (and `transaction/__init__.py`,
54
+ `wallet/__init__.py`) were empty.
55
+ - **`Decoder.IDLs` is an instance attribute now, not a class attribute.**
56
+ Previously every `Decoder` you created in the same process shared (and
57
+ kept appending to) the same list.
58
+ - **`Decoder(programs, idls={...})`** - pass pre-loaded IDLs directly instead
59
+ of subclassing to override `_fetch_idl()`. See the Solscan 403 note above.
60
+ - **`extract_program_data()` / `extract_all_program_data()` now track the
61
+ actual invoke/success call stack** instead of assuming a `Program data:`
62
+ line always precedes the next invoke of the same program - which breaks
63
+ on transactions with nested self-CPI event logging. Also fixed an
64
+ off-by-one that silently dropped a data line if it was the very last log
65
+ line.
66
+
67
+ **Breaking change:** `WalletManager`'s methods are `async def` now
68
+ (`get_balance`, `transfer`, `instant_transfer`, `airdrop`,
69
+ `get_recent_blockhash`, `estimate_transfer_fee`, `get_network_info` all need
70
+ `await`; there's also a new `await wallet.close()`). This wasn't optional -
71
+ the synchronous `solana.rpc.api.Client` it depended on no longer exists in
72
+ current solana-py, so there was no working synchronous behavior left to
73
+ preserve. Verified end-to-end against Solana devnet (client construction,
74
+ wallet creation, network info, balance checks, and the insufficient-balance
75
+ error path); the airdrop-then-send round trip specifically couldn't be
76
+ re-confirmed live in one sitting because the public devnet faucet was
77
+ rate-limited at the time - transaction construction/signing itself uses the
78
+ same `MessageV0` + `VersionedTransaction` pattern already verified in the
79
+ decoder/transaction paths.
80
+
81
+ ## Documentation
82
+
83
+ https://github.com/apchhui/checkpoint-python/tree/main/docs
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "checkpoint-sdk"
7
+ version = "0.2.0"
8
+ description = "Program-agnostic Solana IDL transaction decoder and SDK"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ { name = "apchhui" }
13
+ ]
14
+ requires-python = ">=3.10"
15
+
16
+ dependencies = [
17
+ "requests>=2.31.0",
18
+ "solana>=0.30.0",
19
+ "solders>=0.19.0",
20
+ "base58>=2.1.1"
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/apchhui/checkpoint-sdk"
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
28
+ include = ["checkpoint_sdk*"]
29
+ exclude = ["tests*", "test*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ from .decoder.decoder import Decoder
2
+ from .transaction.transaction import TransactionManager
3
+
4
+ __all__ = [
5
+ "Decoder",
6
+ "TransactionManager",
7
+ "WalletManager",
8
+ ]
9
+
10
+
11
+ def __getattr__(name):
12
+ # WalletManager is imported lazily so that a problem in the wallet module
13
+ # (or one of its dependencies) can never break `from checkpoint_sdk import
14
+ # Decoder` - which is the part of this package basically everyone uses.
15
+ if name == "WalletManager":
16
+ from .wallet.wallet import WalletManager
17
+
18
+ return WalletManager
19
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,3 @@
1
+ from .decoder import Decoder
2
+
3
+ __all__ = ["Decoder"]
@@ -0,0 +1,386 @@
1
+ ### --!-- --!-- --!-- better not to use python in Solana high performance production apps --!-- --!-- --!-- ###
2
+ import re
3
+ import requests
4
+ from typing import Optional, Dict, Any, Tuple, List
5
+ from checkpoint_sdk.decoder.exceptions import TooManyElements, NoElements
6
+ import base64
7
+ import struct
8
+
9
+ _INVOKE_RE = re.compile(r"^Program (\w+) invoke \[(\d+)\]$")
10
+ _RESULT_RE = re.compile(r"^Program (\w+) (success|failed)")
11
+
12
+ class Decoder:
13
+ MAX = 6
14
+
15
+ def __init__(self, programs, idls: Optional[Dict[str, Dict[str, Any]]] = None):
16
+ """
17
+ programs: program addresses to decode events for.
18
+ idls: optional {program_address: idl_dict} of pre-loaded IDLs. Any
19
+ program_address found here skips the live fetch entirely - useful
20
+ because the built-in fetch hits an undocumented Solscan endpoint
21
+ that returns 403 for non-browser requests fairly often. Fetch
22
+ your own IDL (e.g. from the program's public source repo, or by
23
+ reading its on-chain IDL account) and pass it here instead of
24
+ relying on that endpoint.
25
+ """
26
+ # IDLs used to be a class attribute here, which meant every Decoder
27
+ # instance in a process shared (and kept appending to) the same list.
28
+ # It's an instance attribute now.
29
+ self.IDLs = []
30
+
31
+ programs_count = len(programs)
32
+
33
+ if programs_count > self.MAX:
34
+ raise TooManyElements(
35
+ f"Too many programs input! Max programs supported: {self.MAX} | Current input: {programs_count}"
36
+ )
37
+ elif programs_count == 0:
38
+ raise NoElements(
39
+ f"No programs on input! Expected MIN: 1 | MAX: {self.MAX} elements array"
40
+ )
41
+
42
+ idls = idls or {}
43
+ for program_address in programs:
44
+ idl = idls.get(program_address) or self._fetch_idl(program_address)
45
+ self.IDLs.append(idl)
46
+
47
+ def _find_idl(self, program_address: str):
48
+ for idl in self.IDLs:
49
+ idl_address = idl.get("address")
50
+ if idl_address and idl_address == program_address:
51
+ return idl
52
+
53
+ def decode(self, base64_str: str, program_address: str):
54
+ """
55
+ Decodes input `Program data:` base64 string by program IDL
56
+ """
57
+ if not isinstance(base64_str, str): raise ValueError(f"Base 64 must be typeof string! Current type: {type(base64_str)} | decode()")
58
+ def normalize_b64(s: str) -> str:
59
+ s = s.strip().split()[-1]
60
+ return s + "=" * (-len(s) % 4)
61
+
62
+ idl = self._find_idl(program_address)
63
+ if not idl:
64
+ raise ValueError(f"IDL not found for program: {program_address}")
65
+
66
+ events = idl.get("events", [])
67
+
68
+ b64_clean = normalize_b64(base64_str)
69
+ base64_raw = base64.b64decode(b64_clean)
70
+ discriminator = list(base64_raw[:8])
71
+
72
+ for event in events:
73
+ if event.get("discriminator") == discriminator:
74
+ name = event.get("name")
75
+ for _type in idl.get("types", []):
76
+ if _type.get("name") == name:
77
+ offset = 8
78
+ result = {}
79
+ if "type" in _type and "fields" in _type["type"]:
80
+ for arg in _type["type"]["fields"]:
81
+ val, offset = self._read_type(arg["type"], base64_raw, offset, idl)
82
+ result[arg["name"]] = val
83
+ return result
84
+ return None
85
+
86
+ def _read_primitive(self, t: str, data: bytes, offset: int) -> Tuple[Any, int]:
87
+ if t == "bool":
88
+ return data[offset] == 1, offset + 1
89
+
90
+ if t == "u8":
91
+ return data[offset], offset + 1
92
+ if t == "i8":
93
+ return struct.unpack_from("<b", data, offset)[0], offset + 1
94
+
95
+ if t == "u16":
96
+ return struct.unpack_from("<H", data, offset)[0], offset + 2
97
+ if t == "i16":
98
+ return struct.unpack_from("<h", data, offset)[0], offset + 2
99
+
100
+ if t == "u32":
101
+ return struct.unpack_from("<I", data, offset)[0], offset + 4
102
+ if t == "i32":
103
+ return struct.unpack_from("<i", data, offset)[0], offset + 4
104
+
105
+ if t == "u64":
106
+ return struct.unpack_from("<Q", data, offset)[0], offset + 8
107
+ if t == "i64":
108
+ return struct.unpack_from("<q", data, offset)[0], offset + 8
109
+
110
+ if t == "u128":
111
+ lo, hi = struct.unpack_from("<QQ", data, offset)
112
+ return (hi << 64) | lo, offset + 16
113
+
114
+ if t == "i128":
115
+ lo, hi = struct.unpack_from("<QQ", data, offset)
116
+ val = (hi << 64) | lo
117
+ if hi & (1 << 63):
118
+ val -= 1 << 128
119
+ return val, offset + 16
120
+
121
+ if t == "pubkey":
122
+ pubkey_bytes = data[offset:offset+32]
123
+ return self._bytes_to_base58(pubkey_bytes), offset + 32
124
+
125
+ raise ValueError(f"Unknown primitive type: {t}")
126
+
127
+ def _bytes_to_base58(self, data: bytes) -> str:
128
+ alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
129
+ n = int.from_bytes(data, 'big')
130
+ result = ''
131
+ while n > 0:
132
+ n, remainder = divmod(n, 58)
133
+ result = alphabet[remainder] + result
134
+
135
+ leading_zeros = 0
136
+ for byte in data:
137
+ if byte == 0:
138
+ leading_zeros += 1
139
+ else:
140
+ break
141
+ return '1' * leading_zeros + result
142
+
143
+ def _read_vec(self, inner, data, offset, idl):
144
+ length, offset = self._read_primitive("u32", data, offset)
145
+ items = []
146
+
147
+ for _ in range(length):
148
+ val, offset = self._read_type(inner, data, offset, idl)
149
+ items.append(val)
150
+
151
+ return items, offset
152
+
153
+ def _read_string(self, data, offset):
154
+ length, offset = self._read_primitive("u32", data, offset)
155
+ s = data[offset:offset+length].decode("utf-8")
156
+ return s, offset + length
157
+
158
+ def _read_bytes(self, data, offset):
159
+ length, offset = self._read_primitive("u32", data, offset)
160
+ return data[offset:offset+length], offset + length
161
+
162
+ def _read_defined(self, name: str, data: bytes, offset: int, idl):
163
+ type_def = None
164
+ for t in idl.get("types", []):
165
+ if t.get("name") == name:
166
+ type_def = t
167
+ break
168
+
169
+ # if not type_def:
170
+ # raise ValueError(f"Type definition not found: {name}")
171
+
172
+ kind = type_def["type"]["kind"]
173
+
174
+ if kind == "struct":
175
+ result = {}
176
+ for field in type_def["type"]["fields"]:
177
+ val, offset = self._read_type(field["type"], data, offset, idl)
178
+ result[field["name"]] = val
179
+ return result, offset
180
+
181
+ if kind == "enum":
182
+ discr, offset = self._read_primitive("u8", data, offset)
183
+ if discr >= len(type_def["type"]["variants"]):
184
+ raise ValueError(f"Invalid discriminator: {discr}")
185
+
186
+ variant = type_def["type"]["variants"][discr]
187
+
188
+ if "fields" not in variant:
189
+ return variant["name"], offset
190
+
191
+ if isinstance(variant["fields"], list):
192
+ values = []
193
+ for f in variant["fields"]:
194
+ if isinstance(f, dict):
195
+ val, offset = self._read_type(f.get("type", f), data, offset, idl)
196
+ else:
197
+ val, offset = self._read_type(f, data, offset, idl)
198
+ values.append(val)
199
+ return {variant["name"]: values}, offset
200
+ else:
201
+ obj = {}
202
+ for f in variant["fields"]:
203
+ val, offset = self._read_type(f["type"], data, offset, idl)
204
+ obj[f["name"]] = val
205
+ return {variant["name"]: obj}, offset
206
+
207
+ def _read_type(self, t, data: bytes, offset: int, idl):
208
+ if isinstance(t, str):
209
+ if t == "string":
210
+ return self._read_string(data, offset)
211
+ if t == "bytes":
212
+ return self._read_bytes(data, offset)
213
+ return self._read_primitive(t, data, offset)
214
+
215
+ if isinstance(t, dict):
216
+
217
+ if t.get("kind") == "struct":
218
+ result = {}
219
+ for field in t.get("fields", []):
220
+ val, offset = self._read_type(field["type"], data, offset, idl)
221
+ result[field["name"]] = val
222
+ return result, offset
223
+
224
+ if t.get("kind") == "enum":
225
+ discr, offset = self._read_primitive("u8", data, offset)
226
+ variant = t["variants"][discr]
227
+
228
+ if "fields" not in variant:
229
+ return variant["name"], offset
230
+
231
+ values = {}
232
+ for f in variant.get("fields", []):
233
+ val, offset = self._read_type(f["type"], data, offset, idl)
234
+ values[f["name"]] = val
235
+
236
+ return {variant["name"]: values}, offset
237
+
238
+ if "option" in t:
239
+ flag, offset = self._read_primitive("u8", data, offset)
240
+ if flag == 0:
241
+ return None, offset
242
+ return self._read_type(t["option"], data, offset, idl)
243
+
244
+ if "vec" in t:
245
+ return self._read_vec(t["vec"], data, offset, idl)
246
+
247
+ if "array" in t:
248
+ inner, size = t["array"]
249
+ arr = []
250
+ for _ in range(size):
251
+ val, offset = self._read_type(inner, data, offset, idl)
252
+ arr.append(val)
253
+ return arr, offset
254
+
255
+ if "defined" in t:
256
+ return self._read_defined(t["defined"], data, offset, idl)
257
+
258
+ raise ValueError(f"Unknown IDL type: {t}")
259
+
260
+ def _attribute_program_data(self, logs: List[str]) -> List[Tuple[Optional[str], str]]:
261
+ """
262
+ Walk the logs' actual invoke/success call stack and return
263
+ (program_id, base64_data) for every `Program data:` line, attributed
264
+ to whichever program was really executing when it was logged.
265
+
266
+ This replaces a heuristic that assumed a `Program data:` line always
267
+ precedes the *next* invoke of the same program - true often enough to
268
+ look right, but wrong on transactions with nested self-CPI event
269
+ logging, where a different program's data line can end up immediately
270
+ before your target program's invoke line.
271
+ """
272
+ stack: List[str] = []
273
+ result: List[Tuple[Optional[str], str]] = []
274
+
275
+ for line in logs:
276
+ if not isinstance(line, str):
277
+ continue
278
+
279
+ m = _INVOKE_RE.match(line)
280
+ if m:
281
+ stack.append(m.group(1))
282
+ continue
283
+
284
+ m = _RESULT_RE.match(line)
285
+ if m and stack and stack[-1] == m.group(1):
286
+ stack.pop()
287
+ continue
288
+
289
+ if line.startswith("Program data:"):
290
+ current = stack[-1] if stack else None
291
+ result.append((current, line.replace("Program data:", "").strip()))
292
+
293
+ return result
294
+
295
+ def extract_program_data(self, tx: Dict, program_address: str) -> Optional[str]:
296
+ """
297
+ Finds the first `Program data:` log line actually emitted while
298
+ `program_address` was executing, using the real invoke/success call
299
+ stack rather than assuming line order.
300
+ """
301
+ params = tx.get("params")
302
+ if not params:
303
+ return None
304
+
305
+ results = params.get("result")
306
+ if not results:
307
+ return None
308
+
309
+ value = results.get("value")
310
+ if not value:
311
+ return None
312
+
313
+ logs = value.get("logs")
314
+ if not isinstance(logs, list):
315
+ return None
316
+
317
+ for prog, data in self._attribute_program_data(logs):
318
+ if prog == program_address:
319
+ return data
320
+
321
+ return None
322
+
323
+ def decode_on_demand(self, tx, program_id):
324
+ """
325
+ Automatically extracts first entry `Program data:` of set program address and returns decoded data
326
+ """
327
+ data = self.extract_program_data(tx, program_id)
328
+ return self.decode(data, program_id) if data else None
329
+
330
+ def extract_all_program_data(self, tx):
331
+ """
332
+ Extracts all `Program data:` strings from tx, so you can use it to `unsafe` parse all your strings using batch_decode()
333
+ """
334
+ params = tx.get("params")
335
+ if not params:
336
+ return None
337
+
338
+ results = params.get("result")
339
+ if not results:
340
+ return None
341
+
342
+ value = results.get("value")
343
+ if not value:
344
+ return None
345
+
346
+ logs = value.get("logs")
347
+ if not isinstance(logs, list):
348
+ return None
349
+
350
+ # (previously `range(len(logs) - 1)`, which silently dropped a
351
+ # `Program data:` line if it happened to be the very last log line)
352
+ return [data for _prog, data in self._attribute_program_data(logs)]
353
+
354
+ def batch_decode(self, extract_all_program_data_result: list, program_address: str) -> Optional[List[dict]]:
355
+ results = {}
356
+ for data in extract_all_program_data_result:
357
+ results[data[:10]] = self.decode(data, program_address)
358
+ return results
359
+
360
+ def _fetch_idl(self, program_address: str) -> Optional[Dict[str, Any]]:
361
+ url = f"https://api-v2.solscan.io/v2/account/anchor_idl?address={program_address}"
362
+
363
+ headers = {
364
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0",
365
+ "Accept": "application/json, text/plain, */*",
366
+ "Origin": "https://solscan.io",
367
+ "Referer": "https://solscan.io/",
368
+ }
369
+
370
+ try:
371
+ response = requests.get(
372
+ url,
373
+ headers=headers,
374
+ timeout=30
375
+ )
376
+
377
+ response.raise_for_status()
378
+ data = response.json()
379
+
380
+ if data.get("success"):
381
+ return data.get("data")
382
+
383
+ return None
384
+
385
+ except Exception as e:
386
+ raise Exception(f"Error fetching IDL for {program_address}: {e}")
@@ -0,0 +1,8 @@
1
+ class TooManyElements(Exception):
2
+ pass
3
+
4
+ class NoElements(Exception):
5
+ pass
6
+
7
+ class NoIDLMatch(Exception):
8
+ pass
@@ -0,0 +1,3 @@
1
+ from .transaction import TransactionManager, TransactionConfig, TransactionStatus, TransactionEncoding
2
+
3
+ __all__ = ["TransactionManager", "TransactionConfig", "TransactionStatus", "TransactionEncoding"]
@@ -0,0 +1,17 @@
1
+ class EncodingNotSupported(Exception):
2
+ pass
3
+
4
+ class TransactionError(Exception):
5
+ pass
6
+
7
+ class TransactionTimeoutError(TransactionError):
8
+ pass
9
+
10
+ class TransactionNotFoundError(TransactionError):
11
+ pass
12
+
13
+ class RPCConnectionError(TransactionError):
14
+ pass
15
+
16
+ class InvalidTransactionError(TransactionError):
17
+ pass