checkpoint-sdk 0.1.0__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.
- checkpoint_sdk/__init__.py +9 -0
- checkpoint_sdk/decoder/__init__.py +0 -0
- checkpoint_sdk/decoder/decoder.py +347 -0
- checkpoint_sdk/decoder/exceptions.py +8 -0
- checkpoint_sdk/transaction/__init__.py +0 -0
- checkpoint_sdk/transaction/exceptions.py +17 -0
- checkpoint_sdk/transaction/transaction.py +289 -0
- checkpoint_sdk/wallet/__init__.py +0 -0
- checkpoint_sdk/wallet/exceptions.py +2 -0
- checkpoint_sdk/wallet/wallet.py +224 -0
- checkpoint_sdk-0.1.0.dist-info/METADATA +29 -0
- checkpoint_sdk-0.1.0.dist-info/RECORD +15 -0
- checkpoint_sdk-0.1.0.dist-info/WHEEL +5 -0
- checkpoint_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- checkpoint_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
### --!-- --!-- --!-- better not to use python in Solana high performance production apps --!-- --!-- --!-- ###
|
|
2
|
+
import requests
|
|
3
|
+
from typing import Optional, Dict, Any, Tuple, List
|
|
4
|
+
from checkpoint.decoder.exceptions import TooManyElements, NoElements
|
|
5
|
+
import base64
|
|
6
|
+
import struct
|
|
7
|
+
|
|
8
|
+
class Decoder:
|
|
9
|
+
MAX = 6
|
|
10
|
+
IDLs = []
|
|
11
|
+
|
|
12
|
+
def __init__(self, programs):
|
|
13
|
+
programs_count = len(programs)
|
|
14
|
+
|
|
15
|
+
if programs_count > self.MAX:
|
|
16
|
+
raise TooManyElements(
|
|
17
|
+
f"Too many programs input! Max programs supported: {self.MAX} | Current input: {programs_count}"
|
|
18
|
+
)
|
|
19
|
+
elif programs_count == 0:
|
|
20
|
+
raise NoElements(
|
|
21
|
+
f"No programs on input! Expected MIN: 1 | MAX: {self.MAX} elements array"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
for program_address in programs:
|
|
25
|
+
idl = self._fetch_idl(program_address)
|
|
26
|
+
self.IDLs.append(idl)
|
|
27
|
+
|
|
28
|
+
def _find_idl(self, program_address: str):
|
|
29
|
+
for idl in self.IDLs:
|
|
30
|
+
idl_address = idl.get("address")
|
|
31
|
+
if idl_address and idl_address == program_address:
|
|
32
|
+
return idl
|
|
33
|
+
|
|
34
|
+
def decode(self, base64_str: str, program_address: str):
|
|
35
|
+
"""
|
|
36
|
+
Decodes input `Program data:` base64 string by program IDL
|
|
37
|
+
"""
|
|
38
|
+
if not isinstance(base64_str, str): raise ValueError(f"Base 64 must be typeof string! Current type: {type(base64_str)} | decode()")
|
|
39
|
+
def normalize_b64(s: str) -> str:
|
|
40
|
+
s = s.strip().split()[-1]
|
|
41
|
+
return s + "=" * (-len(s) % 4)
|
|
42
|
+
|
|
43
|
+
idl = self._find_idl(program_address)
|
|
44
|
+
if not idl:
|
|
45
|
+
raise ValueError(f"IDL not found for program: {program_address}")
|
|
46
|
+
|
|
47
|
+
events = idl.get("events", [])
|
|
48
|
+
|
|
49
|
+
b64_clean = normalize_b64(base64_str)
|
|
50
|
+
base64_raw = base64.b64decode(b64_clean)
|
|
51
|
+
discriminator = list(base64_raw[:8])
|
|
52
|
+
|
|
53
|
+
for event in events:
|
|
54
|
+
if event.get("discriminator") == discriminator:
|
|
55
|
+
name = event.get("name")
|
|
56
|
+
for _type in idl.get("types", []):
|
|
57
|
+
if _type.get("name") == name:
|
|
58
|
+
offset = 8
|
|
59
|
+
result = {}
|
|
60
|
+
if "type" in _type and "fields" in _type["type"]:
|
|
61
|
+
for arg in _type["type"]["fields"]:
|
|
62
|
+
val, offset = self._read_type(arg["type"], base64_raw, offset, idl)
|
|
63
|
+
result[arg["name"]] = val
|
|
64
|
+
return result
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def _read_primitive(self, t: str, data: bytes, offset: int) -> Tuple[Any, int]:
|
|
68
|
+
if t == "bool":
|
|
69
|
+
return data[offset] == 1, offset + 1
|
|
70
|
+
|
|
71
|
+
if t == "u8":
|
|
72
|
+
return data[offset], offset + 1
|
|
73
|
+
if t == "i8":
|
|
74
|
+
return struct.unpack_from("<b", data, offset)[0], offset + 1
|
|
75
|
+
|
|
76
|
+
if t == "u16":
|
|
77
|
+
return struct.unpack_from("<H", data, offset)[0], offset + 2
|
|
78
|
+
if t == "i16":
|
|
79
|
+
return struct.unpack_from("<h", data, offset)[0], offset + 2
|
|
80
|
+
|
|
81
|
+
if t == "u32":
|
|
82
|
+
return struct.unpack_from("<I", data, offset)[0], offset + 4
|
|
83
|
+
if t == "i32":
|
|
84
|
+
return struct.unpack_from("<i", data, offset)[0], offset + 4
|
|
85
|
+
|
|
86
|
+
if t == "u64":
|
|
87
|
+
return struct.unpack_from("<Q", data, offset)[0], offset + 8
|
|
88
|
+
if t == "i64":
|
|
89
|
+
return struct.unpack_from("<q", data, offset)[0], offset + 8
|
|
90
|
+
|
|
91
|
+
if t == "u128":
|
|
92
|
+
lo, hi = struct.unpack_from("<QQ", data, offset)
|
|
93
|
+
return (hi << 64) | lo, offset + 16
|
|
94
|
+
|
|
95
|
+
if t == "i128":
|
|
96
|
+
lo, hi = struct.unpack_from("<QQ", data, offset)
|
|
97
|
+
val = (hi << 64) | lo
|
|
98
|
+
if hi & (1 << 63):
|
|
99
|
+
val -= 1 << 128
|
|
100
|
+
return val, offset + 16
|
|
101
|
+
|
|
102
|
+
if t == "pubkey":
|
|
103
|
+
pubkey_bytes = data[offset:offset+32]
|
|
104
|
+
return self._bytes_to_base58(pubkey_bytes), offset + 32
|
|
105
|
+
|
|
106
|
+
raise ValueError(f"Unknown primitive type: {t}")
|
|
107
|
+
|
|
108
|
+
def _bytes_to_base58(self, data: bytes) -> str:
|
|
109
|
+
alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
110
|
+
n = int.from_bytes(data, 'big')
|
|
111
|
+
result = ''
|
|
112
|
+
while n > 0:
|
|
113
|
+
n, remainder = divmod(n, 58)
|
|
114
|
+
result = alphabet[remainder] + result
|
|
115
|
+
|
|
116
|
+
leading_zeros = 0
|
|
117
|
+
for byte in data:
|
|
118
|
+
if byte == 0:
|
|
119
|
+
leading_zeros += 1
|
|
120
|
+
else:
|
|
121
|
+
break
|
|
122
|
+
return '1' * leading_zeros + result
|
|
123
|
+
|
|
124
|
+
def _read_vec(self, inner, data, offset, idl):
|
|
125
|
+
length, offset = self._read_primitive("u32", data, offset)
|
|
126
|
+
items = []
|
|
127
|
+
|
|
128
|
+
for _ in range(length):
|
|
129
|
+
val, offset = self._read_type(inner, data, offset, idl)
|
|
130
|
+
items.append(val)
|
|
131
|
+
|
|
132
|
+
return items, offset
|
|
133
|
+
|
|
134
|
+
def _read_string(self, data, offset):
|
|
135
|
+
length, offset = self._read_primitive("u32", data, offset)
|
|
136
|
+
s = data[offset:offset+length].decode("utf-8")
|
|
137
|
+
return s, offset + length
|
|
138
|
+
|
|
139
|
+
def _read_bytes(self, data, offset):
|
|
140
|
+
length, offset = self._read_primitive("u32", data, offset)
|
|
141
|
+
return data[offset:offset+length], offset + length
|
|
142
|
+
|
|
143
|
+
def _read_defined(self, name: str, data: bytes, offset: int, idl):
|
|
144
|
+
type_def = None
|
|
145
|
+
for t in idl.get("types", []):
|
|
146
|
+
if t.get("name") == name:
|
|
147
|
+
type_def = t
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
# if not type_def:
|
|
151
|
+
# raise ValueError(f"Type definition not found: {name}")
|
|
152
|
+
|
|
153
|
+
kind = type_def["type"]["kind"]
|
|
154
|
+
|
|
155
|
+
if kind == "struct":
|
|
156
|
+
result = {}
|
|
157
|
+
for field in type_def["type"]["fields"]:
|
|
158
|
+
val, offset = self._read_type(field["type"], data, offset, idl)
|
|
159
|
+
result[field["name"]] = val
|
|
160
|
+
return result, offset
|
|
161
|
+
|
|
162
|
+
if kind == "enum":
|
|
163
|
+
discr, offset = self._read_primitive("u8", data, offset)
|
|
164
|
+
if discr >= len(type_def["type"]["variants"]):
|
|
165
|
+
raise ValueError(f"Invalid discriminator: {discr}")
|
|
166
|
+
|
|
167
|
+
variant = type_def["type"]["variants"][discr]
|
|
168
|
+
|
|
169
|
+
if "fields" not in variant:
|
|
170
|
+
return variant["name"], offset
|
|
171
|
+
|
|
172
|
+
if isinstance(variant["fields"], list):
|
|
173
|
+
values = []
|
|
174
|
+
for f in variant["fields"]:
|
|
175
|
+
if isinstance(f, dict):
|
|
176
|
+
val, offset = self._read_type(f.get("type", f), data, offset, idl)
|
|
177
|
+
else:
|
|
178
|
+
val, offset = self._read_type(f, data, offset, idl)
|
|
179
|
+
values.append(val)
|
|
180
|
+
return {variant["name"]: values}, offset
|
|
181
|
+
else:
|
|
182
|
+
obj = {}
|
|
183
|
+
for f in variant["fields"]:
|
|
184
|
+
val, offset = self._read_type(f["type"], data, offset, idl)
|
|
185
|
+
obj[f["name"]] = val
|
|
186
|
+
return {variant["name"]: obj}, offset
|
|
187
|
+
|
|
188
|
+
def _read_type(self, t, data: bytes, offset: int, idl):
|
|
189
|
+
if isinstance(t, str):
|
|
190
|
+
if t == "string":
|
|
191
|
+
return self._read_string(data, offset)
|
|
192
|
+
if t == "bytes":
|
|
193
|
+
return self._read_bytes(data, offset)
|
|
194
|
+
return self._read_primitive(t, data, offset)
|
|
195
|
+
|
|
196
|
+
if isinstance(t, dict):
|
|
197
|
+
|
|
198
|
+
if t.get("kind") == "struct":
|
|
199
|
+
result = {}
|
|
200
|
+
for field in t.get("fields", []):
|
|
201
|
+
val, offset = self._read_type(field["type"], data, offset, idl)
|
|
202
|
+
result[field["name"]] = val
|
|
203
|
+
return result, offset
|
|
204
|
+
|
|
205
|
+
if t.get("kind") == "enum":
|
|
206
|
+
discr, offset = self._read_primitive("u8", data, offset)
|
|
207
|
+
variant = t["variants"][discr]
|
|
208
|
+
|
|
209
|
+
if "fields" not in variant:
|
|
210
|
+
return variant["name"], offset
|
|
211
|
+
|
|
212
|
+
values = {}
|
|
213
|
+
for f in variant.get("fields", []):
|
|
214
|
+
val, offset = self._read_type(f["type"], data, offset, idl)
|
|
215
|
+
values[f["name"]] = val
|
|
216
|
+
|
|
217
|
+
return {variant["name"]: values}, offset
|
|
218
|
+
|
|
219
|
+
if "option" in t:
|
|
220
|
+
flag, offset = self._read_primitive("u8", data, offset)
|
|
221
|
+
if flag == 0:
|
|
222
|
+
return None, offset
|
|
223
|
+
return self._read_type(t["option"], data, offset, idl)
|
|
224
|
+
|
|
225
|
+
if "vec" in t:
|
|
226
|
+
return self._read_vec(t["vec"], data, offset, idl)
|
|
227
|
+
|
|
228
|
+
if "array" in t:
|
|
229
|
+
inner, size = t["array"]
|
|
230
|
+
arr = []
|
|
231
|
+
for _ in range(size):
|
|
232
|
+
val, offset = self._read_type(inner, data, offset, idl)
|
|
233
|
+
arr.append(val)
|
|
234
|
+
return arr, offset
|
|
235
|
+
|
|
236
|
+
if "defined" in t:
|
|
237
|
+
return self._read_defined(t["defined"], data, offset, idl)
|
|
238
|
+
|
|
239
|
+
raise ValueError(f"Unknown IDL type: {t}")
|
|
240
|
+
|
|
241
|
+
def extract_program_data(self, tx: Dict, program_address: str) -> Optional[str]:
|
|
242
|
+
"""
|
|
243
|
+
This function only works in some cases!!!
|
|
244
|
+
Be care using it, not always we can find correct str, so in most cases you need to find base64 data at your own
|
|
245
|
+
"""
|
|
246
|
+
params = tx.get("params")
|
|
247
|
+
if not params:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
results = params.get("result")
|
|
251
|
+
if not results:
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
value = results.get("value")
|
|
255
|
+
if not value:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
logs = value.get("logs")
|
|
259
|
+
if not isinstance(logs, list):
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
for i in range(len(logs) - 1):
|
|
263
|
+
line = logs[i]
|
|
264
|
+
next_line = logs[i + 1]
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
isinstance(line, str)
|
|
268
|
+
and line.startswith("Program data:")
|
|
269
|
+
and isinstance(next_line, str)
|
|
270
|
+
and next_line.startswith(f"Program {program_address} invoke")
|
|
271
|
+
):
|
|
272
|
+
return line.replace("Program data:", "").strip()
|
|
273
|
+
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
def decode_on_demand(self, tx, program_id):
|
|
277
|
+
"""
|
|
278
|
+
Automatically extracts first entry `Program data:` of set program address and returns decoded data
|
|
279
|
+
"""
|
|
280
|
+
data = self.extract_program_data(tx, program_id)
|
|
281
|
+
return self.decode(data, program_id) if data else None
|
|
282
|
+
|
|
283
|
+
def extract_all_program_data(self, tx):
|
|
284
|
+
"""
|
|
285
|
+
Extracts all `Program data:` strings from tx, so you can use it to `unsafe` parse all your strings using batch_decode()
|
|
286
|
+
"""
|
|
287
|
+
result = []
|
|
288
|
+
params = tx.get("params")
|
|
289
|
+
if not params:
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
results = params.get("result")
|
|
293
|
+
if not results:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
value = results.get("value")
|
|
297
|
+
if not value:
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
logs = value.get("logs")
|
|
301
|
+
if not isinstance(logs, list):
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
for i in range(len(logs) - 1):
|
|
305
|
+
line = logs[i]
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
isinstance(line, str)
|
|
309
|
+
and line.startswith("Program data:")
|
|
310
|
+
):
|
|
311
|
+
result.append(line.replace("Program data:", "").strip())
|
|
312
|
+
|
|
313
|
+
return result
|
|
314
|
+
|
|
315
|
+
def batch_decode(self, extract_all_program_data_result: list, program_address: str) -> Optional[List[dict]]:
|
|
316
|
+
results = {}
|
|
317
|
+
for data in extract_all_program_data_result:
|
|
318
|
+
results[data[:10]] = self.decode(data, program_address)
|
|
319
|
+
return results
|
|
320
|
+
|
|
321
|
+
def _fetch_idl(self, program_address: str) -> Optional[Dict[str, Any]]:
|
|
322
|
+
url = f"https://api-v2.solscan.io/v2/account/anchor_idl?address={program_address}"
|
|
323
|
+
|
|
324
|
+
headers = {
|
|
325
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0",
|
|
326
|
+
"Accept": "application/json, text/plain, */*",
|
|
327
|
+
"Origin": "https://solscan.io",
|
|
328
|
+
"Referer": "https://solscan.io/",
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
response = requests.get(
|
|
333
|
+
url,
|
|
334
|
+
headers=headers,
|
|
335
|
+
timeout=30
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
response.raise_for_status()
|
|
339
|
+
data = response.json()
|
|
340
|
+
|
|
341
|
+
if data.get("success"):
|
|
342
|
+
return data.get("data")
|
|
343
|
+
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
raise Exception(f"Error fetching IDL for {program_address}: {e}")
|
|
File without changes
|
|
@@ -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
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Any, Union
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from checkpoint.transaction.exceptions import *
|
|
8
|
+
|
|
9
|
+
class TransactionStatus(str, Enum):
|
|
10
|
+
CONFIRMED = "confirmed"
|
|
11
|
+
FINALIZED = "finalized"
|
|
12
|
+
PROCESSED = "processed"
|
|
13
|
+
|
|
14
|
+
class TransactionEncoding(str, Enum):
|
|
15
|
+
BASE64 = "base64"
|
|
16
|
+
BASE58 = "base58"
|
|
17
|
+
JSON_PARSED = "jsonParsed"
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TransactionConfig:
|
|
21
|
+
encoding: TransactionEncoding = TransactionEncoding.JSON_PARSED
|
|
22
|
+
commitment: TransactionStatus = TransactionStatus.CONFIRMED
|
|
23
|
+
max_supported_transaction_version: int = 0
|
|
24
|
+
timeout: int = 30
|
|
25
|
+
|
|
26
|
+
class TransactionManager:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
rpc_url: str = "https://api.mainnet-beta.solana.com",
|
|
30
|
+
headers: Optional[Dict[str, str]] = None,
|
|
31
|
+
config: Optional[TransactionConfig] = None
|
|
32
|
+
):
|
|
33
|
+
self.rpc_url = rpc_url
|
|
34
|
+
self.headers = headers or {"Content-Type": "application/json"}
|
|
35
|
+
self.config = config or TransactionConfig()
|
|
36
|
+
self.session = requests.Session()
|
|
37
|
+
|
|
38
|
+
if self.headers:
|
|
39
|
+
self.session.headers.update(self.headers)
|
|
40
|
+
|
|
41
|
+
def _validate_encoding(self, encoding: str) -> None:
|
|
42
|
+
if encoding not in [e.value for e in TransactionEncoding]:
|
|
43
|
+
available = ", ".join([e.value for e in TransactionEncoding])
|
|
44
|
+
raise EncodingNotSupported(
|
|
45
|
+
f"Encoding '{encoding}' not supported. Available: {available}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def _make_request(self, method: str, params: List[Any]) -> Dict[str, Any]:
|
|
49
|
+
payload = {
|
|
50
|
+
"jsonrpc": "2.0",
|
|
51
|
+
"id": 1,
|
|
52
|
+
"method": method,
|
|
53
|
+
"params": params
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
response = self.session.post(
|
|
58
|
+
self.rpc_url,
|
|
59
|
+
json=payload,
|
|
60
|
+
timeout=self.config.timeout
|
|
61
|
+
)
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
return response.json()
|
|
64
|
+
except requests.exceptions.Timeout as e:
|
|
65
|
+
raise TransactionTimeoutError(f"Request timeout exceeded: {str(e)}")
|
|
66
|
+
except requests.exceptions.RequestException as e:
|
|
67
|
+
raise RPCConnectionError(f"RPC connection error: {str(e)}")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise RPCConnectionError(f"Unexpected error: {str(e)}")
|
|
70
|
+
|
|
71
|
+
def get_transaction(
|
|
72
|
+
self,
|
|
73
|
+
signature: str,
|
|
74
|
+
encoding: Optional[str] = None,
|
|
75
|
+
commitment: Optional[str] = None
|
|
76
|
+
) -> Dict[str, Any]:
|
|
77
|
+
encoding = encoding or self.config.encoding.value
|
|
78
|
+
commitment = commitment or self.config.commitment.value
|
|
79
|
+
|
|
80
|
+
self._validate_encoding(encoding)
|
|
81
|
+
|
|
82
|
+
params = [
|
|
83
|
+
signature,
|
|
84
|
+
{
|
|
85
|
+
"encoding": encoding,
|
|
86
|
+
"commitment": commitment,
|
|
87
|
+
"maxSupportedTransactionVersion": self.config.max_supported_transaction_version
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
return self._make_request("getTransaction", params)
|
|
92
|
+
|
|
93
|
+
def get_transaction_batch(
|
|
94
|
+
self,
|
|
95
|
+
signatures: List[str],
|
|
96
|
+
encoding: Optional[str] = None,
|
|
97
|
+
commitment: Optional[str] = None
|
|
98
|
+
) -> List[Dict[str, Any]]:
|
|
99
|
+
encoding = encoding or self.config.encoding.value
|
|
100
|
+
commitment = commitment or self.config.commitment.value
|
|
101
|
+
|
|
102
|
+
self._validate_encoding(encoding)
|
|
103
|
+
|
|
104
|
+
payload = [
|
|
105
|
+
{
|
|
106
|
+
"jsonrpc": "2.0",
|
|
107
|
+
"id": i,
|
|
108
|
+
"method": "getTransaction",
|
|
109
|
+
"params": [
|
|
110
|
+
sig,
|
|
111
|
+
{
|
|
112
|
+
"encoding": encoding,
|
|
113
|
+
"commitment": commitment,
|
|
114
|
+
"maxSupportedTransactionVersion": self.config.max_supported_transaction_version
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
for i, sig in enumerate(signatures)
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
response = self.session.post(
|
|
123
|
+
self.rpc_url,
|
|
124
|
+
json=payload,
|
|
125
|
+
timeout=self.config.timeout
|
|
126
|
+
)
|
|
127
|
+
response.raise_for_status()
|
|
128
|
+
return response.json()
|
|
129
|
+
except requests.exceptions.RequestException as e:
|
|
130
|
+
raise RPCConnectionError(f"Batch request failed: {str(e)}")
|
|
131
|
+
|
|
132
|
+
def get_signatures_for_address(
|
|
133
|
+
self,
|
|
134
|
+
address: str,
|
|
135
|
+
limit: int = 1000,
|
|
136
|
+
before: Optional[str] = None,
|
|
137
|
+
until: Optional[str] = None,
|
|
138
|
+
commitment: Optional[str] = None
|
|
139
|
+
) -> Dict[str, Any]:
|
|
140
|
+
commitment = commitment or self.config.commitment.value
|
|
141
|
+
|
|
142
|
+
params = [address]
|
|
143
|
+
options = {"limit": limit}
|
|
144
|
+
|
|
145
|
+
if before:
|
|
146
|
+
options["before"] = before
|
|
147
|
+
if until:
|
|
148
|
+
options["until"] = until
|
|
149
|
+
|
|
150
|
+
if options:
|
|
151
|
+
params.append(options)
|
|
152
|
+
|
|
153
|
+
return self._make_request("getSignaturesForAddress", params)
|
|
154
|
+
|
|
155
|
+
def get_transaction_history(
|
|
156
|
+
self,
|
|
157
|
+
address: str,
|
|
158
|
+
limit: int = 10,
|
|
159
|
+
encoding: Optional[str] = None
|
|
160
|
+
) -> List[Dict[str, Any]]:
|
|
161
|
+
signatures_response = self.get_signatures_for_address(address, limit=limit)
|
|
162
|
+
|
|
163
|
+
if "error" in signatures_response:
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
signatures = [sig["signature"] for sig in signatures_response.get("result", [])]
|
|
167
|
+
|
|
168
|
+
return self.get_transaction_batch(signatures, encoding=encoding)
|
|
169
|
+
|
|
170
|
+
def get_latest_blockhash(
|
|
171
|
+
self,
|
|
172
|
+
commitment: Optional[str] = None
|
|
173
|
+
) -> Dict[str, Any]:
|
|
174
|
+
commitment = commitment or self.config.commitment.value
|
|
175
|
+
|
|
176
|
+
return self._make_request("getLatestBlockhash", [{"commitment": commitment}])
|
|
177
|
+
|
|
178
|
+
def confirm_transaction(
|
|
179
|
+
self,
|
|
180
|
+
signature: str,
|
|
181
|
+
commitment: Optional[str] = None,
|
|
182
|
+
timeout: int = 30,
|
|
183
|
+
poll_interval: int = 1
|
|
184
|
+
) -> bool:
|
|
185
|
+
import time
|
|
186
|
+
|
|
187
|
+
commitment = commitment or self.config.commitment.value
|
|
188
|
+
end_time = time.time() + timeout
|
|
189
|
+
|
|
190
|
+
while time.time() < end_time:
|
|
191
|
+
try:
|
|
192
|
+
result = self.get_transaction(signature, commitment=commitment)
|
|
193
|
+
|
|
194
|
+
if result.get("result") is not None:
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
time.sleep(poll_interval)
|
|
198
|
+
except Exception:
|
|
199
|
+
time.sleep(poll_interval)
|
|
200
|
+
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
def parse_transaction_data(
|
|
204
|
+
self,
|
|
205
|
+
transaction_data: Dict[str, Any],
|
|
206
|
+
format_output: bool = False
|
|
207
|
+
) -> Union[Dict[str, Any], str]:
|
|
208
|
+
|
|
209
|
+
if "error" in transaction_data:
|
|
210
|
+
return transaction_data
|
|
211
|
+
|
|
212
|
+
result = transaction_data.get("result", {})
|
|
213
|
+
|
|
214
|
+
if not format_output:
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
parsed = {
|
|
218
|
+
"signature": result.get("transaction", {}).get("signatures", [""])[0],
|
|
219
|
+
"slot": result.get("slot"),
|
|
220
|
+
"block_time": datetime.fromtimestamp(result.get("blockTime", 0))
|
|
221
|
+
if result.get("blockTime") else None,
|
|
222
|
+
"fee": result.get("meta", {}).get("fee"),
|
|
223
|
+
"status": "Success" if result.get("meta", {}).get("err") is None else "Failed",
|
|
224
|
+
"instructions_count": len(result.get("transaction", {}).get("message", {}).get("instructions", []))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return parsed
|
|
228
|
+
|
|
229
|
+
def simulate_transaction(
|
|
230
|
+
self,
|
|
231
|
+
transaction_data: str,
|
|
232
|
+
commitment: Optional[str] = None
|
|
233
|
+
) -> Dict[str, Any]:
|
|
234
|
+
commitment = commitment or self.config.commitment.value
|
|
235
|
+
|
|
236
|
+
params = [
|
|
237
|
+
transaction_data,
|
|
238
|
+
{
|
|
239
|
+
"encoding": "base64",
|
|
240
|
+
"commitment": commitment
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
return self._make_request("simulateTransaction", params)
|
|
245
|
+
|
|
246
|
+
def get_transaction_cost(
|
|
247
|
+
self,
|
|
248
|
+
transaction_data: str
|
|
249
|
+
) -> Dict[str, Any]:
|
|
250
|
+
simulation = self.simulate_transaction(transaction_data)
|
|
251
|
+
|
|
252
|
+
if "error" in simulation:
|
|
253
|
+
return {"error": simulation["error"]}
|
|
254
|
+
|
|
255
|
+
result = simulation.get("result", {})
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
"compute_units_consumed": result.get("unitsConsumed"),
|
|
259
|
+
"fee": result.get("fee"),
|
|
260
|
+
"logs": result.get("logs", [])
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
def get_program_accounts(
|
|
264
|
+
self,
|
|
265
|
+
program_id: str,
|
|
266
|
+
encoding: Optional[str] = None,
|
|
267
|
+
commitment: Optional[str] = None
|
|
268
|
+
) -> Dict[str, Any]:
|
|
269
|
+
encoding = encoding or self.config.encoding.value
|
|
270
|
+
commitment = commitment or self.config.commitment.value
|
|
271
|
+
|
|
272
|
+
params = [
|
|
273
|
+
program_id,
|
|
274
|
+
{
|
|
275
|
+
"encoding": encoding,
|
|
276
|
+
"commitment": commitment
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
return self._make_request("getProgramAccounts", params)
|
|
281
|
+
|
|
282
|
+
def close(self):
|
|
283
|
+
self.session.close()
|
|
284
|
+
|
|
285
|
+
def __enter__(self):
|
|
286
|
+
return self
|
|
287
|
+
|
|
288
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
289
|
+
self.close()
|
|
File without changes
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from solders.pubkey import Pubkey
|
|
2
|
+
from solders.keypair import Keypair
|
|
3
|
+
from solders.system_program import TransferParams, transfer
|
|
4
|
+
from solana.rpc.api import Client
|
|
5
|
+
from solders.transaction import Transaction
|
|
6
|
+
import base58
|
|
7
|
+
from typing import Dict, Optional, Union
|
|
8
|
+
from checkpoint.wallet.exceptions import InsufficientBalance
|
|
9
|
+
|
|
10
|
+
class TransferConfig:
|
|
11
|
+
def __init__(self, fixed_fee_lamports: int = 0, min_amount_lamports: int = 0,
|
|
12
|
+
max_amount_lamports: int = 2**63-1, enabled: bool = True):
|
|
13
|
+
self.fixed_fee_lamports = fixed_fee_lamports
|
|
14
|
+
self.min_amount_lamports = min_amount_lamports
|
|
15
|
+
self.max_amount_lamports = max_amount_lamports
|
|
16
|
+
self.enabled = enabled
|
|
17
|
+
|
|
18
|
+
class WalletManager:
|
|
19
|
+
def __init__(self, rpc_url: str = "https://api.mainnet-beta.solana.com", base_wallet_keypair: Optional[Keypair] = None):
|
|
20
|
+
self.client = Client(rpc_url)
|
|
21
|
+
self.wallets: Dict[str, Keypair] = {}
|
|
22
|
+
self.transfer_config = TransferConfig()
|
|
23
|
+
self.transaction_history = []
|
|
24
|
+
|
|
25
|
+
if base_wallet_keypair:
|
|
26
|
+
self.base_wallet = base_wallet_keypair
|
|
27
|
+
wallet_id = str(base_wallet_keypair.pubkey())
|
|
28
|
+
self.wallets[wallet_id] = base_wallet_keypair
|
|
29
|
+
else:
|
|
30
|
+
self.base_wallet = Keypair()
|
|
31
|
+
wallet_id = str(self.base_wallet.pubkey())
|
|
32
|
+
self.wallets[wallet_id] = self.base_wallet
|
|
33
|
+
|
|
34
|
+
def create_wallet(self) -> str:
|
|
35
|
+
new_keypair = Keypair()
|
|
36
|
+
wallet_id = str(new_keypair.pubkey())
|
|
37
|
+
self.wallets[wallet_id] = new_keypair
|
|
38
|
+
return wallet_id
|
|
39
|
+
|
|
40
|
+
def import_wallet_from_private_key(self, private_key_base58: str) -> str:
|
|
41
|
+
secret_key = base58.b58decode(private_key_base58)
|
|
42
|
+
keypair = Keypair.from_bytes(secret_key)
|
|
43
|
+
wallet_id = str(keypair.pubkey())
|
|
44
|
+
self.wallets[wallet_id] = keypair
|
|
45
|
+
return wallet_id
|
|
46
|
+
|
|
47
|
+
def get_balance(self, wallet_id: str) -> int:
|
|
48
|
+
if wallet_id not in self.wallets:
|
|
49
|
+
raise ValueError(f"Wallet not found")
|
|
50
|
+
|
|
51
|
+
pubkey = Pubkey.from_string(wallet_id)
|
|
52
|
+
response = self.client.get_balance(pubkey)
|
|
53
|
+
return response.value
|
|
54
|
+
|
|
55
|
+
def get_balance_sol(self, wallet_id: str) -> float:
|
|
56
|
+
lamports = self.get_balance(wallet_id)
|
|
57
|
+
return lamports / 1_000_000_000
|
|
58
|
+
|
|
59
|
+
def set_transfer_config(self, fixed_fee_sol: float = 0.0, min_amount_sol: float = 0.0,
|
|
60
|
+
max_amount_sol: float = float('inf'), enabled: bool = True):
|
|
61
|
+
fixed_fee_lamports = int(fixed_fee_sol * 1_000_000_000)
|
|
62
|
+
min_amount_lamports = int(min_amount_sol * 1_000_000_000)
|
|
63
|
+
max_amount_lamports = int(max_amount_sol * 1_000_000_000) if max_amount_sol != float('inf') else 2**63-1
|
|
64
|
+
|
|
65
|
+
self.transfer_config = TransferConfig(fixed_fee_lamports, min_amount_lamports, max_amount_lamports, enabled)
|
|
66
|
+
|
|
67
|
+
def transfer(self, from_wallet_id: str, to_wallet_id: str, amount_sol: float,
|
|
68
|
+
use_config: bool = False, custom_config: Optional[TransferConfig] = None) -> str:
|
|
69
|
+
if from_wallet_id not in self.wallets:
|
|
70
|
+
raise ValueError(f"Sender wallet not found")
|
|
71
|
+
|
|
72
|
+
from_keypair = self.wallets[from_wallet_id]
|
|
73
|
+
to_pubkey = Pubkey.from_string(to_wallet_id)
|
|
74
|
+
|
|
75
|
+
amount_lamports = int(amount_sol * 1_000_000_000)
|
|
76
|
+
|
|
77
|
+
config = None
|
|
78
|
+
if use_config:
|
|
79
|
+
config = custom_config if custom_config else self.transfer_config
|
|
80
|
+
|
|
81
|
+
if config and config.enabled:
|
|
82
|
+
if amount_lamports < config.min_amount_lamports:
|
|
83
|
+
raise ValueError(f"Transfer amount too small")
|
|
84
|
+
if amount_lamports > config.max_amount_lamports:
|
|
85
|
+
raise ValueError(f"Transfer amount too large")
|
|
86
|
+
|
|
87
|
+
fee_lamports = config.fixed_fee_lamports if config and config.enabled else 0
|
|
88
|
+
total_lamports = amount_lamports + fee_lamports
|
|
89
|
+
|
|
90
|
+
balance = self.get_balance(from_wallet_id)
|
|
91
|
+
if balance < total_lamports:
|
|
92
|
+
raise InsufficientBalance(f"Insufficient SOL balance")
|
|
93
|
+
|
|
94
|
+
transaction = Transaction()
|
|
95
|
+
transaction.add(
|
|
96
|
+
transfer(
|
|
97
|
+
TransferParams(
|
|
98
|
+
from_pubkey=from_keypair.pubkey(),
|
|
99
|
+
to_pubkey=to_pubkey,
|
|
100
|
+
lamports=amount_lamports
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if fee_lamports > 0:
|
|
106
|
+
fee_receiver = Pubkey.from_string("11111111111111111111111111111111")
|
|
107
|
+
transaction.add(
|
|
108
|
+
transfer(
|
|
109
|
+
TransferParams(
|
|
110
|
+
from_pubkey=from_keypair.pubkey(),
|
|
111
|
+
to_pubkey=fee_receiver,
|
|
112
|
+
lamports=fee_lamports
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
result = self.client.send_transaction(transaction, from_keypair)
|
|
118
|
+
signature = str(result.value)
|
|
119
|
+
|
|
120
|
+
transaction_record = {
|
|
121
|
+
'from': from_wallet_id,
|
|
122
|
+
'to': to_wallet_id,
|
|
123
|
+
'amount_sol': amount_sol,
|
|
124
|
+
'amount_lamports': amount_lamports,
|
|
125
|
+
'fee_sol': fee_lamports / 1_000_000_000,
|
|
126
|
+
'fee_lamports': fee_lamports,
|
|
127
|
+
'signature': signature,
|
|
128
|
+
'timestamp': self._get_timestamp()
|
|
129
|
+
}
|
|
130
|
+
self.transaction_history.append(transaction_record)
|
|
131
|
+
|
|
132
|
+
return signature
|
|
133
|
+
|
|
134
|
+
def instant_transfer(self, from_wallet_id: str, to_wallet_id: str, amount_sol: float,
|
|
135
|
+
use_config: bool = False, custom_config: Optional[TransferConfig] = None) -> Dict:
|
|
136
|
+
try:
|
|
137
|
+
signature = self.transfer(from_wallet_id, to_wallet_id, amount_sol, use_config, custom_config)
|
|
138
|
+
|
|
139
|
+
from_balance = self.get_balance_sol(from_wallet_id)
|
|
140
|
+
to_balance = self.get_balance_sol(to_wallet_id)
|
|
141
|
+
|
|
142
|
+
result = {
|
|
143
|
+
'status': 'success',
|
|
144
|
+
'signature': signature,
|
|
145
|
+
'explorer_url': f"https://solscan.io/tx/{signature}",
|
|
146
|
+
'from_wallet': from_wallet_id,
|
|
147
|
+
'to_wallet': to_wallet_id,
|
|
148
|
+
'amount_sol': amount_sol,
|
|
149
|
+
'from_balance_sol': from_balance,
|
|
150
|
+
'to_balance_sol': to_balance
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
except (InsufficientBalance, ValueError) as e:
|
|
156
|
+
result = {
|
|
157
|
+
'status': 'error',
|
|
158
|
+
'message': str(e),
|
|
159
|
+
'from_wallet': from_wallet_id,
|
|
160
|
+
'to_wallet': to_wallet_id
|
|
161
|
+
}
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
def airdrop(self, wallet_id: str, amount_sol: float = 1.0) -> str:
|
|
165
|
+
if wallet_id not in self.wallets:
|
|
166
|
+
raise ValueError(f"Wallet not found")
|
|
167
|
+
|
|
168
|
+
pubkey = Pubkey.from_string(wallet_id)
|
|
169
|
+
amount_lamports = int(amount_sol * 1_000_000_000)
|
|
170
|
+
|
|
171
|
+
result = self.client.request_airdrop(pubkey, amount_lamports)
|
|
172
|
+
return str(result.value)
|
|
173
|
+
|
|
174
|
+
def get_recent_blockhash(self) -> str:
|
|
175
|
+
response = self.client.get_recent_blockhash()
|
|
176
|
+
return str(response.value.blockhash)
|
|
177
|
+
|
|
178
|
+
def estimate_transfer_fee(self, amount_sol: float, use_config: bool = False) -> float:
|
|
179
|
+
amount_lamports = int(amount_sol * 1_000_000_000)
|
|
180
|
+
|
|
181
|
+
config = None
|
|
182
|
+
if use_config:
|
|
183
|
+
config = self.transfer_config
|
|
184
|
+
|
|
185
|
+
fee_lamports = config.fixed_fee_lamports if config and config.enabled else 0
|
|
186
|
+
|
|
187
|
+
response = self.client.get_fee_for_message(self.client.get_recent_blockhash().value.blockhash)
|
|
188
|
+
priority_fee = response.value or 0
|
|
189
|
+
|
|
190
|
+
total_fee_lamports = fee_lamports + priority_fee
|
|
191
|
+
return total_fee_lamports / 1_000_000_000
|
|
192
|
+
|
|
193
|
+
def get_wallet_private_key(self, wallet_id: str) -> str:
|
|
194
|
+
if wallet_id not in self.wallets:
|
|
195
|
+
raise ValueError(f"Wallet not found")
|
|
196
|
+
|
|
197
|
+
keypair = self.wallets[wallet_id]
|
|
198
|
+
return base58.b58encode(bytes(keypair)).decode('utf-8')
|
|
199
|
+
|
|
200
|
+
def get_wallet_public_key(self, wallet_id: str) -> str:
|
|
201
|
+
if wallet_id not in self.wallets:
|
|
202
|
+
raise ValueError(f"Wallet not found")
|
|
203
|
+
|
|
204
|
+
return wallet_id
|
|
205
|
+
|
|
206
|
+
def get_transaction_history(self, wallet_id: Optional[str] = None) -> list:
|
|
207
|
+
if wallet_id:
|
|
208
|
+
return [t for t in self.transaction_history
|
|
209
|
+
if t['from'] == wallet_id or t['to'] == wallet_id]
|
|
210
|
+
return self.transaction_history
|
|
211
|
+
|
|
212
|
+
def _get_timestamp(self) -> str:
|
|
213
|
+
from datetime import datetime
|
|
214
|
+
return datetime.now().isoformat()
|
|
215
|
+
|
|
216
|
+
def get_network_info(self) -> Dict:
|
|
217
|
+
version = self.client.get_version()
|
|
218
|
+
slot = self.client.get_slot()
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
'network': str(self.client._provider.endpoint_uri),
|
|
222
|
+
'solana_version': str(version.value),
|
|
223
|
+
'current_slot': slot.value
|
|
224
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: checkpoint-sdk
|
|
3
|
+
Version: 0.1.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
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install checkpoint-sdk
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
checkpoint_sdk/__init__.py,sha256=IBWcv9CmWNz-58jUzJ2Kw-8PFfziTwVmnU9kJAwCH2Y,211
|
|
2
|
+
checkpoint_sdk/decoder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
checkpoint_sdk/decoder/decoder.py,sha256=LRhQVpmJGhp9JV9e_UiWlFFufkVtbU_ykr7uD7xkix8,12234
|
|
4
|
+
checkpoint_sdk/decoder/exceptions.py,sha256=_79SdYcipOtARlma6xxbyevoBSLD5gsStfqmv3-sCp8,120
|
|
5
|
+
checkpoint_sdk/transaction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
checkpoint_sdk/transaction/exceptions.py,sha256=-xvU_x5sbZmqSlws5tzco4To8a8IY9m2hJakQ4ya4oQ,324
|
|
7
|
+
checkpoint_sdk/transaction/transaction.py,sha256=K7KW40ws5eGLWiCYlMWhDbVGErDSZqqawwBjK9FoYS4,8807
|
|
8
|
+
checkpoint_sdk/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
checkpoint_sdk/wallet/exceptions.py,sha256=OKUjDRUrTAaboyipffMbtgrBveTXQ-7Hs1RLDS1AXkU,46
|
|
10
|
+
checkpoint_sdk/wallet/wallet.py,sha256=uDKYybYkI3InuqdIO5K0wFEb72YMiJpb0_4UOSZMV6Y,8939
|
|
11
|
+
checkpoint_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=sj7YvNopsXtWmDnVHfpLIDLW4GepOVBTz_7ZmPzs1y0,1064
|
|
12
|
+
checkpoint_sdk-0.1.0.dist-info/METADATA,sha256=IizF-exI8AY595owFo5vk5PZ1IQJLa8IljXiz9cZxPo,651
|
|
13
|
+
checkpoint_sdk-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
checkpoint_sdk-0.1.0.dist-info/top_level.txt,sha256=nh91TPgvkL3xy4qPQRZ3FYRXryJR_U8wDbBal86_4yk,15
|
|
15
|
+
checkpoint_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
checkpoint_sdk
|