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/__init__.py +342 -0
- chipi_sdk/client.py +505 -0
- chipi_sdk/constants.py +171 -0
- chipi_sdk/encryption.py +179 -0
- chipi_sdk/errors.py +130 -0
- chipi_sdk/execute_paymaster.py +434 -0
- chipi_sdk/formatters.py +154 -0
- chipi_sdk/models/__init__.py +145 -0
- chipi_sdk/models/core.py +96 -0
- chipi_sdk/models/session.py +119 -0
- chipi_sdk/models/sku.py +28 -0
- chipi_sdk/models/sku_transaction.py +30 -0
- chipi_sdk/models/transaction.py +192 -0
- chipi_sdk/models/user.py +31 -0
- chipi_sdk/models/wallet.py +178 -0
- chipi_sdk/models/x402.py +117 -0
- chipi_sdk/py.typed +1 -0
- chipi_sdk/sdk.py +1021 -0
- chipi_sdk/sessions.py +836 -0
- chipi_sdk/sku_transactions.py +58 -0
- chipi_sdk/skus.py +93 -0
- chipi_sdk/transactions.py +447 -0
- chipi_sdk/users.py +92 -0
- chipi_sdk/validators.py +75 -0
- chipi_sdk/wallets.py +465 -0
- chipi_sdk/x402_client.py +207 -0
- chipi_sdk/x402_facilitator.py +200 -0
- chipi_sdk/x402_middleware.py +280 -0
- chipi_stack-2.0.0.dist-info/METADATA +366 -0
- chipi_stack-2.0.0.dist-info/RECORD +33 -0
- chipi_stack-2.0.0.dist-info/WHEEL +5 -0
- chipi_stack-2.0.0.dist-info/licenses/LICENSE +21 -0
- chipi_stack-2.0.0.dist-info/top_level.txt +1 -0
chipi_sdk/sessions.py
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
"""Session key management for CHIPI wallets (SNIP-9 compatible)."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import secrets
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from starknet_py.net.full_node_client import FullNodeClient
|
|
7
|
+
from starknet_py.net.signer.stark_curve_signer import KeyPair
|
|
8
|
+
|
|
9
|
+
from .models.session import (
|
|
10
|
+
SessionKeyData,
|
|
11
|
+
CreateSessionKeyParams,
|
|
12
|
+
AddSessionKeyParams,
|
|
13
|
+
RevokeSessionKeyParams,
|
|
14
|
+
GetSessionDataParams,
|
|
15
|
+
SessionDataResponse,
|
|
16
|
+
ExecuteWithSessionParams,
|
|
17
|
+
SetSpendingPolicyParams,
|
|
18
|
+
GetSpendingPolicyParams,
|
|
19
|
+
RemoveSpendingPolicyParams,
|
|
20
|
+
SpendingPolicyData,
|
|
21
|
+
)
|
|
22
|
+
from .models.wallet import WalletType
|
|
23
|
+
from .encryption import encrypt_private_key
|
|
24
|
+
from .errors import ChipiSessionError
|
|
25
|
+
from .constants import (
|
|
26
|
+
WALLET_CLASS_HASHES,
|
|
27
|
+
WALLET_RPC_ENDPOINTS,
|
|
28
|
+
SESSION_DEFAULTS,
|
|
29
|
+
SESSION_ERRORS,
|
|
30
|
+
SESSION_ENTRYPOINTS,
|
|
31
|
+
)
|
|
32
|
+
from .client import ChipiClient
|
|
33
|
+
from .execute_paymaster import (
|
|
34
|
+
execute_paymaster_transaction,
|
|
35
|
+
execute_paymaster_transaction_sync,
|
|
36
|
+
execute_paymaster_transaction_with_session,
|
|
37
|
+
execute_paymaster_transaction_with_session_sync,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ChipiSessions:
|
|
42
|
+
"""Session key management for CHIPI wallets."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, client: ChipiClient):
|
|
45
|
+
"""
|
|
46
|
+
Initialize session manager.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
client: Chipi HTTP client
|
|
50
|
+
"""
|
|
51
|
+
self.client = client
|
|
52
|
+
|
|
53
|
+
def _validate_chipi_wallet(
|
|
54
|
+
self,
|
|
55
|
+
wallet_type: Optional[WalletType],
|
|
56
|
+
operation: str,
|
|
57
|
+
wallet_public_key: Optional[str] = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Validate that the wallet is a CHIPI wallet.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
wallet_type: Wallet type
|
|
64
|
+
operation: Operation name for error messages
|
|
65
|
+
wallet_public_key: Optional wallet address for logging
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ChipiSessionError: If wallet type is not CHIPI
|
|
69
|
+
"""
|
|
70
|
+
if wallet_type != WalletType.CHIPI:
|
|
71
|
+
print(
|
|
72
|
+
f"[ChipiSDK:Session:{operation}] Invalid wallet type",
|
|
73
|
+
{
|
|
74
|
+
"provided": wallet_type,
|
|
75
|
+
"required": "CHIPI",
|
|
76
|
+
"expectedClassHash": WALLET_CLASS_HASHES[WalletType.CHIPI],
|
|
77
|
+
"walletAddress": (
|
|
78
|
+
wallet_public_key[:15] + "..." if wallet_public_key else "(not provided)"
|
|
79
|
+
),
|
|
80
|
+
"hint": "Session keys only work with CHIPI wallets (SNIP-9 compatible)",
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
raise ChipiSessionError(
|
|
84
|
+
f"Session keys require CHIPI wallet type. Got: \"{wallet_type or 'undefined'}\". "
|
|
85
|
+
f"Session keys are only supported for CHIPI wallets with class hash {WALLET_CLASS_HASHES[WalletType.CHIPI]}",
|
|
86
|
+
SESSION_ERRORS["INVALID_WALLET_TYPE_FOR_SESSION"],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _to_hex(self, data: bytes) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Convert bytes to hex string with 0x prefix.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
data: Bytes to convert
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Hex string with 0x prefix
|
|
98
|
+
"""
|
|
99
|
+
return "0x" + data.hex()
|
|
100
|
+
|
|
101
|
+
def create_session_key(self, params: CreateSessionKeyParams) -> SessionKeyData:
|
|
102
|
+
"""
|
|
103
|
+
Generate a new session keypair locally.
|
|
104
|
+
|
|
105
|
+
This method generates the session keys but does NOT register them on-chain.
|
|
106
|
+
The returned SessionKeyData should be stored externally by the developer.
|
|
107
|
+
|
|
108
|
+
After generating, call `add_session_key_to_contract()` to register on-chain.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
params: Session creation parameters
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Session key data for external storage
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ChipiSessionError: If session creation fails
|
|
118
|
+
"""
|
|
119
|
+
encrypt_key = params.encrypt_key
|
|
120
|
+
duration_seconds = params.duration_seconds or SESSION_DEFAULTS["DURATION_SECONDS"]
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Generate random private key (32 bytes)
|
|
124
|
+
raw_private_key = secrets.token_bytes(32)
|
|
125
|
+
private_key_hex = self._to_hex(raw_private_key)
|
|
126
|
+
|
|
127
|
+
# Derive public key using starknet.py
|
|
128
|
+
key_pair = KeyPair.from_private_key(int(private_key_hex, 16))
|
|
129
|
+
public_key = hex(key_pair.public_key)
|
|
130
|
+
|
|
131
|
+
# Calculate expiration timestamp
|
|
132
|
+
valid_until = int(time.time()) + duration_seconds
|
|
133
|
+
|
|
134
|
+
# Encrypt the private key for storage
|
|
135
|
+
encrypted_private_key = encrypt_private_key(private_key_hex, encrypt_key)
|
|
136
|
+
|
|
137
|
+
return SessionKeyData(
|
|
138
|
+
public_key=public_key,
|
|
139
|
+
encrypted_private_key=encrypted_private_key,
|
|
140
|
+
valid_until=valid_until,
|
|
141
|
+
)
|
|
142
|
+
except Exception as error:
|
|
143
|
+
raise ChipiSessionError(
|
|
144
|
+
f"Failed to create session key: {str(error)}",
|
|
145
|
+
SESSION_ERRORS["SESSION_CREATION_FAILED"],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
async def aadd_session_key_to_contract(
|
|
149
|
+
self, params: AddSessionKeyParams, bearer_token: str
|
|
150
|
+
) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Register a session key on the smart contract (async).
|
|
153
|
+
|
|
154
|
+
Executes a sponsored transaction to call `add_or_update_session_key`.
|
|
155
|
+
The session must be registered before it can be used for transactions.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
params: Session registration parameters
|
|
159
|
+
bearer_token: Authentication token
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Transaction hash
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
ChipiSessionError: If registration fails
|
|
166
|
+
"""
|
|
167
|
+
encrypt_key = params.encrypt_key
|
|
168
|
+
wallet = params.wallet
|
|
169
|
+
session_config = params.session_config
|
|
170
|
+
|
|
171
|
+
# Validate CHIPI wallet
|
|
172
|
+
self._validate_chipi_wallet(
|
|
173
|
+
wallet.wallet_type, "AddToContract", wallet.public_key
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
print(
|
|
178
|
+
"[ChipiSDK:Session:AddToContract] Registering session on-chain",
|
|
179
|
+
{
|
|
180
|
+
"walletAddress": wallet.public_key[:15] + "...",
|
|
181
|
+
"sessionPubKey": session_config.session_public_key[:15] + "...",
|
|
182
|
+
"validUntil": time.strftime(
|
|
183
|
+
"%Y-%m-%d %H:%M:%S", time.gmtime(session_config.valid_until)
|
|
184
|
+
),
|
|
185
|
+
"maxCalls": session_config.max_calls,
|
|
186
|
+
"allowedEntrypoints": len(session_config.allowed_entrypoints),
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Build calldata for add_or_update_session_key
|
|
191
|
+
calldata = [
|
|
192
|
+
session_config.session_public_key,
|
|
193
|
+
hex(session_config.valid_until),
|
|
194
|
+
hex(session_config.max_calls),
|
|
195
|
+
hex(len(session_config.allowed_entrypoints)),
|
|
196
|
+
*session_config.allowed_entrypoints,
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
# Execute via paymaster (owner signature)
|
|
200
|
+
from .models.transaction import Call
|
|
201
|
+
|
|
202
|
+
tx_hash = await execute_paymaster_transaction(
|
|
203
|
+
params={
|
|
204
|
+
"encryptKey": encrypt_key,
|
|
205
|
+
"wallet": {**wallet.model_dump(), "walletType": "CHIPI"},
|
|
206
|
+
"calls": [
|
|
207
|
+
Call(
|
|
208
|
+
contractAddress=wallet.public_key,
|
|
209
|
+
entrypoint=SESSION_ENTRYPOINTS["ADD_OR_UPDATE"],
|
|
210
|
+
calldata=calldata,
|
|
211
|
+
).model_dump()
|
|
212
|
+
],
|
|
213
|
+
"saveToDatabase": False, # Don't record session management txs
|
|
214
|
+
},
|
|
215
|
+
bearer_token=bearer_token,
|
|
216
|
+
client=self.client,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
print(
|
|
220
|
+
"[ChipiSDK:Session:AddToContract] Session registered successfully",
|
|
221
|
+
{
|
|
222
|
+
"txHash": tx_hash,
|
|
223
|
+
"sessionPubKey": session_config.session_public_key[:15] + "...",
|
|
224
|
+
},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return tx_hash
|
|
228
|
+
except Exception as error:
|
|
229
|
+
print(
|
|
230
|
+
"[ChipiSDK:Session:AddToContract] Registration failed",
|
|
231
|
+
{"error": str(error)},
|
|
232
|
+
)
|
|
233
|
+
raise
|
|
234
|
+
|
|
235
|
+
def add_session_key_to_contract(
|
|
236
|
+
self, params: AddSessionKeyParams, bearer_token: str
|
|
237
|
+
) -> str:
|
|
238
|
+
"""
|
|
239
|
+
Register a session key on the smart contract (sync).
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
params: Session registration parameters
|
|
243
|
+
bearer_token: Authentication token
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Transaction hash
|
|
247
|
+
"""
|
|
248
|
+
import asyncio
|
|
249
|
+
return asyncio.run(self.aadd_session_key_to_contract(params, bearer_token))
|
|
250
|
+
|
|
251
|
+
async def arevoke_session_key(
|
|
252
|
+
self, params: RevokeSessionKeyParams, bearer_token: str
|
|
253
|
+
) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Revoke a session key from the smart contract (async).
|
|
256
|
+
|
|
257
|
+
After revocation, the session key can no longer be used for transactions.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
params: Session revocation parameters
|
|
261
|
+
bearer_token: Authentication token
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Transaction hash
|
|
265
|
+
"""
|
|
266
|
+
encrypt_key = params.encrypt_key
|
|
267
|
+
wallet = params.wallet
|
|
268
|
+
session_public_key = params.session_public_key
|
|
269
|
+
|
|
270
|
+
# Validate CHIPI wallet
|
|
271
|
+
self._validate_chipi_wallet(wallet.wallet_type, "Revoke", wallet.public_key)
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
print(
|
|
275
|
+
"[ChipiSDK:Session:Revoke] Revoking session from contract",
|
|
276
|
+
{
|
|
277
|
+
"walletAddress": wallet.public_key[:15] + "...",
|
|
278
|
+
"sessionToRevoke": session_public_key[:15] + "...",
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Execute via paymaster (owner signature)
|
|
283
|
+
from .models.transaction import Call
|
|
284
|
+
|
|
285
|
+
tx_hash = await execute_paymaster_transaction(
|
|
286
|
+
params={
|
|
287
|
+
"encryptKey": encrypt_key,
|
|
288
|
+
"wallet": {**wallet.model_dump(), "walletType": "CHIPI"},
|
|
289
|
+
"calls": [
|
|
290
|
+
Call(
|
|
291
|
+
contractAddress=wallet.public_key,
|
|
292
|
+
entrypoint=SESSION_ENTRYPOINTS["REVOKE"],
|
|
293
|
+
calldata=[session_public_key],
|
|
294
|
+
).model_dump()
|
|
295
|
+
],
|
|
296
|
+
"saveToDatabase": False,
|
|
297
|
+
},
|
|
298
|
+
bearer_token=bearer_token,
|
|
299
|
+
client=self.client,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
print(
|
|
303
|
+
"[ChipiSDK:Session:Revoke] Session revoked successfully",
|
|
304
|
+
{
|
|
305
|
+
"txHash": tx_hash,
|
|
306
|
+
"sessionRevoked": session_public_key[:15] + "...",
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return tx_hash
|
|
311
|
+
except Exception as error:
|
|
312
|
+
print("[ChipiSDK:Session:Revoke] Revocation failed", {"error": str(error)})
|
|
313
|
+
raise
|
|
314
|
+
|
|
315
|
+
def revoke_session_key(
|
|
316
|
+
self, params: RevokeSessionKeyParams, bearer_token: str
|
|
317
|
+
) -> str:
|
|
318
|
+
"""
|
|
319
|
+
Revoke a session key from the smart contract (sync).
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
params: Session revocation parameters
|
|
323
|
+
bearer_token: Authentication token
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Transaction hash
|
|
327
|
+
"""
|
|
328
|
+
import asyncio
|
|
329
|
+
return asyncio.run(self.arevoke_session_key(params, bearer_token))
|
|
330
|
+
|
|
331
|
+
async def aget_session_data(
|
|
332
|
+
self, params: GetSessionDataParams
|
|
333
|
+
) -> SessionDataResponse:
|
|
334
|
+
"""
|
|
335
|
+
Query session data from the smart contract (async).
|
|
336
|
+
|
|
337
|
+
This is a read-only call that does not require signing or gas.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
params: Query parameters
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Session data including status and remaining calls
|
|
344
|
+
"""
|
|
345
|
+
wallet_address = params.wallet_address
|
|
346
|
+
session_public_key = params.session_public_key
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
provider = FullNodeClient(node_url=WALLET_RPC_ENDPOINTS[WalletType.CHIPI])
|
|
350
|
+
|
|
351
|
+
print(
|
|
352
|
+
"[ChipiSDK:Session:GetData] Querying session data",
|
|
353
|
+
{
|
|
354
|
+
"walletAddress": wallet_address[:15] + "...",
|
|
355
|
+
"sessionPubKey": session_public_key[:15] + "...",
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
result = await provider.call_contract(
|
|
360
|
+
call={
|
|
361
|
+
"contract_address": int(wallet_address, 16),
|
|
362
|
+
"entry_point_selector": get_selector_from_name(
|
|
363
|
+
SESSION_ENTRYPOINTS["GET_DATA"]
|
|
364
|
+
),
|
|
365
|
+
"calldata": [int(session_public_key, 16)],
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Parse the response
|
|
370
|
+
if len(result) < 4:
|
|
371
|
+
print(
|
|
372
|
+
"[ChipiSDK:Session:GetData] Unexpected response format",
|
|
373
|
+
{"resultLength": len(result)},
|
|
374
|
+
)
|
|
375
|
+
return SessionDataResponse(
|
|
376
|
+
is_active=False,
|
|
377
|
+
valid_until=0,
|
|
378
|
+
remaining_calls=0,
|
|
379
|
+
allowed_entrypoints=[],
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
is_active = result[0] == 1
|
|
383
|
+
valid_until = int(result[1])
|
|
384
|
+
remaining_calls = int(result[2])
|
|
385
|
+
entrypoints_len = int(result[3])
|
|
386
|
+
allowed_entrypoints = [hex(val) for val in result[4 : 4 + entrypoints_len]]
|
|
387
|
+
|
|
388
|
+
session_data = SessionDataResponse(
|
|
389
|
+
is_active=is_active,
|
|
390
|
+
valid_until=valid_until,
|
|
391
|
+
remaining_calls=remaining_calls,
|
|
392
|
+
allowed_entrypoints=allowed_entrypoints,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
print(
|
|
396
|
+
"[ChipiSDK:Session:GetData] Session data retrieved",
|
|
397
|
+
{
|
|
398
|
+
"isActive": is_active,
|
|
399
|
+
"validUntil": time.strftime(
|
|
400
|
+
"%Y-%m-%d %H:%M:%S", time.gmtime(valid_until)
|
|
401
|
+
),
|
|
402
|
+
"remainingCalls": remaining_calls,
|
|
403
|
+
"allowedEntrypointsCount": len(allowed_entrypoints),
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
return session_data
|
|
408
|
+
except Exception as error:
|
|
409
|
+
print(
|
|
410
|
+
"[ChipiSDK:Session:GetData] Query failed", {"error": str(error)}
|
|
411
|
+
)
|
|
412
|
+
# Return inactive session on error
|
|
413
|
+
return SessionDataResponse(
|
|
414
|
+
is_active=False,
|
|
415
|
+
valid_until=0,
|
|
416
|
+
remaining_calls=0,
|
|
417
|
+
allowed_entrypoints=[],
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def get_session_data(self, params: GetSessionDataParams) -> SessionDataResponse:
|
|
421
|
+
"""
|
|
422
|
+
Query session data from the smart contract (sync).
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
params: Query parameters
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Session data
|
|
429
|
+
"""
|
|
430
|
+
import asyncio
|
|
431
|
+
return asyncio.run(self.aget_session_data(params))
|
|
432
|
+
|
|
433
|
+
async def aexecute_transaction_with_session(
|
|
434
|
+
self, params: ExecuteWithSessionParams, bearer_token: str
|
|
435
|
+
) -> str:
|
|
436
|
+
"""
|
|
437
|
+
Execute a gasless transaction using a session key (async).
|
|
438
|
+
|
|
439
|
+
The session must be registered on-chain before use.
|
|
440
|
+
CHIPI wallets only.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
params: Session execution parameters
|
|
444
|
+
bearer_token: Authentication token
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Transaction hash
|
|
448
|
+
"""
|
|
449
|
+
# Validate wallet type if provided
|
|
450
|
+
if params.wallet.wallet_type and params.wallet.wallet_type != WalletType.CHIPI:
|
|
451
|
+
raise ChipiSessionError(
|
|
452
|
+
f"Session execution only supports CHIPI wallets. Got: {params.wallet.wallet_type}",
|
|
453
|
+
SESSION_ERRORS["INVALID_WALLET_TYPE_FOR_SESSION"],
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
return await execute_paymaster_transaction_with_session(
|
|
457
|
+
params={
|
|
458
|
+
"encryptKey": params.encrypt_key,
|
|
459
|
+
"wallet": {**params.wallet.model_dump(), "walletType": "CHIPI"},
|
|
460
|
+
"session": params.session.model_dump(),
|
|
461
|
+
"calls": [call.model_dump() for call in params.calls],
|
|
462
|
+
"saveToDatabase": True,
|
|
463
|
+
},
|
|
464
|
+
bearer_token=bearer_token,
|
|
465
|
+
client=self.client,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def execute_transaction_with_session(
|
|
469
|
+
self, params: ExecuteWithSessionParams, bearer_token: str
|
|
470
|
+
) -> str:
|
|
471
|
+
"""
|
|
472
|
+
Execute a gasless transaction using a session key (sync).
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
params: Session execution parameters
|
|
476
|
+
bearer_token: Authentication token
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Transaction hash
|
|
480
|
+
"""
|
|
481
|
+
# Validate wallet type if provided
|
|
482
|
+
if params.wallet.wallet_type and params.wallet.wallet_type != WalletType.CHIPI:
|
|
483
|
+
raise ChipiSessionError(
|
|
484
|
+
f"Session execution only supports CHIPI wallets. Got: {params.wallet.wallet_type}",
|
|
485
|
+
SESSION_ERRORS["INVALID_WALLET_TYPE_FOR_SESSION"],
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
return execute_paymaster_transaction_with_session_sync(
|
|
489
|
+
params={
|
|
490
|
+
"encryptKey": params.encrypt_key,
|
|
491
|
+
"wallet": {**params.wallet.model_dump(), "walletType": "CHIPI"},
|
|
492
|
+
"session": params.session.model_dump(),
|
|
493
|
+
"calls": [call.model_dump() for call in params.calls],
|
|
494
|
+
"saveToDatabase": True,
|
|
495
|
+
},
|
|
496
|
+
bearer_token=bearer_token,
|
|
497
|
+
client=self.client,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# --- Spending Policy Methods ---
|
|
502
|
+
|
|
503
|
+
def _split_u256(self, value: int) -> tuple[str, str]:
|
|
504
|
+
"""Split an integer into Cairo u256 (low u128, high u128) hex strings."""
|
|
505
|
+
mask = (1 << 128) - 1
|
|
506
|
+
return (hex(value & mask), hex(value >> 128))
|
|
507
|
+
|
|
508
|
+
def _join_u256(self, low: int, high: int) -> int:
|
|
509
|
+
"""Reconstruct an integer from Cairo u256 (low, high) parts."""
|
|
510
|
+
return low + (high << 128)
|
|
511
|
+
|
|
512
|
+
async def aset_spending_policy(
|
|
513
|
+
self, params: SetSpendingPolicyParams, bearer_token: str
|
|
514
|
+
) -> str:
|
|
515
|
+
"""
|
|
516
|
+
Set a spending policy for a session key + token pair (async).
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
params: Spending policy parameters
|
|
520
|
+
bearer_token: Authentication token
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Transaction hash
|
|
524
|
+
"""
|
|
525
|
+
encrypt_key = params.encrypt_key
|
|
526
|
+
wallet = params.wallet
|
|
527
|
+
config = params.spending_policy_config
|
|
528
|
+
|
|
529
|
+
# Validate wallet type if provided (backward compat: omitted = assume CHIPI)
|
|
530
|
+
if wallet.wallet_type and wallet.wallet_type != WalletType.CHIPI:
|
|
531
|
+
raise ChipiSessionError(
|
|
532
|
+
f"Spending policies only support CHIPI wallets. Got: {wallet.wallet_type}",
|
|
533
|
+
SESSION_ERRORS["INVALID_WALLET_TYPE_FOR_SESSION"],
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Validate config
|
|
537
|
+
u256_max = (1 << 256) - 1
|
|
538
|
+
u64_max = (1 << 64) - 1
|
|
539
|
+
|
|
540
|
+
if not config.token or config.token.strip() == "":
|
|
541
|
+
raise ChipiSessionError(
|
|
542
|
+
f"Invalid token address for spending policy: \"{config.token}\"",
|
|
543
|
+
SESSION_ERRORS["INVALID_SPENDING_POLICY"],
|
|
544
|
+
)
|
|
545
|
+
if not isinstance(config.window_seconds, int) or config.window_seconds <= 0 or config.window_seconds > u64_max:
|
|
546
|
+
raise ChipiSessionError(
|
|
547
|
+
f"Invalid window duration: {config.window_seconds}. Must be a positive integer within u64 range.",
|
|
548
|
+
SESSION_ERRORS["INVALID_SPENDING_POLICY"],
|
|
549
|
+
)
|
|
550
|
+
if config.max_per_call < 0 or config.max_per_call > u256_max:
|
|
551
|
+
raise ChipiSessionError(
|
|
552
|
+
"Invalid max_per_call: must be >= 0 and <= 2^256 - 1",
|
|
553
|
+
SESSION_ERRORS["INVALID_SPENDING_POLICY"],
|
|
554
|
+
)
|
|
555
|
+
if config.max_per_window < 0 or config.max_per_window > u256_max:
|
|
556
|
+
raise ChipiSessionError(
|
|
557
|
+
"Invalid max_per_window: must be >= 0 and <= 2^256 - 1",
|
|
558
|
+
SESSION_ERRORS["INVALID_SPENDING_POLICY"],
|
|
559
|
+
)
|
|
560
|
+
if config.max_per_window > 0 and config.max_per_call > config.max_per_window:
|
|
561
|
+
raise ChipiSessionError(
|
|
562
|
+
"Invalid spending policy: max_per_call cannot exceed max_per_window",
|
|
563
|
+
SESSION_ERRORS["INVALID_SPENDING_POLICY"],
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
print(
|
|
568
|
+
"[ChipiSDK:Session:SetSpendingPolicy] Setting spending policy",
|
|
569
|
+
{
|
|
570
|
+
"walletAddress": wallet.public_key[:15] + "...",
|
|
571
|
+
"sessionPubKey": config.session_public_key[:15] + "...",
|
|
572
|
+
"token": config.token[:15] + "...",
|
|
573
|
+
"maxPerCall": str(config.max_per_call),
|
|
574
|
+
"maxPerWindow": str(config.max_per_window),
|
|
575
|
+
"windowSeconds": config.window_seconds,
|
|
576
|
+
},
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
max_per_call_low, max_per_call_high = self._split_u256(config.max_per_call)
|
|
580
|
+
max_per_window_low, max_per_window_high = self._split_u256(config.max_per_window)
|
|
581
|
+
|
|
582
|
+
calldata = [
|
|
583
|
+
config.session_public_key,
|
|
584
|
+
config.token,
|
|
585
|
+
max_per_call_low,
|
|
586
|
+
max_per_call_high,
|
|
587
|
+
max_per_window_low,
|
|
588
|
+
max_per_window_high,
|
|
589
|
+
hex(config.window_seconds),
|
|
590
|
+
]
|
|
591
|
+
|
|
592
|
+
from .models.transaction import Call
|
|
593
|
+
|
|
594
|
+
tx_hash = await execute_paymaster_transaction(
|
|
595
|
+
params={
|
|
596
|
+
"encryptKey": encrypt_key,
|
|
597
|
+
"wallet": {**wallet.model_dump(), "walletType": "CHIPI"},
|
|
598
|
+
"calls": [
|
|
599
|
+
Call(
|
|
600
|
+
contractAddress=wallet.public_key,
|
|
601
|
+
entrypoint=SESSION_ENTRYPOINTS["SET_SPENDING_POLICY"],
|
|
602
|
+
calldata=calldata,
|
|
603
|
+
).model_dump()
|
|
604
|
+
],
|
|
605
|
+
"saveToDatabase": False,
|
|
606
|
+
},
|
|
607
|
+
bearer_token=bearer_token,
|
|
608
|
+
client=self.client,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
print(
|
|
612
|
+
"[ChipiSDK:Session:SetSpendingPolicy] Spending policy set successfully",
|
|
613
|
+
{"txHash": tx_hash},
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
return tx_hash
|
|
617
|
+
except Exception as error:
|
|
618
|
+
print(
|
|
619
|
+
"[ChipiSDK:Session:SetSpendingPolicy] Failed",
|
|
620
|
+
{"error": str(error)},
|
|
621
|
+
)
|
|
622
|
+
raise
|
|
623
|
+
|
|
624
|
+
def set_spending_policy(
|
|
625
|
+
self, params: SetSpendingPolicyParams, bearer_token: str
|
|
626
|
+
) -> str:
|
|
627
|
+
"""Set a spending policy for a session key + token pair (sync)."""
|
|
628
|
+
import asyncio
|
|
629
|
+
return asyncio.run(self.aset_spending_policy(params, bearer_token))
|
|
630
|
+
|
|
631
|
+
async def aget_spending_policy(
|
|
632
|
+
self, params: GetSpendingPolicyParams
|
|
633
|
+
) -> SpendingPolicyData:
|
|
634
|
+
"""
|
|
635
|
+
Query a spending policy from the smart contract (async).
|
|
636
|
+
|
|
637
|
+
Read-only — no signature or gas required.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
params: Query parameters
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
Spending policy data
|
|
644
|
+
"""
|
|
645
|
+
wallet_address = params.wallet_address
|
|
646
|
+
session_public_key = params.session_public_key
|
|
647
|
+
token = params.token
|
|
648
|
+
|
|
649
|
+
if not token or token.strip() == "":
|
|
650
|
+
raise ChipiSessionError(
|
|
651
|
+
f"Invalid token address for spending policy query: \"{token}\"",
|
|
652
|
+
SESSION_ERRORS["INVALID_SPENDING_POLICY"],
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
try:
|
|
656
|
+
provider = FullNodeClient(node_url=WALLET_RPC_ENDPOINTS[WalletType.CHIPI])
|
|
657
|
+
|
|
658
|
+
print(
|
|
659
|
+
"[ChipiSDK:Session:GetSpendingPolicy] Querying spending policy",
|
|
660
|
+
{
|
|
661
|
+
"walletAddress": wallet_address[:15] + "...",
|
|
662
|
+
"sessionPubKey": session_public_key[:15] + "...",
|
|
663
|
+
"token": token[:15] + "...",
|
|
664
|
+
},
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
result = await provider.call_contract(
|
|
668
|
+
call={
|
|
669
|
+
"contract_address": int(wallet_address, 16),
|
|
670
|
+
"entry_point_selector": get_selector_from_name(
|
|
671
|
+
SESSION_ENTRYPOINTS["GET_SPENDING_POLICY"]
|
|
672
|
+
),
|
|
673
|
+
"calldata": [int(session_public_key, 16), int(token, 16)],
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
if len(result) < 8:
|
|
678
|
+
print(
|
|
679
|
+
"[ChipiSDK:Session:GetSpendingPolicy] Unexpected response format",
|
|
680
|
+
{"resultLength": len(result)},
|
|
681
|
+
)
|
|
682
|
+
return SpendingPolicyData(
|
|
683
|
+
max_per_call=0,
|
|
684
|
+
max_per_window=0,
|
|
685
|
+
window_seconds=0,
|
|
686
|
+
spent_in_window=0,
|
|
687
|
+
window_start=0,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
policy_data = SpendingPolicyData(
|
|
691
|
+
max_per_call=self._join_u256(int(result[0]), int(result[1])),
|
|
692
|
+
max_per_window=self._join_u256(int(result[2]), int(result[3])),
|
|
693
|
+
window_seconds=int(result[4]),
|
|
694
|
+
spent_in_window=self._join_u256(int(result[5]), int(result[6])),
|
|
695
|
+
window_start=int(result[7]),
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
print(
|
|
699
|
+
"[ChipiSDK:Session:GetSpendingPolicy] Spending policy retrieved",
|
|
700
|
+
{
|
|
701
|
+
"maxPerCall": str(policy_data.max_per_call),
|
|
702
|
+
"maxPerWindow": str(policy_data.max_per_window),
|
|
703
|
+
"windowSeconds": policy_data.window_seconds,
|
|
704
|
+
},
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
return policy_data
|
|
708
|
+
except Exception as error:
|
|
709
|
+
import re
|
|
710
|
+
error_msg = str(error)
|
|
711
|
+
not_found_patterns = [
|
|
712
|
+
r"entry point.*not found",
|
|
713
|
+
r"requested entrypoint was not found",
|
|
714
|
+
r"invalid message selector",
|
|
715
|
+
r"contract not found",
|
|
716
|
+
r"is not deployed",
|
|
717
|
+
]
|
|
718
|
+
is_not_found = any(re.search(p, error_msg, re.IGNORECASE) for p in not_found_patterns)
|
|
719
|
+
|
|
720
|
+
if is_not_found:
|
|
721
|
+
print(
|
|
722
|
+
"[ChipiSDK:Session:GetSpendingPolicy] No policy found",
|
|
723
|
+
{"walletAddress": wallet_address[:15] + "..."},
|
|
724
|
+
)
|
|
725
|
+
return SpendingPolicyData(
|
|
726
|
+
max_per_call=0,
|
|
727
|
+
max_per_window=0,
|
|
728
|
+
window_seconds=0,
|
|
729
|
+
spent_in_window=0,
|
|
730
|
+
window_start=0,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
print(
|
|
734
|
+
"[ChipiSDK:Session:GetSpendingPolicy] RPC/network error",
|
|
735
|
+
{"error": error_msg},
|
|
736
|
+
)
|
|
737
|
+
raise
|
|
738
|
+
|
|
739
|
+
def get_spending_policy(self, params: GetSpendingPolicyParams) -> SpendingPolicyData:
|
|
740
|
+
"""Query a spending policy from the smart contract (sync)."""
|
|
741
|
+
import asyncio
|
|
742
|
+
return asyncio.run(self.aget_spending_policy(params))
|
|
743
|
+
|
|
744
|
+
async def aremove_spending_policy(
|
|
745
|
+
self, params: RemoveSpendingPolicyParams, bearer_token: str
|
|
746
|
+
) -> str:
|
|
747
|
+
"""
|
|
748
|
+
Remove a spending policy for a session key + token pair (async).
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
params: Policy removal parameters
|
|
752
|
+
bearer_token: Authentication token
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
Transaction hash
|
|
756
|
+
"""
|
|
757
|
+
encrypt_key = params.encrypt_key
|
|
758
|
+
wallet = params.wallet
|
|
759
|
+
session_public_key = params.session_public_key
|
|
760
|
+
token = params.token
|
|
761
|
+
|
|
762
|
+
# Validate wallet type if provided (backward compat: omitted = assume CHIPI)
|
|
763
|
+
if wallet.wallet_type and wallet.wallet_type != WalletType.CHIPI:
|
|
764
|
+
raise ChipiSessionError(
|
|
765
|
+
f"Spending policies only support CHIPI wallets. Got: {wallet.wallet_type}",
|
|
766
|
+
SESSION_ERRORS["INVALID_WALLET_TYPE_FOR_SESSION"],
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
if not token or token.strip() == "":
|
|
770
|
+
raise ChipiSessionError(
|
|
771
|
+
f"Invalid token address for spending policy removal: \"{token}\"",
|
|
772
|
+
SESSION_ERRORS["INVALID_SPENDING_POLICY"],
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
try:
|
|
776
|
+
print(
|
|
777
|
+
"[ChipiSDK:Session:RemoveSpendingPolicy] Removing spending policy",
|
|
778
|
+
{
|
|
779
|
+
"walletAddress": wallet.public_key[:15] + "...",
|
|
780
|
+
"sessionPubKey": session_public_key[:15] + "...",
|
|
781
|
+
"token": token[:15] + "...",
|
|
782
|
+
},
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
from .models.transaction import Call
|
|
786
|
+
|
|
787
|
+
tx_hash = await execute_paymaster_transaction(
|
|
788
|
+
params={
|
|
789
|
+
"encryptKey": encrypt_key,
|
|
790
|
+
"wallet": {**wallet.model_dump(), "walletType": "CHIPI"},
|
|
791
|
+
"calls": [
|
|
792
|
+
Call(
|
|
793
|
+
contractAddress=wallet.public_key,
|
|
794
|
+
entrypoint=SESSION_ENTRYPOINTS["REMOVE_SPENDING_POLICY"],
|
|
795
|
+
calldata=[session_public_key, token],
|
|
796
|
+
).model_dump()
|
|
797
|
+
],
|
|
798
|
+
"saveToDatabase": False,
|
|
799
|
+
},
|
|
800
|
+
bearer_token=bearer_token,
|
|
801
|
+
client=self.client,
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
print(
|
|
805
|
+
"[ChipiSDK:Session:RemoveSpendingPolicy] Spending policy removed",
|
|
806
|
+
{"txHash": tx_hash},
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
return tx_hash
|
|
810
|
+
except Exception as error:
|
|
811
|
+
print(
|
|
812
|
+
"[ChipiSDK:Session:RemoveSpendingPolicy] Failed",
|
|
813
|
+
{"error": str(error)},
|
|
814
|
+
)
|
|
815
|
+
raise
|
|
816
|
+
|
|
817
|
+
def remove_spending_policy(
|
|
818
|
+
self, params: RemoveSpendingPolicyParams, bearer_token: str
|
|
819
|
+
) -> str:
|
|
820
|
+
"""Remove a spending policy for a session key + token pair (sync)."""
|
|
821
|
+
import asyncio
|
|
822
|
+
return asyncio.run(self.aremove_spending_policy(params, bearer_token))
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def get_selector_from_name(name: str) -> int:
|
|
826
|
+
"""
|
|
827
|
+
Get selector from function name.
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
name: Function name
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
Selector as integer
|
|
834
|
+
"""
|
|
835
|
+
from starknet_py.hash.selector import get_selector_from_name as _get_selector
|
|
836
|
+
return _get_selector(name)
|