chipi-stack 2.0.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.
chipi_sdk/client.py ADDED
@@ -0,0 +1,505 @@
1
+ """HTTP client for Chipi API interactions."""
2
+
3
+ from typing import Any, Dict, Optional, TypeVar
4
+ import httpx
5
+
6
+ from .models.core import ChipiSDKConfig
7
+ from .errors import ChipiAuthError, ChipiApiError, handle_api_error
8
+ from .validators import is_valid_api_key, validate_error_response
9
+ from .constants import API_VERSION, API_VERSION_DATE, STARKNET_NETWORKS
10
+
11
+ T = TypeVar('T')
12
+
13
+
14
+ class ChipiClient:
15
+ """HTTP client with sync and async support."""
16
+
17
+ def __init__(self, config: ChipiSDKConfig):
18
+ """
19
+ Initialize the Chipi HTTP client.
20
+
21
+ Args:
22
+ config: SDK configuration
23
+
24
+ Raises:
25
+ ChipiAuthError: If API key is invalid
26
+ """
27
+ if not is_valid_api_key(config.api_public_key):
28
+ raise ChipiAuthError("Invalid API key format")
29
+
30
+ self.api_public_key = config.api_public_key
31
+ self.custom_alpha_url = config.alpha_url
32
+ self.base_url = self._get_base_url()
33
+ self.node_url = config.node_url or STARKNET_NETWORKS["MAINNET"]
34
+ self.sdk_version = "2.0.0"
35
+
36
+ # Initialize HTTP clients
37
+ self._sync_client = httpx.Client(timeout=30.0)
38
+ self._async_client = httpx.AsyncClient(timeout=30.0)
39
+
40
+ def get_api_public_key(self) -> str:
41
+ """Get the API public key (for internal SDK use)."""
42
+ return self.api_public_key
43
+
44
+ def _get_base_url(self) -> str:
45
+ """Construct the base API URL."""
46
+ version = f"v{API_VERSION}"
47
+
48
+ if self.custom_alpha_url:
49
+ # Remove any existing version suffix and add the current one
50
+ clean_url = self.custom_alpha_url.rstrip('/')
51
+ # Remove existing version if present
52
+ if clean_url.endswith(f'/v{API_VERSION}'):
53
+ clean_url = clean_url[:-len(f'/v{API_VERSION}')]
54
+ return f"{clean_url}/{version}"
55
+
56
+ return f"https://api.chipipay.com/{version}"
57
+
58
+ def _add_version_params(self, params: Dict[str, Any]) -> Dict[str, Any]:
59
+ """
60
+ Add API version query parameters.
61
+
62
+ Args:
63
+ params: Existing parameters
64
+
65
+ Returns:
66
+ Parameters with version info added
67
+ """
68
+ if params is None:
69
+ params = {}
70
+ params["__chipi_api_version"] = API_VERSION_DATE
71
+ return params
72
+
73
+ def _get_headers(self, bearer_token: Optional[str] = None) -> Dict[str, str]:
74
+ """
75
+ Get HTTP headers for requests.
76
+
77
+ Args:
78
+ bearer_token: Optional bearer token for authentication
79
+
80
+ Returns:
81
+ Dictionary of headers
82
+ """
83
+ headers = {
84
+ "Content-Type": "application/json",
85
+ "x-api-key": self.api_public_key,
86
+ }
87
+
88
+ if bearer_token:
89
+ headers["Authorization"] = f"Bearer {bearer_token}"
90
+
91
+ return headers
92
+
93
+ def get(
94
+ self,
95
+ endpoint: str,
96
+ params: Optional[Dict[str, Any]] = None,
97
+ bearer_token: Optional[str] = None,
98
+ ) -> Any:
99
+ """
100
+ Synchronous GET request.
101
+
102
+ Args:
103
+ endpoint: API endpoint path
104
+ params: Query parameters
105
+ bearer_token: Optional bearer token
106
+
107
+ Returns:
108
+ Response data
109
+
110
+ Raises:
111
+ ChipiApiError: If request fails
112
+ """
113
+ try:
114
+ url = f"{self.base_url}{endpoint}"
115
+
116
+ # Add version tracking parameters
117
+ params = self._add_version_params(params or {})
118
+
119
+ # Filter out None values
120
+ params = {k: v for k, v in params.items() if v is not None}
121
+
122
+ response = self._sync_client.get(
123
+ url,
124
+ params=params,
125
+ headers=self._get_headers(bearer_token),
126
+ )
127
+
128
+ data = response.json()
129
+
130
+ if not response.is_success:
131
+ error_data = validate_error_response(data)
132
+ raise ChipiApiError(
133
+ error_data["message"],
134
+ error_data.get("code", f"HTTP_{response.status_code}"),
135
+ response.status_code,
136
+ )
137
+
138
+ return data
139
+ except ChipiApiError:
140
+ raise
141
+ except Exception as error:
142
+ raise handle_api_error(error)
143
+
144
+ async def aget(
145
+ self,
146
+ endpoint: str,
147
+ params: Optional[Dict[str, Any]] = None,
148
+ bearer_token: Optional[str] = None,
149
+ ) -> Any:
150
+ """
151
+ Async GET request.
152
+
153
+ Args:
154
+ endpoint: API endpoint path
155
+ params: Query parameters
156
+ bearer_token: Optional bearer token
157
+
158
+ Returns:
159
+ Response data
160
+
161
+ Raises:
162
+ ChipiApiError: If request fails
163
+ """
164
+ try:
165
+ url = f"{self.base_url}{endpoint}"
166
+
167
+ # Add version tracking parameters
168
+ params = self._add_version_params(params or {})
169
+
170
+ # Filter out None values
171
+ params = {k: v for k, v in params.items() if v is not None}
172
+
173
+ response = await self._async_client.get(
174
+ url,
175
+ params=params,
176
+ headers=self._get_headers(bearer_token),
177
+ )
178
+
179
+ data = response.json()
180
+
181
+ if not response.is_success:
182
+ error_data = validate_error_response(data)
183
+ raise ChipiApiError(
184
+ error_data["message"],
185
+ error_data.get("code", f"HTTP_{response.status_code}"),
186
+ response.status_code,
187
+ )
188
+
189
+ return data
190
+ except ChipiApiError:
191
+ raise
192
+ except Exception as error:
193
+ raise handle_api_error(error)
194
+
195
+ def post(
196
+ self,
197
+ endpoint: str,
198
+ bearer_token: str,
199
+ body: Optional[Dict[str, Any]] = None,
200
+ ) -> Any:
201
+ """
202
+ Synchronous POST request.
203
+
204
+ Args:
205
+ endpoint: API endpoint path
206
+ bearer_token: Bearer token for authentication
207
+ body: Request body
208
+
209
+ Returns:
210
+ Response data
211
+
212
+ Raises:
213
+ ChipiApiError: If request fails
214
+ """
215
+ try:
216
+ url = f"{self.base_url}{endpoint}"
217
+
218
+ # Add version tracking to URL
219
+ params = self._add_version_params({})
220
+
221
+ response = self._sync_client.post(
222
+ url,
223
+ params=params,
224
+ json=body,
225
+ headers=self._get_headers(bearer_token),
226
+ )
227
+
228
+ data = response.json()
229
+
230
+ if not response.is_success:
231
+ error_data = validate_error_response(data)
232
+ raise ChipiApiError(
233
+ error_data["message"],
234
+ error_data.get("code", f"HTTP_{response.status_code}"),
235
+ response.status_code,
236
+ )
237
+
238
+ return data
239
+ except ChipiApiError:
240
+ raise
241
+ except Exception as error:
242
+ raise handle_api_error(error)
243
+
244
+ async def apost(
245
+ self,
246
+ endpoint: str,
247
+ bearer_token: str,
248
+ body: Optional[Dict[str, Any]] = None,
249
+ ) -> Any:
250
+ """
251
+ Async POST request.
252
+
253
+ Args:
254
+ endpoint: API endpoint path
255
+ bearer_token: Bearer token for authentication
256
+ body: Request body
257
+
258
+ Returns:
259
+ Response data
260
+
261
+ Raises:
262
+ ChipiApiError: If request fails
263
+ """
264
+ try:
265
+ url = f"{self.base_url}{endpoint}"
266
+
267
+ # Add version tracking to URL
268
+ params = self._add_version_params({})
269
+
270
+ response = await self._async_client.post(
271
+ url,
272
+ params=params,
273
+ json=body,
274
+ headers=self._get_headers(bearer_token),
275
+ )
276
+
277
+ data = response.json()
278
+
279
+ if not response.is_success:
280
+ error_data = validate_error_response(data)
281
+ raise ChipiApiError(
282
+ error_data["message"],
283
+ error_data.get("code", f"HTTP_{response.status_code}"),
284
+ response.status_code,
285
+ )
286
+
287
+ return data
288
+ except ChipiApiError:
289
+ raise
290
+ except Exception as error:
291
+ raise handle_api_error(error)
292
+
293
+ def put(
294
+ self,
295
+ endpoint: str,
296
+ bearer_token: str,
297
+ body: Optional[Dict[str, Any]] = None,
298
+ ) -> Any:
299
+ """
300
+ Synchronous PUT request.
301
+
302
+ Args:
303
+ endpoint: API endpoint path
304
+ bearer_token: Bearer token for authentication
305
+ body: Request body
306
+
307
+ Returns:
308
+ Response data
309
+
310
+ Raises:
311
+ ChipiApiError: If request fails
312
+ """
313
+ try:
314
+ url = f"{self.base_url}{endpoint}"
315
+
316
+ # Add version tracking to URL
317
+ params = self._add_version_params({})
318
+
319
+ response = self._sync_client.put(
320
+ url,
321
+ params=params,
322
+ json=body,
323
+ headers=self._get_headers(bearer_token),
324
+ )
325
+
326
+ data = response.json()
327
+
328
+ if not response.is_success:
329
+ error_data = validate_error_response(data)
330
+ raise ChipiApiError(
331
+ error_data["message"],
332
+ error_data.get("code", f"HTTP_{response.status_code}"),
333
+ response.status_code,
334
+ )
335
+
336
+ return data
337
+ except ChipiApiError:
338
+ raise
339
+ except Exception as error:
340
+ raise handle_api_error(error)
341
+
342
+ async def aput(
343
+ self,
344
+ endpoint: str,
345
+ bearer_token: str,
346
+ body: Optional[Dict[str, Any]] = None,
347
+ ) -> Any:
348
+ """
349
+ Async PUT request.
350
+
351
+ Args:
352
+ endpoint: API endpoint path
353
+ bearer_token: Bearer token for authentication
354
+ body: Request body
355
+
356
+ Returns:
357
+ Response data
358
+
359
+ Raises:
360
+ ChipiApiError: If request fails
361
+ """
362
+ try:
363
+ url = f"{self.base_url}{endpoint}"
364
+
365
+ # Add version tracking to URL
366
+ params = self._add_version_params({})
367
+
368
+ response = await self._async_client.put(
369
+ url,
370
+ params=params,
371
+ json=body,
372
+ headers=self._get_headers(bearer_token),
373
+ )
374
+
375
+ data = response.json()
376
+
377
+ if not response.is_success:
378
+ error_data = validate_error_response(data)
379
+ raise ChipiApiError(
380
+ error_data["message"],
381
+ error_data.get("code", f"HTTP_{response.status_code}"),
382
+ response.status_code,
383
+ )
384
+
385
+ return data
386
+ except ChipiApiError:
387
+ raise
388
+ except Exception as error:
389
+ raise handle_api_error(error)
390
+
391
+ def delete(
392
+ self,
393
+ endpoint: str,
394
+ bearer_token: str,
395
+ ) -> Any:
396
+ """
397
+ Synchronous DELETE request.
398
+
399
+ Args:
400
+ endpoint: API endpoint path
401
+ bearer_token: Bearer token for authentication
402
+
403
+ Returns:
404
+ Response data
405
+
406
+ Raises:
407
+ ChipiApiError: If request fails
408
+ """
409
+ try:
410
+ url = f"{self.base_url}{endpoint}"
411
+
412
+ # Add version tracking to URL
413
+ params = self._add_version_params({})
414
+
415
+ response = self._sync_client.delete(
416
+ url,
417
+ params=params,
418
+ headers=self._get_headers(bearer_token),
419
+ )
420
+
421
+ data = response.json()
422
+
423
+ if not response.is_success:
424
+ error_data = validate_error_response(data)
425
+ raise ChipiApiError(
426
+ error_data["message"],
427
+ error_data.get("code", f"HTTP_{response.status_code}"),
428
+ response.status_code,
429
+ )
430
+
431
+ return data
432
+ except ChipiApiError:
433
+ raise
434
+ except Exception as error:
435
+ raise handle_api_error(error)
436
+
437
+ async def adelete(
438
+ self,
439
+ endpoint: str,
440
+ bearer_token: str,
441
+ ) -> Any:
442
+ """
443
+ Async DELETE request.
444
+
445
+ Args:
446
+ endpoint: API endpoint path
447
+ bearer_token: Bearer token for authentication
448
+
449
+ Returns:
450
+ Response data
451
+
452
+ Raises:
453
+ ChipiApiError: If request fails
454
+ """
455
+ try:
456
+ url = f"{self.base_url}{endpoint}"
457
+
458
+ # Add version tracking to URL
459
+ params = self._add_version_params({})
460
+
461
+ response = await self._async_client.delete(
462
+ url,
463
+ params=params,
464
+ headers=self._get_headers(bearer_token),
465
+ )
466
+
467
+ data = response.json()
468
+
469
+ if not response.is_success:
470
+ error_data = validate_error_response(data)
471
+ raise ChipiApiError(
472
+ error_data["message"],
473
+ error_data.get("code", f"HTTP_{response.status_code}"),
474
+ response.status_code,
475
+ )
476
+
477
+ return data
478
+ except ChipiApiError:
479
+ raise
480
+ except Exception as error:
481
+ raise handle_api_error(error)
482
+
483
+ def close(self):
484
+ """Close the sync HTTP client."""
485
+ self._sync_client.close()
486
+
487
+ async def aclose(self):
488
+ """Close the async HTTP client."""
489
+ await self._async_client.aclose()
490
+
491
+ def __enter__(self):
492
+ """Context manager entry."""
493
+ return self
494
+
495
+ def __exit__(self, exc_type, exc_val, exc_tb):
496
+ """Context manager exit."""
497
+ self.close()
498
+
499
+ async def __aenter__(self):
500
+ """Async context manager entry."""
501
+ return self
502
+
503
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
504
+ """Async context manager exit."""
505
+ await self.aclose()
chipi_sdk/constants.py ADDED
@@ -0,0 +1,171 @@
1
+ """Constants used across Chipi SDK."""
2
+
3
+ from typing import Optional
4
+ from .models.wallet import WalletType
5
+
6
+
7
+ # API Versioning
8
+ API_VERSION = "1"
9
+ API_VERSION_DATE = "2025-12-30"
10
+
11
+ # Starknet Networks
12
+ STARKNET_NETWORKS = {
13
+ "MAINNET": "https://starknet-mainnet.public.blastapi.io/rpc/v0_7",
14
+ "SEPOLIA": "https://starknet-sepolia.public.blastapi.io/rpc/v0_7",
15
+ }
16
+
17
+ # Contract Addresses
18
+ CONTRACT_ADDRESSES = {
19
+ "USDC_MAINNET": "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb",
20
+ "VESU_USDC_MAINNET": "0x017f19582c61479f2fe0b6606300e975c0a8f439102f43eeecc1d0e9b3d84350",
21
+ }
22
+
23
+ # Token Decimals
24
+ TOKEN_DECIMALS = {
25
+ "USDC": 6,
26
+ "USDT": 6,
27
+ "ETH": 18,
28
+ "STRK": 18,
29
+ "DAI": 18,
30
+ "WBTC": 8,
31
+ }
32
+
33
+ # API Endpoints
34
+ API_ENDPOINTS = {
35
+ "CHIPI_WALLETS": "/chipi-wallets",
36
+ "TRANSACTIONS": "/transactions",
37
+ "SKUS": "/skus",
38
+ "SKU_TRANSACTIONS": "/sku-transactions",
39
+ "EXCHANGES": "/exchanges",
40
+ "USERS": "/users",
41
+ }
42
+
43
+ # Default Pagination
44
+ DEFAULT_PAGINATION = {
45
+ "PAGE": 1,
46
+ "LIMIT": 10,
47
+ "MAX_LIMIT": 100,
48
+ }
49
+
50
+ # Error Codes
51
+ ERRORS = {
52
+ "INVALID_API_KEY": "INVALID_API_KEY",
53
+ "WALLET_NOT_FOUND": "WALLET_NOT_FOUND",
54
+ "INSUFFICIENT_BALANCE": "INSUFFICIENT_BALANCE",
55
+ "TRANSACTION_FAILED": "TRANSACTION_FAILED",
56
+ "INVALID_SIGNATURE": "INVALID_SIGNATURE",
57
+ "SKU_NOT_FOUND": "SKU_NOT_FOUND",
58
+ "SKU_UNAVAILABLE": "SKU_UNAVAILABLE",
59
+ }
60
+
61
+ # SKU Contracts
62
+ SKU_CONTRACTS = {
63
+ "RECHARGER_WITH_STRK_MAINNET": "0x02d65bb726d2c29e3c97669cf297c5145eac19284fb6f935c05c0bfc68dae2b7",
64
+ "CHIPI_BILL_SERVICE": "0x4e8150110d580069de26adec9b179023289d55859ea07487aaade5458d7aa8b",
65
+ }
66
+
67
+ # Service Types
68
+ SERVICE_TYPES = {
69
+ "BUY_SERVICE": "BUY_SERVICE",
70
+ }
71
+
72
+ # Carrier IDs
73
+ CARRIER_IDS = {
74
+ "CHIPI_PAY": "chipi_pay",
75
+ }
76
+
77
+ # Chain Types
78
+ CHAIN_TYPES = {
79
+ "STARKNET": "STARKNET",
80
+ }
81
+
82
+ # Chain Token Types
83
+ CHAIN_TOKEN_TYPES = {
84
+ "USDC": "USDC",
85
+ "USDT": "USDT",
86
+ "ETH": "ETH",
87
+ "STRK": "STRK",
88
+ "DAI": "DAI",
89
+ "WBTC": "WBTC",
90
+ "OTHER": "OTHER",
91
+ }
92
+
93
+ # Wallet Class Hashes
94
+ WALLET_CLASS_HASHES: dict[WalletType, str] = {
95
+ WalletType.CHIPI: "0x0484bbd2404b3c7264bea271f7267d6d4004821ac7787a9eed7f472e79ef40d1", # v33 — latest, spending policy + audit fixes
96
+ WalletType.READY: "0x036078334509b514626504edc9fb252328d1a240e4e948bef8d0c08dff45927f",
97
+ }
98
+
99
+ # Previous CHIPI wallet class hashes still deployed on-chain (newest → oldest)
100
+ LEGACY_CHIPI_CLASS_HASHES: list[str] = [
101
+ "0x035a2251aca25daba18a5d8950deffa8372a7d84774554e75283cb85552eebc9", # v32 — audit 3
102
+ "0x0254f6dd0427319ec614c29e4e3929500d1ba95d0da87ff81d67051ce572667", # v31
103
+ "0x072b77b033a874fa1b8f7ff52e18be8fb5ce01a00c59cd184ae15f5b29bc0e57", # v30
104
+ "0x053f4f8791ed5bed0fddaa553d180c664e32cfaf8316bb232ae77bb08f459f2a", # v29 — previous default
105
+ "0x02de1565226d5215a38b68c4d9a4913989b54edff64c68c45e453c417b44cd83", # v28
106
+ ]
107
+
108
+
109
+ def get_wallet_type_from_class_hash(class_hash: str) -> Optional[str]:
110
+ """
111
+ Determine wallet type from an on-chain class hash.
112
+
113
+ Args:
114
+ class_hash: Class hash to look up
115
+
116
+ Returns:
117
+ Wallet type string ("CHIPI" or "READY"), or None if unknown
118
+ """
119
+ def normalize(h: str) -> str:
120
+ import re
121
+ return re.sub(r"^0x0+", "0x", h.lower())
122
+
123
+ normalized = normalize(class_hash)
124
+
125
+ if normalize(WALLET_CLASS_HASHES[WalletType.READY]) == normalized:
126
+ return "READY"
127
+ if normalize(WALLET_CLASS_HASHES[WalletType.CHIPI]) == normalized:
128
+ return "CHIPI"
129
+ if any(normalize(h) == normalized for h in LEGACY_CHIPI_CLASS_HASHES):
130
+ return "CHIPI"
131
+ return None
132
+
133
+ # RPC Endpoints per Wallet Type
134
+ WALLET_RPC_ENDPOINTS: dict[WalletType, str] = {
135
+ WalletType.CHIPI: "https://starknet-mainnet.public.blastapi.io/rpc/v0_7",
136
+ WalletType.READY: "https://cloud.argent-api.com/v1/starknet/mainnet/rpc/v0.7",
137
+ }
138
+
139
+ # Paymaster Configuration
140
+ PAYMASTER_CONFIG = {
141
+ "URL": "https://paymaster.chipipay.com",
142
+ }
143
+
144
+ # Session Key Configuration (CHIPI wallets only - SNIP-9 compatible)
145
+ SESSION_DEFAULTS = {
146
+ "DURATION_SECONDS": 21600, # Default session duration: 6 hours
147
+ "MAX_CALLS": 1000, # Default max calls per session
148
+ }
149
+
150
+ # Session-specific Error Codes
151
+ SESSION_ERRORS = {
152
+ "INVALID_WALLET_TYPE_FOR_SESSION": "INVALID_WALLET_TYPE_FOR_SESSION",
153
+ "SESSION_EXPIRED": "SESSION_EXPIRED",
154
+ "SESSION_NOT_REGISTERED": "SESSION_NOT_REGISTERED",
155
+ "SESSION_REVOKED": "SESSION_REVOKED",
156
+ "SESSION_MAX_CALLS_EXCEEDED": "SESSION_MAX_CALLS_EXCEEDED",
157
+ "SESSION_ENTRYPOINT_NOT_ALLOWED": "SESSION_ENTRYPOINT_NOT_ALLOWED",
158
+ "SESSION_DECRYPTION_FAILED": "SESSION_DECRYPTION_FAILED",
159
+ "SESSION_CREATION_FAILED": "SESSION_CREATION_FAILED",
160
+ "INVALID_SPENDING_POLICY": "INVALID_SPENDING_POLICY",
161
+ }
162
+
163
+ # Session Contract Entrypoint Names
164
+ SESSION_ENTRYPOINTS = {
165
+ "ADD_OR_UPDATE": "add_or_update_session_key",
166
+ "REVOKE": "revoke_session_key",
167
+ "GET_DATA": "get_session_data",
168
+ "SET_SPENDING_POLICY": "set_spending_policy",
169
+ "GET_SPENDING_POLICY": "get_spending_policy",
170
+ "REMOVE_SPENDING_POLICY": "remove_spending_policy",
171
+ }