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
|
@@ -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
|
+
)
|
chipi_sdk/formatters.py
ADDED
|
@@ -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:]
|