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.
@@ -0,0 +1,75 @@
1
+ """Validation utilities for Chipi SDK."""
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+
7
+ def is_valid_api_key(api_key: str) -> bool:
8
+ """
9
+ Validate API key format.
10
+
11
+ Args:
12
+ api_key: API key string to validate
13
+
14
+ Returns:
15
+ True if valid, False otherwise
16
+ """
17
+ if not api_key or not isinstance(api_key, str):
18
+ return False
19
+
20
+ # API keys should be non-empty strings with reasonable length
21
+ return len(api_key) > 0 and len(api_key) < 500
22
+
23
+
24
+ def validate_address(address: str) -> bool:
25
+ """
26
+ Validate Starknet address format.
27
+
28
+ Args:
29
+ address: Starknet address to validate
30
+
31
+ Returns:
32
+ True if valid, False otherwise
33
+ """
34
+ if not address or not isinstance(address, str):
35
+ return False
36
+
37
+ # Starknet addresses should start with 0x and be hex
38
+ if not address.startswith("0x"):
39
+ return False
40
+
41
+ # Remove 0x prefix and validate hex
42
+ hex_part = address[2:]
43
+ if not hex_part:
44
+ return False
45
+
46
+ # Check if it's valid hex (0-9, a-f, A-F)
47
+ return bool(re.match(r"^[0-9a-fA-F]+$", hex_part))
48
+
49
+
50
+ def validate_error_response(response: dict[str, Any]) -> dict[str, Any]:
51
+ """
52
+ Validate and normalize error response.
53
+
54
+ Args:
55
+ response: Error response dictionary
56
+
57
+ Returns:
58
+ Normalized error response with message and code
59
+ """
60
+ if not isinstance(response, dict):
61
+ return {
62
+ "message": "Unknown error occurred",
63
+ "code": "UNKNOWN_ERROR",
64
+ }
65
+
66
+ message = response.get("message")
67
+ if not message:
68
+ message = response.get("error", "Unknown error occurred")
69
+
70
+ code = response.get("code", "UNKNOWN_ERROR")
71
+
72
+ return {
73
+ "message": str(message),
74
+ "code": str(code),
75
+ }
chipi_sdk/wallets.py ADDED
@@ -0,0 +1,465 @@
1
+ """Wallet management utilities."""
2
+
3
+ import os
4
+ import secrets
5
+ from typing import Optional
6
+ from starknet_py.net.full_node_client import FullNodeClient
7
+ from starknet_py.net.account.account import Account
8
+ from starknet_py.net.models import StarknetChainId
9
+ from starknet_py.hash.address import compute_address
10
+ from starknet_py.hash.selector import get_selector_from_name
11
+ from starknet_py.cairo.felt import encode_shortstring
12
+
13
+ from .models.wallet import (
14
+ CreateWalletParams,
15
+ CreateWalletResponse,
16
+ GetWalletParams,
17
+ GetWalletResponse,
18
+ GetTokenBalanceParams,
19
+ GetTokenBalanceResponse,
20
+ WalletType,
21
+ WalletData,
22
+ DeploymentData,
23
+ CreateCustodialWalletParams,
24
+ PrepareWalletUpgradeParams,
25
+ PrepareWalletUpgradeResponse,
26
+ ExecuteWalletUpgradeParams,
27
+ ExecuteWalletUpgradeResponse,
28
+ )
29
+ from .encryption import encrypt_private_key
30
+ from .errors import ChipiTransactionError, ChipiApiError
31
+ from .constants import (
32
+ API_ENDPOINTS,
33
+ WALLET_CLASS_HASHES,
34
+ WALLET_RPC_ENDPOINTS,
35
+ )
36
+ from .client import ChipiClient
37
+
38
+
39
+ class ChipiWallets:
40
+ """Wallet management class."""
41
+
42
+ def __init__(self, client: ChipiClient):
43
+ """
44
+ Initialize wallet manager.
45
+
46
+ Args:
47
+ client: Chipi HTTP client
48
+ """
49
+ self.client = client
50
+
51
+ def _get_private_key(self) -> str:
52
+ """
53
+ Generate a random private key compatible with Starknet.
54
+
55
+ Returns:
56
+ Private key as hex string with 0x prefix
57
+ """
58
+ # Generate 32 random bytes (256 bits)
59
+ private_key_bytes = secrets.token_bytes(32)
60
+ private_key = private_key_bytes.hex()
61
+ full_private_key = f"0x{private_key}"
62
+
63
+ # Ensure the private key is within Starknet's valid range (0 to 2^251 - 1)
64
+ max_starknet_value = 2**251
65
+ private_key_int = int(full_private_key, 16) % max_starknet_value
66
+
67
+ # Convert back to hex string with '0x' prefix
68
+ return f"0x{private_key_int:064x}"
69
+
70
+ def _build_constructor_calldata(
71
+ self, wallet_type: WalletType, stark_key_pub: str
72
+ ) -> list[int]:
73
+ """
74
+ Build constructor calldata based on wallet type.
75
+
76
+ Args:
77
+ wallet_type: Type of wallet (CHIPI or READY)
78
+ stark_key_pub: Public key
79
+
80
+ Returns:
81
+ List of calldata values as integers
82
+ """
83
+ if wallet_type == WalletType.READY:
84
+ # Argent X Account: owner (CairoCustomEnum) + guardian (CairoOption None)
85
+ # This is a simplified version - full Cairo enum encoding needed
86
+ # For now, return basic structure
87
+ return [int(stark_key_pub, 16), 0] # owner pubkey, no guardian
88
+
89
+ # CHIPI wallet: Simple OpenZeppelin account with just public_key
90
+ return [int(stark_key_pub, 16)]
91
+
92
+ async def acreate_wallet(
93
+ self, params: CreateWalletParams, bearer_token: str
94
+ ) -> CreateWalletResponse:
95
+ """
96
+ Create a new wallet (async).
97
+
98
+ Args:
99
+ params: Wallet creation parameters
100
+ bearer_token: Authentication token
101
+
102
+ Returns:
103
+ Wallet creation response with transaction hash and wallet data
104
+
105
+ Raises:
106
+ ChipiTransactionError: If wallet creation fails
107
+ """
108
+ try:
109
+ encrypt_key = params.encrypt_key
110
+ external_user_id = params.external_user_id
111
+ user_id = params.user_id
112
+ wallet_type = params.wallet_type or WalletType.CHIPI
113
+ use_passkey = params.use_passkey or False
114
+
115
+ if not encrypt_key:
116
+ error_msg = (
117
+ "encryptKey is required when using passkey. The passkey authentication should have provided the encryptKey."
118
+ if use_passkey
119
+ else "encryptKey is required for wallet creation"
120
+ )
121
+ raise ValueError(error_msg)
122
+
123
+ # Select RPC endpoint based on wallet type
124
+ rpc_url = WALLET_RPC_ENDPOINTS.get(
125
+ wallet_type, WALLET_RPC_ENDPOINTS[WalletType.CHIPI]
126
+ )
127
+
128
+ provider = FullNodeClient(node_url=rpc_url)
129
+
130
+ # Generate private key
131
+ private_key_ax = self._get_private_key()
132
+
133
+ # Get public key from private key
134
+ from starknet_py.net.signer.stark_curve_signer import KeyPair
135
+ key_pair = KeyPair.from_private_key(int(private_key_ax, 16))
136
+ stark_key_pub_ax = hex(key_pair.public_key)
137
+
138
+ # Select class hash based on wallet type
139
+ account_class_hash = WALLET_CLASS_HASHES.get(
140
+ wallet_type, WALLET_CLASS_HASHES[WalletType.CHIPI]
141
+ )
142
+
143
+ # Build constructor calldata
144
+ constructor_calldata = self._build_constructor_calldata(
145
+ wallet_type, stark_key_pub_ax
146
+ )
147
+
148
+ # Calculate future address of the account
149
+ public_key = compute_address(
150
+ salt=int(stark_key_pub_ax, 16),
151
+ class_hash=int(account_class_hash, 16),
152
+ constructor_calldata=constructor_calldata,
153
+ deployer_address=0,
154
+ )
155
+ public_key_hex = hex(public_key)
156
+
157
+ # Create account instance
158
+ account = Account(
159
+ client=provider,
160
+ address=public_key_hex,
161
+ key_pair=key_pair,
162
+ chain=StarknetChainId.MAINNET,
163
+ )
164
+
165
+ # Prepare wallet creation via API
166
+ typed_data_response = await self.client.apost(
167
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/prepare-creation",
168
+ bearer_token=bearer_token,
169
+ body={
170
+ "publicKey": public_key_hex,
171
+ "walletType": wallet_type.value,
172
+ "starkKeyPubAX": stark_key_pub_ax,
173
+ },
174
+ )
175
+
176
+ typed_data = typed_data_response["typedData"]
177
+ account_class_hash_response = typed_data_response["accountClassHash"]
178
+
179
+ # Sign the typed data
180
+ user_signature = account.sign_message(typed_data)
181
+
182
+ # Prepare deployment data
183
+ deployment_data = {
184
+ "class_hash": account_class_hash_response,
185
+ "salt": stark_key_pub_ax,
186
+ "unique": hex(0),
187
+ "calldata": [hex(val) for val in constructor_calldata],
188
+ }
189
+
190
+ # Encrypt private key
191
+ encrypted_private_key = encrypt_private_key(private_key_ax, encrypt_key)
192
+
193
+ # Create wallet via API
194
+ response = await self.client.apost(
195
+ endpoint=API_ENDPOINTS["CHIPI_WALLETS"],
196
+ bearer_token=bearer_token,
197
+ body={
198
+ "externalUserId": external_user_id,
199
+ "userId": user_id,
200
+ "publicKey": public_key_hex,
201
+ "walletType": wallet_type.value,
202
+ "userSignature": {
203
+ "r": str(user_signature[0]),
204
+ "s": str(user_signature[1]),
205
+ "recovery": 0,
206
+ },
207
+ "typedData": typed_data,
208
+ "encryptedPrivateKey": encrypted_private_key,
209
+ "deploymentData": deployment_data,
210
+ },
211
+ )
212
+
213
+ return CreateWalletResponse(**response)
214
+ except Exception as error:
215
+ print(f"Detailed error: {error}")
216
+ raise ChipiTransactionError(
217
+ f"Failed to create wallet: {str(error)}", "WALLET_CREATION_FAILED"
218
+ )
219
+
220
+ def create_wallet(
221
+ self, params: CreateWalletParams, bearer_token: str
222
+ ) -> CreateWalletResponse:
223
+ """
224
+ Create a new wallet (sync).
225
+
226
+ Args:
227
+ params: Wallet creation parameters
228
+ bearer_token: Authentication token
229
+
230
+ Returns:
231
+ Wallet creation response
232
+
233
+ Raises:
234
+ ChipiTransactionError: If wallet creation fails
235
+ """
236
+ # Sync version - simplified, users should prefer async
237
+ import asyncio
238
+ return asyncio.run(self.acreate_wallet(params, bearer_token))
239
+
240
+ async def aget_wallet(
241
+ self, params: GetWalletParams, bearer_token: str
242
+ ) -> Optional[GetWalletResponse]:
243
+ """
244
+ Retrieve a wallet by external user ID (async).
245
+
246
+ Args:
247
+ params: Wallet query parameters
248
+ bearer_token: Authentication token
249
+
250
+ Returns:
251
+ Wallet data or None if not found
252
+
253
+ Raises:
254
+ ChipiApiError: If request fails (except 404)
255
+ """
256
+ try:
257
+ response = await self.client.aget(
258
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/by-user",
259
+ params={"externalUserId": params.external_user_id},
260
+ bearer_token=bearer_token,
261
+ )
262
+ return GetWalletResponse(**response)
263
+ except ChipiApiError as err:
264
+ if err.status == 404:
265
+ return None
266
+ raise
267
+
268
+ def get_wallet(
269
+ self, params: GetWalletParams, bearer_token: str
270
+ ) -> Optional[GetWalletResponse]:
271
+ """
272
+ Retrieve a wallet by external user ID (sync).
273
+
274
+ Args:
275
+ params: Wallet query parameters
276
+ bearer_token: Authentication token
277
+
278
+ Returns:
279
+ Wallet data or None if not found
280
+ """
281
+ try:
282
+ response = self.client.get(
283
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/by-user",
284
+ params={"externalUserId": params.external_user_id},
285
+ bearer_token=bearer_token,
286
+ )
287
+ return GetWalletResponse(**response)
288
+ except ChipiApiError as err:
289
+ if err.status == 404:
290
+ return None
291
+ raise
292
+
293
+ async def aget_token_balance(
294
+ self, params: GetTokenBalanceParams, bearer_token: str
295
+ ) -> GetTokenBalanceResponse:
296
+ """
297
+ Query token balance (async).
298
+
299
+ Args:
300
+ params: Balance query parameters
301
+ bearer_token: Authentication token
302
+
303
+ Returns:
304
+ Token balance information
305
+ """
306
+ response = await self.client.aget(
307
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/token-balance",
308
+ params=params.model_dump(by_alias=True, exclude_none=True),
309
+ bearer_token=bearer_token,
310
+ )
311
+ return GetTokenBalanceResponse(**response)
312
+
313
+ def get_token_balance(
314
+ self, params: GetTokenBalanceParams, bearer_token: str
315
+ ) -> GetTokenBalanceResponse:
316
+ """
317
+ Query token balance (sync).
318
+
319
+ Args:
320
+ params: Balance query parameters
321
+ bearer_token: Authentication token
322
+
323
+ Returns:
324
+ Token balance information
325
+ """
326
+ response = self.client.get(
327
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/token-balance",
328
+ params=params.model_dump(by_alias=True, exclude_none=True),
329
+ bearer_token=bearer_token,
330
+ )
331
+ return GetTokenBalanceResponse(**response)
332
+
333
+ async def acreate_custodial_wallet(
334
+ self, params: CreateCustodialWalletParams, bearer_token: str
335
+ ) -> WalletData:
336
+ """
337
+ Create a custodial merchant wallet (async).
338
+
339
+ Args:
340
+ params: Custodial wallet parameters
341
+ bearer_token: Authentication token
342
+
343
+ Returns:
344
+ Wallet data
345
+ """
346
+ response = await self.client.apost(
347
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/custodial",
348
+ bearer_token=bearer_token,
349
+ body=params.model_dump(),
350
+ )
351
+ return WalletData(**response)
352
+
353
+ def create_custodial_wallet(
354
+ self, params: CreateCustodialWalletParams, bearer_token: str
355
+ ) -> WalletData:
356
+ """
357
+ Create a custodial merchant wallet (sync).
358
+
359
+ Args:
360
+ params: Custodial wallet parameters
361
+ bearer_token: Authentication token
362
+
363
+ Returns:
364
+ Wallet data
365
+ """
366
+ response = self.client.post(
367
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/custodial",
368
+ bearer_token=bearer_token,
369
+ body=params.model_dump(),
370
+ )
371
+ return WalletData(**response)
372
+
373
+ async def aprepare_wallet_upgrade(
374
+ self, params: PrepareWalletUpgradeParams, bearer_token: str
375
+ ) -> PrepareWalletUpgradeResponse:
376
+ """
377
+ Prepare a wallet upgrade (async).
378
+
379
+ Args:
380
+ params: Upgrade preparation parameters
381
+ bearer_token: Authentication token
382
+
383
+ Returns:
384
+ Typed data to sign for the upgrade
385
+ """
386
+ body = {"walletAddress": params.wallet_address}
387
+ if params.target_class_hash:
388
+ body["targetClassHash"] = params.target_class_hash
389
+ response = await self.client.apost(
390
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/prepare-upgrade",
391
+ bearer_token=bearer_token,
392
+ body=body,
393
+ )
394
+ return PrepareWalletUpgradeResponse(**response)
395
+
396
+ def prepare_wallet_upgrade(
397
+ self, params: PrepareWalletUpgradeParams, bearer_token: str
398
+ ) -> PrepareWalletUpgradeResponse:
399
+ """
400
+ Prepare a wallet upgrade (sync).
401
+
402
+ Args:
403
+ params: Upgrade preparation parameters
404
+ bearer_token: Authentication token
405
+
406
+ Returns:
407
+ Typed data to sign for the upgrade
408
+ """
409
+ body = {"walletAddress": params.wallet_address}
410
+ if params.target_class_hash:
411
+ body["targetClassHash"] = params.target_class_hash
412
+ response = self.client.post(
413
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/prepare-upgrade",
414
+ bearer_token=bearer_token,
415
+ body=body,
416
+ )
417
+ return PrepareWalletUpgradeResponse(**response)
418
+
419
+ async def aexecute_wallet_upgrade(
420
+ self, params: ExecuteWalletUpgradeParams, bearer_token: str
421
+ ) -> ExecuteWalletUpgradeResponse:
422
+ """
423
+ Execute a wallet upgrade after signing (async).
424
+
425
+ Args:
426
+ params: Upgrade execution parameters
427
+ bearer_token: Authentication token
428
+
429
+ Returns:
430
+ Upgrade result with transaction hash
431
+ """
432
+ response = await self.client.apost(
433
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/upgrade",
434
+ bearer_token=bearer_token,
435
+ body={
436
+ "walletAddress": params.wallet_address,
437
+ "typedData": params.typed_data,
438
+ "signature": params.signature,
439
+ },
440
+ )
441
+ return ExecuteWalletUpgradeResponse(**response)
442
+
443
+ def execute_wallet_upgrade(
444
+ self, params: ExecuteWalletUpgradeParams, bearer_token: str
445
+ ) -> ExecuteWalletUpgradeResponse:
446
+ """
447
+ Execute a wallet upgrade after signing (sync).
448
+
449
+ Args:
450
+ params: Upgrade execution parameters
451
+ bearer_token: Authentication token
452
+
453
+ Returns:
454
+ Upgrade result with transaction hash
455
+ """
456
+ response = self.client.post(
457
+ endpoint=f"{API_ENDPOINTS['CHIPI_WALLETS']}/upgrade",
458
+ bearer_token=bearer_token,
459
+ body={
460
+ "walletAddress": params.wallet_address,
461
+ "typedData": params.typed_data,
462
+ "signature": params.signature,
463
+ },
464
+ )
465
+ return ExecuteWalletUpgradeResponse(**response)