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.
- checkpoint_sdk-0.2.0/LICENSE +21 -0
- checkpoint_sdk-0.2.0/PKG-INFO +99 -0
- checkpoint_sdk-0.2.0/README.md +83 -0
- checkpoint_sdk-0.2.0/pyproject.toml +29 -0
- checkpoint_sdk-0.2.0/setup.cfg +4 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk/__init__.py +19 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk/decoder/__init__.py +3 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk/decoder/decoder.py +386 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk/decoder/exceptions.py +8 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk/transaction/__init__.py +3 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk/transaction/exceptions.py +17 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk/transaction/transaction.py +289 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk/wallet/__init__.py +3 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk/wallet/exceptions.py +2 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk/wallet/wallet.py +248 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk.egg-info/PKG-INFO +99 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk.egg-info/SOURCES.txt +18 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk.egg-info/dependency_links.txt +1 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk.egg-info/requires.txt +4 -0
- checkpoint_sdk-0.2.0/src/checkpoint_sdk.egg-info/top_level.txt +1 -0
|
@@ -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,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,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,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
|