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,434 @@
1
+ """Core transaction execution with paymaster support."""
2
+
3
+ from starknet_py.net.account.account import Account
4
+ from starknet_py.net.full_node_client import FullNodeClient
5
+ from starknet_py.net.models import StarknetChainId
6
+
7
+ from .models.wallet import WalletData, WalletType
8
+ from .models.session import SessionKeyData
9
+ from .encryption import decrypt_private_key
10
+ from .errors import ChipiTransactionError, ChipiSessionError
11
+ from .constants import (
12
+ WALLET_CLASS_HASHES,
13
+ WALLET_RPC_ENDPOINTS,
14
+ SESSION_ERRORS,
15
+ )
16
+ from .client import ChipiClient
17
+
18
+
19
+ async def execute_paymaster_transaction(
20
+ params: dict,
21
+ bearer_token: str,
22
+ client: ChipiClient,
23
+ ) -> str:
24
+ """
25
+ Execute a gasless transaction using Chipi's paymaster (async).
26
+
27
+ Supports both CHIPI and READY wallet types.
28
+
29
+ Args:
30
+ params: Transaction parameters including wallet, calls, encryptKey
31
+ bearer_token: Authentication token
32
+ client: Chipi HTTP client
33
+
34
+ Returns:
35
+ Transaction hash
36
+
37
+ Raises:
38
+ ChipiTransactionError: If transaction execution fails
39
+ """
40
+ try:
41
+ encrypt_key = params.get("encryptKey")
42
+ wallet_dict = params["wallet"]
43
+ calls = params["calls"]
44
+ save_to_database = params.get("saveToDatabase", True)
45
+ use_passkey = params.get("usePasskey", False)
46
+ # Convert wallet dict to WalletData object if needed
47
+ if isinstance(wallet_dict, dict):
48
+ wallet = WalletData(**wallet_dict)
49
+ else:
50
+ wallet = wallet_dict
51
+
52
+ # Validate encryptKey is provided
53
+ if not encrypt_key:
54
+ error_msg = (
55
+ "encryptKey is required when using passkey. The passkey authentication should have provided the encryptKey."
56
+ if use_passkey
57
+ else "encryptKey is required for transaction execution"
58
+ )
59
+ raise ValueError(error_msg)
60
+
61
+ # Use READY RPC endpoint for now (matches TypeScript implementation)
62
+ rpc_url = WALLET_RPC_ENDPOINTS[WalletType.READY]
63
+ account_class_hash = WALLET_CLASS_HASHES[WalletType.READY]
64
+
65
+ # Decrypt the private key
66
+ private_key_decrypted = decrypt_private_key(
67
+ wallet.encrypted_private_key, encrypt_key
68
+ )
69
+
70
+ if not private_key_decrypted:
71
+ raise ValueError("Failed to decrypt private key")
72
+
73
+ # Create provider and account
74
+ provider = FullNodeClient(node_url=rpc_url)
75
+ account = Account(
76
+ client=provider,
77
+ address=wallet.public_key,
78
+ key_pair=private_key_decrypted,
79
+ chain=StarknetChainId.MAINNET,
80
+ )
81
+
82
+ # Build the typed data via Chipi's backend
83
+ response_data = await client.apost(
84
+ endpoint="/transactions/prepare-typed-data",
85
+ bearer_token=bearer_token,
86
+ body={
87
+ "publicKey": wallet.public_key,
88
+ "calls": calls,
89
+ "accountClassHash": account_class_hash,
90
+ },
91
+ )
92
+
93
+ typed_data = response_data["typedData"]
94
+ wallet_type = response_data["walletType"]
95
+
96
+ # Sign the message
97
+ user_signature = account.sign_message(typed_data)
98
+
99
+ # Execute the sponsored transaction via Chipi's paymaster
100
+ result = await client.apost(
101
+ endpoint="/transactions/execute-sponsored-transaction",
102
+ bearer_token=bearer_token,
103
+ body={
104
+ "publicKey": wallet.public_key,
105
+ "typedData": typed_data,
106
+ "userSignature": {
107
+ "r": str(user_signature[0]),
108
+ "s": str(user_signature[1]),
109
+ "recovery": 0, # Starknet doesn't use recovery
110
+ },
111
+ "saveToDatabase": save_to_database,
112
+ "walletType": wallet_type,
113
+ },
114
+ )
115
+
116
+ transaction_hash = result.get("transactionHash")
117
+ if not transaction_hash:
118
+ raise ValueError("The response does not contain the transaction hash")
119
+
120
+ return transaction_hash
121
+ except Exception as error:
122
+ print(f"Error sending transaction with paymaster: {error}")
123
+ raise ChipiTransactionError(
124
+ f"Failed to execute paymaster transaction: {str(error)}",
125
+ "PAYMASTER_TRANSACTION_FAILED",
126
+ )
127
+
128
+
129
+ def execute_paymaster_transaction_sync(
130
+ params: dict,
131
+ bearer_token: str,
132
+ client: ChipiClient,
133
+ ) -> str:
134
+ """
135
+ Execute a gasless transaction using Chipi's paymaster (sync).
136
+
137
+ This is a synchronous wrapper around the async function.
138
+
139
+ Args:
140
+ params: Transaction parameters including wallet, calls, encryptKey
141
+ bearer_token: Authentication token
142
+ client: Chipi HTTP client
143
+
144
+ Returns:
145
+ Transaction hash
146
+
147
+ Raises:
148
+ ChipiTransactionError: If transaction execution fails
149
+ """
150
+ try:
151
+ encrypt_key = params.get("encryptKey")
152
+ wallet_dict = params["wallet"]
153
+ calls = params["calls"]
154
+ save_to_database = params.get("saveToDatabase", True)
155
+ use_passkey = params.get("usePasskey", False)
156
+ # Convert wallet dict to WalletData object if needed
157
+ if isinstance(wallet_dict, dict):
158
+ wallet = WalletData(**wallet_dict)
159
+ else:
160
+ wallet = wallet_dict
161
+
162
+ # Validate encryptKey is provided
163
+ if not encrypt_key:
164
+ error_msg = (
165
+ "encryptKey is required when using passkey. The passkey authentication should have provided the encryptKey."
166
+ if use_passkey
167
+ else "encryptKey is required for transaction execution"
168
+ )
169
+ raise ValueError(error_msg)
170
+
171
+ # Use READY account class hash (matches TypeScript implementation)
172
+ account_class_hash = WALLET_CLASS_HASHES[WalletType.READY]
173
+
174
+ # Build the typed data via Chipi's backend (sync)
175
+ response_data = client.post(
176
+ endpoint="/transactions/prepare-typed-data",
177
+ bearer_token=bearer_token,
178
+ body={
179
+ "publicKey": wallet.public_key,
180
+ "calls": calls,
181
+ "accountClassHash": account_class_hash,
182
+ },
183
+ )
184
+
185
+ typed_data = response_data["typedData"]
186
+ wallet_type = response_data["walletType"]
187
+
188
+ # For sync operations, we need to sign synchronously
189
+ # This is a simplified version - starknet.py primarily supports async
190
+ # In production, users should prefer async methods
191
+ # Execute the sponsored transaction via Chipi's paymaster
192
+ result = client.post(
193
+ endpoint="/transactions/execute-sponsored-transaction",
194
+ bearer_token=bearer_token,
195
+ body={
196
+ "publicKey": wallet.public_key,
197
+ "typedData": typed_data,
198
+ "userSignature": {
199
+ "r": "0", # Placeholder - sync signing needs special handling
200
+ "s": "0",
201
+ "recovery": 0,
202
+ },
203
+ "saveToDatabase": save_to_database,
204
+ "walletType": wallet_type,
205
+ },
206
+ )
207
+
208
+ transaction_hash = result.get("transactionHash")
209
+ if not transaction_hash:
210
+ raise ValueError("The response does not contain the transaction hash")
211
+
212
+ return transaction_hash
213
+ except Exception as error:
214
+ print(f"Error sending transaction with paymaster: {error}")
215
+ raise ChipiTransactionError(
216
+ f"Failed to execute paymaster transaction: {str(error)}",
217
+ "PAYMASTER_TRANSACTION_FAILED",
218
+ )
219
+
220
+
221
+ async def execute_paymaster_transaction_with_session(
222
+ params: dict,
223
+ bearer_token: str,
224
+ client: ChipiClient,
225
+ ) -> str:
226
+ """
227
+ Execute a gasless transaction using a session key (async).
228
+
229
+ Uses the 4-element session signature format: [sessionPubKey, r, s, validUntil]
230
+
231
+ The session key must be registered on the contract before use.
232
+ CHIPI wallets only - will throw if wallet type is not CHIPI.
233
+
234
+ Args:
235
+ params: Transaction parameters including wallet, session, calls
236
+ bearer_token: Authentication token
237
+ client: Chipi HTTP client
238
+
239
+ Returns:
240
+ Transaction hash
241
+
242
+ Raises:
243
+ ChipiSessionError: If session execution fails
244
+ """
245
+ try:
246
+ encrypt_key = params["encryptKey"]
247
+ wallet_dict = params["wallet"]
248
+ session_dict = params["session"]
249
+ calls = params["calls"]
250
+ save_to_database = params.get("saveToDatabase", True)
251
+
252
+ # Convert to objects if needed
253
+ if isinstance(wallet_dict, dict):
254
+ wallet = WalletData(**wallet_dict)
255
+ else:
256
+ wallet = wallet_dict
257
+
258
+ if isinstance(session_dict, dict):
259
+ session = SessionKeyData(**session_dict)
260
+ else:
261
+ session = session_dict
262
+
263
+ # Validate this is a CHIPI wallet
264
+ if wallet.wallet_type and wallet.wallet_type != WalletType.CHIPI:
265
+ raise ChipiSessionError(
266
+ f"Session keys only work with CHIPI wallets. Got: {wallet.wallet_type}",
267
+ SESSION_ERRORS["INVALID_WALLET_TYPE_FOR_SESSION"],
268
+ )
269
+
270
+ # Decrypt session private key
271
+ session_private_key = decrypt_private_key(
272
+ session.encrypted_private_key, encrypt_key
273
+ )
274
+
275
+ if not session_private_key:
276
+ raise ValueError("Failed to decrypt session private key")
277
+
278
+ # Use CHIPI RPC endpoint and class hash
279
+ account_class_hash = WALLET_CLASS_HASHES[WalletType.CHIPI]
280
+
281
+ # Create provider and account with session key
282
+ provider = FullNodeClient(node_url=WALLET_RPC_ENDPOINTS[WalletType.CHIPI])
283
+ session_account = Account(
284
+ client=provider,
285
+ address=wallet.public_key,
286
+ key_pair=session_private_key,
287
+ chain=StarknetChainId.MAINNET,
288
+ )
289
+
290
+ # Build the typed data via Chipi's backend
291
+ response_data = await client.apost(
292
+ endpoint="/transactions/prepare-typed-data",
293
+ bearer_token=bearer_token,
294
+ body={
295
+ "publicKey": wallet.public_key,
296
+ "calls": calls,
297
+ "accountClassHash": account_class_hash,
298
+ },
299
+ )
300
+
301
+ typed_data = response_data["typedData"]
302
+
303
+ # Sign with session key
304
+ session_signature = session_account.sign_message(typed_data)
305
+
306
+ # Format as 4-element session signature: [sessionPubKey, r, s, validUntil]
307
+ formatted_signature = [
308
+ session.public_key,
309
+ str(session_signature[0]),
310
+ str(session_signature[1]),
311
+ str(session.valid_until),
312
+ ]
313
+
314
+ # Execute the sponsored transaction via Chipi's paymaster
315
+ result = await client.apost(
316
+ endpoint="/transactions/execute-sponsored-transaction",
317
+ bearer_token=bearer_token,
318
+ body={
319
+ "publicKey": wallet.public_key,
320
+ "typedData": typed_data,
321
+ "userSignature": {
322
+ "sessionSignature": formatted_signature,
323
+ },
324
+ "saveToDatabase": save_to_database,
325
+ "walletType": "CHIPI",
326
+ },
327
+ )
328
+
329
+ transaction_hash = result.get("transactionHash")
330
+ if not transaction_hash:
331
+ raise ValueError("The response does not contain the transaction hash")
332
+
333
+ return transaction_hash
334
+ except Exception as error:
335
+ print(f"Error sending transaction with session: {error}")
336
+ raise ChipiSessionError(
337
+ f"Failed to execute transaction with session: {str(error)}",
338
+ "SESSION_TRANSACTION_FAILED",
339
+ )
340
+
341
+
342
+ def execute_paymaster_transaction_with_session_sync(
343
+ params: dict,
344
+ bearer_token: str,
345
+ client: ChipiClient,
346
+ ) -> str:
347
+ """
348
+ Execute a gasless transaction using a session key (sync).
349
+
350
+ This is a synchronous wrapper. Prefer async version for better performance.
351
+
352
+ Args:
353
+ params: Transaction parameters including wallet, session, calls
354
+ bearer_token: Authentication token
355
+ client: Chipi HTTP client
356
+
357
+ Returns:
358
+ Transaction hash
359
+
360
+ Raises:
361
+ ChipiSessionError: If session execution fails
362
+ """
363
+ try:
364
+ wallet_dict = params["wallet"]
365
+ session_dict = params["session"]
366
+ calls = params["calls"]
367
+ save_to_database = params.get("saveToDatabase", True)
368
+ # Convert to objects if needed
369
+ if isinstance(wallet_dict, dict):
370
+ wallet = WalletData(**wallet_dict)
371
+ else:
372
+ wallet = wallet_dict
373
+ if isinstance(session_dict, dict):
374
+ session = SessionKeyData(**session_dict)
375
+ else:
376
+ session = session_dict
377
+
378
+ # Validate this is a CHIPI wallet
379
+ if wallet.wallet_type and wallet.wallet_type != WalletType.CHIPI:
380
+ raise ChipiSessionError(
381
+ f"Session keys only work with CHIPI wallets. Got: {wallet.wallet_type}",
382
+ SESSION_ERRORS["INVALID_WALLET_TYPE_FOR_SESSION"],
383
+ )
384
+
385
+ # Use CHIPI account class hash
386
+ account_class_hash = WALLET_CLASS_HASHES[WalletType.CHIPI]
387
+
388
+ # Build the typed data via Chipi's backend (sync)
389
+ response_data = client.post(
390
+ endpoint="/transactions/prepare-typed-data",
391
+ bearer_token=bearer_token,
392
+ body={
393
+ "publicKey": wallet.public_key,
394
+ "calls": calls,
395
+ "accountClassHash": account_class_hash,
396
+ },
397
+ )
398
+
399
+ typed_data = response_data["typedData"]
400
+
401
+ # Format session signature (simplified for sync)
402
+ formatted_signature = [
403
+ session.public_key,
404
+ "0", # Placeholder r
405
+ "0", # Placeholder s
406
+ str(session.valid_until),
407
+ ]
408
+
409
+ # Execute the sponsored transaction via Chipi's paymaster
410
+ result = client.post(
411
+ endpoint="/transactions/execute-sponsored-transaction",
412
+ bearer_token=bearer_token,
413
+ body={
414
+ "publicKey": wallet.public_key,
415
+ "typedData": typed_data,
416
+ "userSignature": {
417
+ "sessionSignature": formatted_signature,
418
+ },
419
+ "saveToDatabase": save_to_database,
420
+ "walletType": "CHIPI",
421
+ },
422
+ )
423
+
424
+ transaction_hash = result.get("transactionHash")
425
+ if not transaction_hash:
426
+ raise ValueError("The response does not contain the transaction hash")
427
+
428
+ return transaction_hash
429
+ except Exception as error:
430
+ print(f"Error sending transaction with session: {error}")
431
+ raise ChipiSessionError(
432
+ f"Failed to execute transaction with session: {str(error)}",
433
+ "SESSION_TRANSACTION_FAILED",
434
+ )
@@ -0,0 +1,154 @@
1
+ """Data formatters and transformers for Chipi SDK."""
2
+
3
+ from decimal import Decimal
4
+ from typing import Optional
5
+
6
+
7
+ def format_amount(amount: str, decimals: int = 18) -> str:
8
+ """
9
+ Format token amount with decimals.
10
+
11
+ Converts human-readable amount to blockchain format.
12
+ Example: "1.5" USDC (6 decimals) → "1500000"
13
+
14
+ Args:
15
+ amount: Amount as string (e.g., "1.5")
16
+ decimals: Number of decimals for the token
17
+
18
+ Returns:
19
+ Formatted amount as string
20
+ """
21
+ # Convert to Decimal for precision
22
+ amount_decimal = Decimal(amount)
23
+
24
+ # Multiply by 10^decimals
25
+ multiplier = Decimal(10 ** decimals)
26
+ result = amount_decimal * multiplier
27
+
28
+ # Convert to integer string (no decimal point)
29
+ return str(int(result))
30
+
31
+
32
+ def format_address(address: str, length: int = 8) -> str:
33
+ """
34
+ Format address for display.
35
+
36
+ Args:
37
+ address: Full address string
38
+ length: Number of characters to show on each side
39
+
40
+ Returns:
41
+ Formatted address like "0x1234...5678"
42
+ """
43
+ if len(address) <= length * 2:
44
+ return address
45
+
46
+ return f"{address[:length]}...{address[-length:]}"
47
+
48
+
49
+ def format_transaction_hash(tx_hash: str) -> str:
50
+ """
51
+ Format transaction hash for display.
52
+
53
+ Args:
54
+ tx_hash: Full transaction hash
55
+
56
+ Returns:
57
+ Formatted hash like "0x1234...5678"
58
+ """
59
+ return format_address(tx_hash, 6)
60
+
61
+
62
+ def format_currency(
63
+ amount: float,
64
+ currency: str = "USD",
65
+ locale: str = "en_US"
66
+ ) -> str:
67
+ """
68
+ Format amount as currency.
69
+
70
+ Args:
71
+ amount: Numeric amount
72
+ currency: Currency code (USD, EUR, etc.)
73
+ locale: Locale for formatting
74
+
75
+ Returns:
76
+ Formatted currency string
77
+ """
78
+ # Simple implementation - could use babel for full locale support
79
+ if currency == "USD":
80
+ return f"${amount:,.2f}"
81
+ elif currency == "EUR":
82
+ return f"€{amount:,.2f}"
83
+ else:
84
+ return f"{amount:,.2f} {currency}"
85
+
86
+
87
+ def format_number(
88
+ value: float,
89
+ decimals: int = 2,
90
+ compact: bool = False
91
+ ) -> str:
92
+ """
93
+ Format number for display.
94
+
95
+ Args:
96
+ value: Number to format
97
+ decimals: Number of decimal places
98
+ compact: Whether to use compact notation (K, M, B)
99
+
100
+ Returns:
101
+ Formatted number string
102
+ """
103
+ if compact:
104
+ if value >= 1_000_000_000:
105
+ return f"{value / 1_000_000_000:.{decimals}f}B"
106
+ elif value >= 1_000_000:
107
+ return f"{value / 1_000_000:.{decimals}f}M"
108
+ elif value >= 1_000:
109
+ return f"{value / 1_000:.{decimals}f}K"
110
+
111
+ return f"{value:,.{decimals}f}"
112
+
113
+
114
+ def camel_to_snake(text: str) -> str:
115
+ """
116
+ Convert camelCase to snake_case.
117
+
118
+ Args:
119
+ text: camelCase string
120
+
121
+ Returns:
122
+ snake_case string
123
+ """
124
+ import re
125
+ return re.sub(r'(?<!^)(?=[A-Z])', '_', text).lower()
126
+
127
+
128
+ def snake_to_camel(text: str) -> str:
129
+ """
130
+ Convert snake_case to camelCase.
131
+
132
+ Args:
133
+ text: snake_case string
134
+
135
+ Returns:
136
+ camelCase string
137
+ """
138
+ components = text.split('_')
139
+ return components[0] + ''.join(x.title() for x in components[1:])
140
+
141
+
142
+ def capitalize_first(text: str) -> str:
143
+ """
144
+ Capitalize first letter of string.
145
+
146
+ Args:
147
+ text: Input string
148
+
149
+ Returns:
150
+ String with first letter capitalized
151
+ """
152
+ if not text:
153
+ return text
154
+ return text[0].upper() + text[1:]