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.
@@ -0,0 +1,9 @@
1
+ from .decoder.decoder import Decoder
2
+ from .transaction.transaction import TransactionManager
3
+ from .wallet.wallet import WalletManager
4
+
5
+ __all__ = [
6
+ "Decoder",
7
+ "TransactionManager",
8
+ "WalletManager",
9
+ ]
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}")
@@ -0,0 +1,8 @@
1
+ class TooManyElements(Exception):
2
+ pass
3
+
4
+ class NoElements(Exception):
5
+ pass
6
+
7
+ class NoIDLMatch(Exception):
8
+ pass
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,2 @@
1
+ class InsufficientBalance(Exception):
2
+ pass
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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