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/encryption.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Encryption utilities for private key protection."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
6
|
+
from cryptography.hazmat.backends import default_backend
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def encrypt_private_key(private_key: str, password: str) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Encrypt private key using AES-256-CBC.
|
|
14
|
+
|
|
15
|
+
This implementation is compatible with crypto-es library used in TypeScript SDK.
|
|
16
|
+
Uses OpenSSL-compatible format: "Salted__" + salt + ciphertext
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
private_key: Private key to encrypt
|
|
20
|
+
password: Password for encryption
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Base64 encoded encrypted string
|
|
24
|
+
"""
|
|
25
|
+
# Generate random salt (8 bytes)
|
|
26
|
+
import os
|
|
27
|
+
salt = os.urandom(8)
|
|
28
|
+
|
|
29
|
+
# Derive key and IV from password using OpenSSL's EVP_BytesToKey equivalent
|
|
30
|
+
# This matches crypto-es behavior
|
|
31
|
+
key_iv = _derive_key_and_iv(password.encode('utf-8'), salt, 32, 16)
|
|
32
|
+
key = key_iv[:32]
|
|
33
|
+
iv = key_iv[32:48]
|
|
34
|
+
|
|
35
|
+
# Pad the private key (PKCS7 padding)
|
|
36
|
+
padded_key = _pkcs7_pad(private_key.encode('utf-8'), 16)
|
|
37
|
+
|
|
38
|
+
# Encrypt
|
|
39
|
+
cipher = Cipher(
|
|
40
|
+
algorithms.AES(key),
|
|
41
|
+
modes.CBC(iv),
|
|
42
|
+
backend=default_backend()
|
|
43
|
+
)
|
|
44
|
+
encryptor = cipher.encryptor()
|
|
45
|
+
ciphertext = encryptor.update(padded_key) + encryptor.finalize()
|
|
46
|
+
|
|
47
|
+
# Format as OpenSSL compatible: "Salted__" + salt + ciphertext
|
|
48
|
+
encrypted_data = b'Salted__' + salt + ciphertext
|
|
49
|
+
|
|
50
|
+
# Base64 encode
|
|
51
|
+
return base64.b64encode(encrypted_data).decode('utf-8')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def decrypt_private_key(encrypted_private_key: str, password: str) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Decrypt private key using AES-256-CBC.
|
|
57
|
+
|
|
58
|
+
This implementation is compatible with crypto-es library used in TypeScript SDK.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
encrypted_private_key: Base64 encoded encrypted private key
|
|
62
|
+
password: Password for decryption
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Decrypted private key
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: If decryption fails
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
# Decode base64
|
|
72
|
+
encrypted_data = base64.b64decode(encrypted_private_key)
|
|
73
|
+
|
|
74
|
+
# Check for "Salted__" prefix
|
|
75
|
+
if not encrypted_data.startswith(b'Salted__'):
|
|
76
|
+
raise ValueError("Invalid encrypted data format")
|
|
77
|
+
|
|
78
|
+
# Extract salt and ciphertext
|
|
79
|
+
salt = encrypted_data[8:16]
|
|
80
|
+
ciphertext = encrypted_data[16:]
|
|
81
|
+
|
|
82
|
+
# Derive key and IV from password
|
|
83
|
+
key_iv = _derive_key_and_iv(password.encode('utf-8'), salt, 32, 16)
|
|
84
|
+
key = key_iv[:32]
|
|
85
|
+
iv = key_iv[32:48]
|
|
86
|
+
|
|
87
|
+
# Decrypt
|
|
88
|
+
cipher = Cipher(
|
|
89
|
+
algorithms.AES(key),
|
|
90
|
+
modes.CBC(iv),
|
|
91
|
+
backend=default_backend()
|
|
92
|
+
)
|
|
93
|
+
decryptor = cipher.decryptor()
|
|
94
|
+
padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
|
95
|
+
|
|
96
|
+
# Remove PKCS7 padding
|
|
97
|
+
plaintext = _pkcs7_unpad(padded_plaintext)
|
|
98
|
+
|
|
99
|
+
# Convert to string
|
|
100
|
+
decrypted = plaintext.decode('utf-8')
|
|
101
|
+
|
|
102
|
+
if not decrypted:
|
|
103
|
+
raise ValueError("Decryption resulted in empty string")
|
|
104
|
+
|
|
105
|
+
return decrypted
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise ValueError(f"Decryption failed: {str(e)}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _derive_key_and_iv(password: bytes, salt: bytes, key_len: int, iv_len: int) -> bytes:
|
|
111
|
+
"""
|
|
112
|
+
Derive key and IV using OpenSSL's EVP_BytesToKey algorithm.
|
|
113
|
+
|
|
114
|
+
This matches the behavior of crypto-es to ensure compatibility.
|
|
115
|
+
Uses MD5 hashing in multiple rounds.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
password: Password bytes
|
|
119
|
+
salt: Salt bytes
|
|
120
|
+
key_len: Desired key length
|
|
121
|
+
iv_len: Desired IV length
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Concatenated key + IV bytes
|
|
125
|
+
"""
|
|
126
|
+
target_len = key_len + iv_len
|
|
127
|
+
derived = b''
|
|
128
|
+
last_hash = b''
|
|
129
|
+
|
|
130
|
+
while len(derived) < target_len:
|
|
131
|
+
last_hash = hashlib.md5(last_hash + password + salt).digest()
|
|
132
|
+
derived += last_hash
|
|
133
|
+
|
|
134
|
+
return derived[:target_len]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _pkcs7_pad(data: bytes, block_size: int) -> bytes:
|
|
138
|
+
"""
|
|
139
|
+
Apply PKCS7 padding to data.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
data: Data to pad
|
|
143
|
+
block_size: Block size in bytes
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Padded data
|
|
147
|
+
"""
|
|
148
|
+
padding_len = block_size - (len(data) % block_size)
|
|
149
|
+
padding = bytes([padding_len] * padding_len)
|
|
150
|
+
return data + padding
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _pkcs7_unpad(data: bytes) -> bytes:
|
|
154
|
+
"""
|
|
155
|
+
Remove PKCS7 padding from data.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
data: Padded data
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Unpadded data
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
ValueError: If padding is invalid
|
|
165
|
+
"""
|
|
166
|
+
if not data:
|
|
167
|
+
raise ValueError("Cannot unpad empty data")
|
|
168
|
+
|
|
169
|
+
padding_len = data[-1]
|
|
170
|
+
|
|
171
|
+
if padding_len > len(data) or padding_len == 0:
|
|
172
|
+
raise ValueError("Invalid padding")
|
|
173
|
+
|
|
174
|
+
# Verify all padding bytes are correct
|
|
175
|
+
for i in range(padding_len):
|
|
176
|
+
if data[-(i + 1)] != padding_len:
|
|
177
|
+
raise ValueError("Invalid padding")
|
|
178
|
+
|
|
179
|
+
return data[:-padding_len]
|
chipi_sdk/errors.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Error classes and utilities for Chipi SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ChipiError(Exception):
|
|
7
|
+
"""Base exception for Chipi SDK."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self, message: str, code: str = "UNKNOWN_ERROR", status: Optional[int] = None
|
|
11
|
+
):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.message = message
|
|
14
|
+
self.code = code
|
|
15
|
+
self.status = status
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ChipiApiError(ChipiError):
|
|
19
|
+
"""API request errors."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, code: str, status: int):
|
|
22
|
+
super().__init__(message, code, status)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChipiWalletError(ChipiError):
|
|
26
|
+
"""Wallet operation errors."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message: str, code: str = "WALLET_ERROR"):
|
|
29
|
+
super().__init__(message, code)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ChipiTransactionError(ChipiError):
|
|
33
|
+
"""Transaction execution errors."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, message: str, code: str = "TRANSACTION_ERROR"):
|
|
36
|
+
super().__init__(message, code)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ChipiSessionError(ChipiError):
|
|
40
|
+
"""Session management errors."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, message: str, code: str = "SESSION_ERROR"):
|
|
43
|
+
super().__init__(message, code)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ChipiSkuError(ChipiError):
|
|
47
|
+
"""SKU-related errors."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, message: str, code: str = "SKU_ERROR"):
|
|
50
|
+
super().__init__(message, code)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ChipiValidationError(ChipiError):
|
|
54
|
+
"""Validation errors."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, message: str, code: str = "VALIDATION_ERROR"):
|
|
57
|
+
super().__init__(message, code, 400)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ChipiAuthError(ChipiError):
|
|
61
|
+
"""Authentication errors."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, message: str, code: str = "AUTH_ERROR"):
|
|
64
|
+
super().__init__(message, code, 401)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class WalletNotCompatibleError(ChipiWalletError):
|
|
68
|
+
"""Thrown when attempting an operation the wallet type doesn't support."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, message: str):
|
|
71
|
+
super().__init__(message, "WALLET_NOT_COMPATIBLE")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class WalletClassHashNotFoundError(ChipiWalletError):
|
|
75
|
+
"""Thrown when a custom wallet type's class hash isn't registered."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, wallet_type: str):
|
|
78
|
+
super().__init__(
|
|
79
|
+
f"No class hash registered for wallet type '{wallet_type}'. "
|
|
80
|
+
"Register it via custom_wallet_types in SDK config.",
|
|
81
|
+
"WALLET_CLASS_HASH_NOT_FOUND",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PaymasterIncompatibleError(ChipiError):
|
|
86
|
+
"""Thrown when wallet account doesn't support SNIP-9 outside execution."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, class_hash: str):
|
|
89
|
+
super().__init__(
|
|
90
|
+
f"Wallet class hash {class_hash} is not compatible with Chipi paymaster. "
|
|
91
|
+
"Account must implement SNIP-9 (outside_execution_v2).",
|
|
92
|
+
"PAYMASTER_INCOMPATIBLE",
|
|
93
|
+
400,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_chipi_error(error: Any) -> bool:
|
|
98
|
+
"""Check if an error is a ChipiError instance."""
|
|
99
|
+
return isinstance(error, ChipiError)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def handle_api_error(error: Any) -> ChipiError:
|
|
103
|
+
"""
|
|
104
|
+
Convert various error types to ChipiError.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
error: Any exception or error object
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
ChipiError instance
|
|
111
|
+
"""
|
|
112
|
+
if is_chipi_error(error):
|
|
113
|
+
return error
|
|
114
|
+
|
|
115
|
+
# Handle httpx or requests errors
|
|
116
|
+
if hasattr(error, "response"):
|
|
117
|
+
status = getattr(error.response, "status_code", None)
|
|
118
|
+
if status:
|
|
119
|
+
try:
|
|
120
|
+
data = error.response.json()
|
|
121
|
+
message = data.get("message", str(error))
|
|
122
|
+
code = data.get("code", f"HTTP_{status}")
|
|
123
|
+
except Exception:
|
|
124
|
+
message = str(error)
|
|
125
|
+
code = f"HTTP_{status}"
|
|
126
|
+
return ChipiApiError(message, code, status)
|
|
127
|
+
|
|
128
|
+
# Default case
|
|
129
|
+
message = str(error) if error else "An unknown error occurred"
|
|
130
|
+
return ChipiError(message, "UNKNOWN_ERROR")
|