atp-protocol 1.2.0__py3-none-any.whl → 1.4.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.
- atp/__init__.py +9 -1
- atp/client.py +618 -0
- atp/config.py +26 -0
- atp/encryption.py +155 -0
- atp/middleware.py +444 -107
- atp/schemas.py +186 -0
- atp/settlement_client.py +608 -52
- atp_protocol-1.4.0.dist-info/METADATA +427 -0
- atp_protocol-1.4.0.dist-info/RECORD +11 -0
- atp_protocol-1.2.0.dist-info/METADATA +0 -401
- atp_protocol-1.2.0.dist-info/RECORD +0 -9
- {atp_protocol-1.2.0.dist-info → atp_protocol-1.4.0.dist-info}/LICENSE +0 -0
- {atp_protocol-1.2.0.dist-info → atp_protocol-1.4.0.dist-info}/WHEEL +0 -0
atp/__init__.py
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
from atp.client import ATPClient
|
|
1
2
|
from atp.middleware import ATPSettlementMiddleware, create_settlement_middleware
|
|
2
|
-
from atp.
|
|
3
|
+
from atp.schemas import ATPSettlementMiddlewareConfig
|
|
4
|
+
from atp.settlement_client import (
|
|
5
|
+
SettlementServiceClient,
|
|
6
|
+
SettlementServiceError,
|
|
7
|
+
)
|
|
3
8
|
|
|
4
9
|
__all__ = [
|
|
10
|
+
"ATPClient",
|
|
5
11
|
"ATPSettlementMiddleware",
|
|
6
12
|
"create_settlement_middleware",
|
|
13
|
+
"ATPSettlementMiddlewareConfig",
|
|
7
14
|
"SettlementServiceClient",
|
|
15
|
+
"SettlementServiceError",
|
|
8
16
|
]
|
atp/client.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User-facing client API for ATP Protocol.
|
|
3
|
+
|
|
4
|
+
This module provides a simple, high-level interface for:
|
|
5
|
+
1. Calling the facilitator (settlement service) directly
|
|
6
|
+
2. Making requests to services protected by ATP middleware
|
|
7
|
+
3. Handling wallet authentication and settlement automatically
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import traceback
|
|
14
|
+
from typing import Any, Dict, Optional, Union
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from atp.config import ATP_SETTLEMENT_URL, ATP_SETTLEMENT_TIMEOUT
|
|
19
|
+
from atp.encryption import ResponseEncryptor
|
|
20
|
+
from atp.schemas import PaymentToken
|
|
21
|
+
from atp.settlement_client import (
|
|
22
|
+
SettlementServiceClient,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from loguru import logger
|
|
26
|
+
|
|
27
|
+
class ATPClient:
|
|
28
|
+
"""
|
|
29
|
+
User-facing client for ATP Protocol.
|
|
30
|
+
|
|
31
|
+
This client provides a simple interface for:
|
|
32
|
+
- Calling the facilitator (settlement service) for payment operations
|
|
33
|
+
- Making requests to ATP-protected endpoints with automatic wallet handling
|
|
34
|
+
- Parsing usage data and calculating payments
|
|
35
|
+
- Handling encrypted responses from ATP middleware
|
|
36
|
+
|
|
37
|
+
**Example Usage:**
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from atp.client import ATPClient
|
|
41
|
+
from atp.schemas import PaymentToken
|
|
42
|
+
|
|
43
|
+
# Initialize client with wallet
|
|
44
|
+
client = ATPClient(
|
|
45
|
+
wallet_private_key="[1,2,3,...]", # Your wallet private key
|
|
46
|
+
settlement_service_url="https://facilitator.swarms.world"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Call facilitator directly
|
|
50
|
+
usage = {"input_tokens": 1000, "output_tokens": 500}
|
|
51
|
+
payment = await client.calculate_payment(
|
|
52
|
+
usage=usage,
|
|
53
|
+
input_cost_per_million_usd=10.0,
|
|
54
|
+
output_cost_per_million_usd=30.0,
|
|
55
|
+
payment_token=PaymentToken.SOL
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Execute settlement
|
|
59
|
+
result = await client.settle(
|
|
60
|
+
usage=usage,
|
|
61
|
+
input_cost_per_million_usd=10.0,
|
|
62
|
+
output_cost_per_million_usd=30.0,
|
|
63
|
+
recipient_pubkey="RecipientPublicKeyHere",
|
|
64
|
+
payment_token=PaymentToken.SOL
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Make request to ATP-protected endpoint
|
|
68
|
+
response = await client.request(
|
|
69
|
+
method="POST",
|
|
70
|
+
url="https://api.example.com/v1/chat",
|
|
71
|
+
json={"message": "Hello!"}
|
|
72
|
+
)
|
|
73
|
+
# Wallet is automatically included in headers
|
|
74
|
+
# Response is automatically decrypted if encrypted
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Attributes:**
|
|
78
|
+
wallet_private_key (str): Wallet private key for authentication and payments.
|
|
79
|
+
settlement_service_url (str): Base URL of the settlement service.
|
|
80
|
+
settlement_timeout (float): Timeout for settlement operations in seconds.
|
|
81
|
+
wallet_private_key_header (str): HTTP header name for wallet key (default: "x-wallet-private-key").
|
|
82
|
+
settlement_client (SettlementServiceClient): Internal client for settlement service.
|
|
83
|
+
encryptor (ResponseEncryptor): Encryptor for handling encrypted responses.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
wallet_private_key: Optional[str] = None,
|
|
89
|
+
settlement_service_url: Optional[str] = ATP_SETTLEMENT_URL,
|
|
90
|
+
settlement_timeout: Optional[float] = None,
|
|
91
|
+
wallet_private_key_header: str = "x-wallet-private-key",
|
|
92
|
+
verbose: bool = False,
|
|
93
|
+
):
|
|
94
|
+
"""
|
|
95
|
+
Initialize the ATP client.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
wallet_private_key: Wallet private key for authentication and payments.
|
|
99
|
+
Can be in JSON array format (e.g., "[1,2,3,...]") or base58 string.
|
|
100
|
+
If not provided, must be passed per-request.
|
|
101
|
+
settlement_service_url: Base URL of the settlement service.
|
|
102
|
+
Default: from ATP_SETTLEMENT_URL env var or "https://facilitator.swarms.world".
|
|
103
|
+
settlement_timeout: Timeout for settlement operations in seconds.
|
|
104
|
+
Default: from ATP_SETTLEMENT_TIMEOUT env var or 300.0 (5 minutes).
|
|
105
|
+
wallet_private_key_header: HTTP header name for wallet private key.
|
|
106
|
+
Default: "x-wallet-private-key".
|
|
107
|
+
verbose: If True, enables detailed logging with tracebacks for debugging.
|
|
108
|
+
Default: False.
|
|
109
|
+
"""
|
|
110
|
+
self.wallet_private_key = wallet_private_key
|
|
111
|
+
self.wallet_private_key_header = wallet_private_key_header
|
|
112
|
+
self.settlement_service_url = (
|
|
113
|
+
settlement_service_url or ATP_SETTLEMENT_URL
|
|
114
|
+
)
|
|
115
|
+
self.settlement_timeout = (
|
|
116
|
+
settlement_timeout if settlement_timeout is not None else ATP_SETTLEMENT_TIMEOUT
|
|
117
|
+
)
|
|
118
|
+
self.verbose = verbose
|
|
119
|
+
|
|
120
|
+
# Initialize settlement service client
|
|
121
|
+
self.settlement_client = SettlementServiceClient(
|
|
122
|
+
base_url=self.settlement_service_url,
|
|
123
|
+
timeout=self.settlement_timeout,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Initialize encryptor for handling encrypted responses
|
|
127
|
+
self.encryptor = ResponseEncryptor()
|
|
128
|
+
|
|
129
|
+
if self.verbose:
|
|
130
|
+
logger.info(f"ATPClient initialized with settlement_service_url={self.settlement_service_url}, timeout={self.settlement_timeout}")
|
|
131
|
+
|
|
132
|
+
def _get_headers(
|
|
133
|
+
self, wallet_private_key: Optional[str] = None, **kwargs
|
|
134
|
+
) -> Dict[str, str]:
|
|
135
|
+
"""
|
|
136
|
+
Get HTTP headers with wallet authentication.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
wallet_private_key: Wallet private key to use. If not provided,
|
|
140
|
+
uses the client's default wallet_private_key.
|
|
141
|
+
**kwargs: Additional headers to include.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dict of HTTP headers.
|
|
145
|
+
"""
|
|
146
|
+
headers = {"Content-Type": "application/json", **kwargs}
|
|
147
|
+
|
|
148
|
+
# Add wallet private key if available
|
|
149
|
+
key = wallet_private_key or self.wallet_private_key
|
|
150
|
+
if key:
|
|
151
|
+
headers[self.wallet_private_key_header] = key
|
|
152
|
+
|
|
153
|
+
return headers
|
|
154
|
+
|
|
155
|
+
async def parse_usage(
|
|
156
|
+
self, usage_data: Dict[str, Any]
|
|
157
|
+
) -> Dict[str, Optional[int]]:
|
|
158
|
+
"""
|
|
159
|
+
Parse usage tokens from various API formats.
|
|
160
|
+
|
|
161
|
+
This method uses the facilitator to parse usage data from any supported
|
|
162
|
+
format (OpenAI, Anthropic, Google, etc.) and normalize it to a standard format.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
usage_data: Usage data in any supported format. Can be the entire
|
|
166
|
+
response body or just the usage portion.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Dict with normalized keys:
|
|
170
|
+
- `input_tokens` (Optional[int]): Number of input/prompt tokens
|
|
171
|
+
- `output_tokens` (Optional[int]): Number of output/completion tokens
|
|
172
|
+
- `total_tokens` (Optional[int]): Total number of tokens
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
SettlementServiceError: If the facilitator returns an error.
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
```python
|
|
179
|
+
usage = await client.parse_usage({
|
|
180
|
+
"prompt_tokens": 100,
|
|
181
|
+
"completion_tokens": 50,
|
|
182
|
+
"total_tokens": 150
|
|
183
|
+
})
|
|
184
|
+
# Returns: {"input_tokens": 100, "output_tokens": 50, "total_tokens": 150}
|
|
185
|
+
```
|
|
186
|
+
"""
|
|
187
|
+
if self.verbose:
|
|
188
|
+
logger.debug(f"Parsing usage data: {usage_data}")
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
result = await self.settlement_client.parse_usage(usage_data)
|
|
192
|
+
if self.verbose:
|
|
193
|
+
logger.info(f"Successfully parsed usage: {result}")
|
|
194
|
+
return result
|
|
195
|
+
except Exception as e:
|
|
196
|
+
if self.verbose:
|
|
197
|
+
logger.error(f"Error parsing usage data: {e}\n{traceback.format_exc()}")
|
|
198
|
+
else:
|
|
199
|
+
logger.error(f"Error parsing usage data: {e}")
|
|
200
|
+
raise
|
|
201
|
+
|
|
202
|
+
async def calculate_payment(
|
|
203
|
+
self,
|
|
204
|
+
usage: Dict[str, Any],
|
|
205
|
+
input_cost_per_million_usd: float,
|
|
206
|
+
output_cost_per_million_usd: float,
|
|
207
|
+
payment_token: Union[PaymentToken, str] = PaymentToken.SOL,
|
|
208
|
+
) -> Dict[str, Any]:
|
|
209
|
+
"""
|
|
210
|
+
Calculate payment amounts from usage data.
|
|
211
|
+
|
|
212
|
+
This method uses the facilitator to calculate payment amounts based on
|
|
213
|
+
token usage and pricing rates. It does not execute any payment.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
usage: Usage data containing token counts. Supports same formats as
|
|
217
|
+
`parse_usage` method.
|
|
218
|
+
input_cost_per_million_usd: Cost per million input tokens in USD.
|
|
219
|
+
output_cost_per_million_usd: Cost per million output tokens in USD.
|
|
220
|
+
payment_token: Token to use for payment. Must be "SOL" or "USDC".
|
|
221
|
+
Default: PaymentToken.SOL.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Dict with payment calculation details:
|
|
225
|
+
- `status` (str): "calculated" or "skipped" (if zero cost)
|
|
226
|
+
- `pricing` (dict): Pricing information with token counts and costs
|
|
227
|
+
- `payment_amounts` (dict, optional): Payment amounts in token units
|
|
228
|
+
- `token_price_usd` (float, optional): Current token price in USD
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
SettlementServiceError: If the facilitator returns an error.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
```python
|
|
235
|
+
result = await client.calculate_payment(
|
|
236
|
+
usage={"input_tokens": 1000, "output_tokens": 500},
|
|
237
|
+
input_cost_per_million_usd=10.0,
|
|
238
|
+
output_cost_per_million_usd=30.0,
|
|
239
|
+
payment_token=PaymentToken.SOL
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
"""
|
|
243
|
+
payment_token_str = (
|
|
244
|
+
payment_token.value if isinstance(payment_token, PaymentToken) else payment_token
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if self.verbose:
|
|
248
|
+
logger.debug(
|
|
249
|
+
f"Calculating payment: usage={usage}, "
|
|
250
|
+
f"input_cost_per_million_usd={input_cost_per_million_usd}, "
|
|
251
|
+
f"output_cost_per_million_usd={output_cost_per_million_usd}, "
|
|
252
|
+
f"payment_token={payment_token_str}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
result = await self.settlement_client.calculate_payment(
|
|
257
|
+
usage=usage,
|
|
258
|
+
input_cost_per_million_usd=input_cost_per_million_usd,
|
|
259
|
+
output_cost_per_million_usd=output_cost_per_million_usd,
|
|
260
|
+
payment_token=payment_token_str,
|
|
261
|
+
)
|
|
262
|
+
if self.verbose:
|
|
263
|
+
logger.info(f"Payment calculation successful: {result}")
|
|
264
|
+
return result
|
|
265
|
+
except Exception as e:
|
|
266
|
+
if self.verbose:
|
|
267
|
+
logger.error(f"Error calculating payment: {e}\n{traceback.format_exc()}")
|
|
268
|
+
else:
|
|
269
|
+
logger.error(f"Error calculating payment: {e}")
|
|
270
|
+
raise
|
|
271
|
+
|
|
272
|
+
async def settle(
|
|
273
|
+
self,
|
|
274
|
+
usage: Dict[str, Any],
|
|
275
|
+
input_cost_per_million_usd: float,
|
|
276
|
+
output_cost_per_million_usd: float,
|
|
277
|
+
recipient_pubkey: str,
|
|
278
|
+
payment_token: Union[PaymentToken, str] = PaymentToken.SOL,
|
|
279
|
+
skip_preflight: bool = False,
|
|
280
|
+
commitment: str = "confirmed",
|
|
281
|
+
wallet_private_key: Optional[str] = None,
|
|
282
|
+
) -> Dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
Execute a settlement payment on Solana blockchain.
|
|
285
|
+
|
|
286
|
+
This method uses the facilitator to execute a complete settlement:
|
|
287
|
+
parse usage, calculate payment, fetch token prices, create and sign
|
|
288
|
+
transaction, send to Solana, and wait for confirmation.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
usage: Usage data containing token counts. Supports same formats as
|
|
292
|
+
`parse_usage` method.
|
|
293
|
+
input_cost_per_million_usd: Cost per million input tokens in USD.
|
|
294
|
+
output_cost_per_million_usd: Cost per million output tokens in USD.
|
|
295
|
+
recipient_pubkey: Solana public key of the recipient wallet (base58 encoded).
|
|
296
|
+
This wallet receives the net payment after fees.
|
|
297
|
+
payment_token: Token to use for payment. Currently only "SOL" is supported
|
|
298
|
+
for automatic settlement. Default: PaymentToken.SOL.
|
|
299
|
+
skip_preflight: Whether to skip preflight simulation. Default: False.
|
|
300
|
+
commitment: Solana commitment level for transaction confirmation:
|
|
301
|
+
- "processed": Fastest, but may be rolled back
|
|
302
|
+
- "confirmed": Recommended default, confirmed by cluster
|
|
303
|
+
- "finalized": Slowest, but cannot be rolled back
|
|
304
|
+
Default: "confirmed".
|
|
305
|
+
wallet_private_key: Wallet private key to use for payment. If not provided,
|
|
306
|
+
uses the client's default wallet_private_key.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Dict with payment details:
|
|
310
|
+
- `status` (str): "paid" if successful, "skipped" if zero cost
|
|
311
|
+
- `transaction_signature` (str, optional): Solana transaction signature
|
|
312
|
+
- `pricing` (dict): Complete cost breakdown
|
|
313
|
+
- `payment` (dict, optional): Payment details including amounts and splits
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
SettlementServiceError: If the facilitator returns an error.
|
|
317
|
+
ValueError: If wallet_private_key is not provided.
|
|
318
|
+
|
|
319
|
+
Example:
|
|
320
|
+
```python
|
|
321
|
+
result = await client.settle(
|
|
322
|
+
usage={"input_tokens": 1000, "output_tokens": 500},
|
|
323
|
+
input_cost_per_million_usd=10.0,
|
|
324
|
+
output_cost_per_million_usd=30.0,
|
|
325
|
+
recipient_pubkey="RecipientPublicKeyHere",
|
|
326
|
+
payment_token=PaymentToken.SOL
|
|
327
|
+
)
|
|
328
|
+
```
|
|
329
|
+
"""
|
|
330
|
+
private_key = wallet_private_key or self.wallet_private_key
|
|
331
|
+
if not private_key:
|
|
332
|
+
error_msg = (
|
|
333
|
+
"wallet_private_key must be provided either in client initialization "
|
|
334
|
+
"or as a parameter to this method"
|
|
335
|
+
)
|
|
336
|
+
if self.verbose:
|
|
337
|
+
logger.error(f"{error_msg}\n{traceback.format_exc()}")
|
|
338
|
+
else:
|
|
339
|
+
logger.error(error_msg)
|
|
340
|
+
raise ValueError(error_msg)
|
|
341
|
+
|
|
342
|
+
payment_token_str = (
|
|
343
|
+
payment_token.value if isinstance(payment_token, PaymentToken) else payment_token
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if self.verbose:
|
|
347
|
+
logger.debug(
|
|
348
|
+
f"Settling payment: usage={usage}, "
|
|
349
|
+
f"input_cost_per_million_usd={input_cost_per_million_usd}, "
|
|
350
|
+
f"output_cost_per_million_usd={output_cost_per_million_usd}, "
|
|
351
|
+
f"recipient_pubkey={recipient_pubkey}, "
|
|
352
|
+
f"payment_token={payment_token_str}, "
|
|
353
|
+
f"skip_preflight={skip_preflight}, "
|
|
354
|
+
f"commitment={commitment}"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
result = await self.settlement_client.settle(
|
|
359
|
+
private_key=private_key,
|
|
360
|
+
usage=usage,
|
|
361
|
+
input_cost_per_million_usd=input_cost_per_million_usd,
|
|
362
|
+
output_cost_per_million_usd=output_cost_per_million_usd,
|
|
363
|
+
recipient_pubkey=recipient_pubkey,
|
|
364
|
+
payment_token=payment_token_str,
|
|
365
|
+
skip_preflight=skip_preflight,
|
|
366
|
+
commitment=commitment,
|
|
367
|
+
)
|
|
368
|
+
if self.verbose:
|
|
369
|
+
logger.info(f"Settlement successful: {result}")
|
|
370
|
+
return result
|
|
371
|
+
except Exception as e:
|
|
372
|
+
if self.verbose:
|
|
373
|
+
logger.error(f"Error during settlement: {e}\n{traceback.format_exc()}")
|
|
374
|
+
else:
|
|
375
|
+
logger.error(f"Error during settlement: {e}")
|
|
376
|
+
raise
|
|
377
|
+
|
|
378
|
+
async def health_check(self) -> Dict[str, Any]:
|
|
379
|
+
"""
|
|
380
|
+
Check if the facilitator (settlement service) is healthy.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Dict with health status information, typically including:
|
|
384
|
+
- `status` (str): Service status (e.g., "healthy")
|
|
385
|
+
- `service` (str): Service name
|
|
386
|
+
- `version` (str): Service version
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
SettlementServiceError: If the facilitator is unreachable or returns an error.
|
|
390
|
+
|
|
391
|
+
Example:
|
|
392
|
+
```python
|
|
393
|
+
health = await client.health_check()
|
|
394
|
+
print(f"Service status: {health['status']}")
|
|
395
|
+
```
|
|
396
|
+
"""
|
|
397
|
+
if self.verbose:
|
|
398
|
+
logger.debug("Checking settlement service health")
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
result = await self.settlement_client.health_check()
|
|
402
|
+
if self.verbose:
|
|
403
|
+
logger.info(f"Health check successful: {result}")
|
|
404
|
+
return result
|
|
405
|
+
except Exception as e:
|
|
406
|
+
if self.verbose:
|
|
407
|
+
logger.error(f"Error during health check: {e}\n{traceback.format_exc()}")
|
|
408
|
+
else:
|
|
409
|
+
logger.error(f"Error during health check: {e}")
|
|
410
|
+
raise
|
|
411
|
+
|
|
412
|
+
async def request(
|
|
413
|
+
self,
|
|
414
|
+
method: str,
|
|
415
|
+
url: str,
|
|
416
|
+
wallet_private_key: Optional[str] = None,
|
|
417
|
+
auto_decrypt: bool = True,
|
|
418
|
+
**kwargs,
|
|
419
|
+
) -> Dict[str, Any]:
|
|
420
|
+
"""
|
|
421
|
+
Make an HTTP request to an ATP-protected endpoint.
|
|
422
|
+
|
|
423
|
+
This method automatically:
|
|
424
|
+
- Adds wallet authentication headers
|
|
425
|
+
- Handles encrypted responses from ATP middleware
|
|
426
|
+
- Decrypts response data if encrypted
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
method: HTTP method (GET, POST, PUT, DELETE, etc.).
|
|
430
|
+
url: Full URL of the endpoint.
|
|
431
|
+
wallet_private_key: Wallet private key to use. If not provided,
|
|
432
|
+
uses the client's default wallet_private_key.
|
|
433
|
+
auto_decrypt: Whether to automatically decrypt encrypted responses.
|
|
434
|
+
Default: True.
|
|
435
|
+
**kwargs: Additional arguments to pass to httpx (e.g., json, data, params, etc.).
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Dict containing the response data. If the response was encrypted,
|
|
439
|
+
it will be automatically decrypted if auto_decrypt=True.
|
|
440
|
+
|
|
441
|
+
Raises:
|
|
442
|
+
httpx.HTTPError: If the HTTP request fails.
|
|
443
|
+
ValueError: If wallet_private_key is required but not provided.
|
|
444
|
+
|
|
445
|
+
Example:
|
|
446
|
+
```python
|
|
447
|
+
# Make a POST request to an ATP-protected endpoint
|
|
448
|
+
response = await client.request(
|
|
449
|
+
method="POST",
|
|
450
|
+
url="https://api.example.com/v1/chat",
|
|
451
|
+
json={"message": "Hello!"}
|
|
452
|
+
)
|
|
453
|
+
# Response is automatically decrypted if encrypted
|
|
454
|
+
print(response["output"]) # Agent output
|
|
455
|
+
print(response["atp_settlement"]) # Payment details
|
|
456
|
+
```
|
|
457
|
+
"""
|
|
458
|
+
if self.verbose:
|
|
459
|
+
logger.debug(f"Making {method} request to {url}")
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
# Check if wallet key is available
|
|
463
|
+
key = wallet_private_key or self.wallet_private_key
|
|
464
|
+
if not key:
|
|
465
|
+
raise ValueError(
|
|
466
|
+
"wallet_private_key is required for ATP-protected endpoints. "
|
|
467
|
+
"Provide it in client initialization or pass it to the request method. "
|
|
468
|
+
"Example: client = ATPClient(wallet_private_key='[1,2,3,...]')"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Get headers with wallet authentication
|
|
472
|
+
headers = self._get_headers(wallet_private_key=wallet_private_key)
|
|
473
|
+
|
|
474
|
+
# Merge with any headers provided in kwargs
|
|
475
|
+
if "headers" in kwargs:
|
|
476
|
+
headers.update(kwargs.pop("headers"))
|
|
477
|
+
|
|
478
|
+
if self.verbose:
|
|
479
|
+
logger.debug(f"Request headers: {list(headers.keys())}")
|
|
480
|
+
|
|
481
|
+
# Make the request
|
|
482
|
+
async with httpx.AsyncClient(timeout=self.settlement_timeout) as client:
|
|
483
|
+
response = await client.request(
|
|
484
|
+
method=method,
|
|
485
|
+
url=url,
|
|
486
|
+
headers=headers,
|
|
487
|
+
**kwargs,
|
|
488
|
+
)
|
|
489
|
+
response.raise_for_status()
|
|
490
|
+
|
|
491
|
+
if self.verbose:
|
|
492
|
+
logger.debug(f"Response status: {response.status_code}")
|
|
493
|
+
|
|
494
|
+
# Parse JSON response
|
|
495
|
+
try:
|
|
496
|
+
response_data = response.json()
|
|
497
|
+
except json.JSONDecodeError as e:
|
|
498
|
+
if self.verbose:
|
|
499
|
+
logger.warning(f"Response is not JSON, returning as text: {e}")
|
|
500
|
+
# If not JSON, return text
|
|
501
|
+
return {"text": response.text}
|
|
502
|
+
|
|
503
|
+
# Auto-decrypt if enabled and response appears encrypted
|
|
504
|
+
if auto_decrypt:
|
|
505
|
+
if self.verbose:
|
|
506
|
+
logger.debug("Attempting to decrypt response")
|
|
507
|
+
response_data = self.encryptor.decrypt_response_data(
|
|
508
|
+
response_data
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
if self.verbose:
|
|
512
|
+
logger.info(f"Request successful: {method} {url}")
|
|
513
|
+
|
|
514
|
+
return response_data
|
|
515
|
+
except httpx.HTTPError as e:
|
|
516
|
+
if self.verbose:
|
|
517
|
+
logger.error(f"HTTP error during request {method} {url}: {e}\n{traceback.format_exc()}")
|
|
518
|
+
else:
|
|
519
|
+
logger.error(f"HTTP error during request {method} {url}: {e}")
|
|
520
|
+
raise
|
|
521
|
+
except Exception as e:
|
|
522
|
+
if self.verbose:
|
|
523
|
+
logger.error(f"Error during request {method} {url}: {e}\n{traceback.format_exc()}")
|
|
524
|
+
else:
|
|
525
|
+
logger.error(f"Error during request {method} {url}: {e}")
|
|
526
|
+
raise
|
|
527
|
+
|
|
528
|
+
async def post(
|
|
529
|
+
self,
|
|
530
|
+
url: str,
|
|
531
|
+
wallet_private_key: Optional[str] = None,
|
|
532
|
+
auto_decrypt: bool = True,
|
|
533
|
+
**kwargs,
|
|
534
|
+
) -> Dict[str, Any]:
|
|
535
|
+
"""
|
|
536
|
+
Make a POST request to an ATP-protected endpoint.
|
|
537
|
+
|
|
538
|
+
Convenience method for POST requests. See `request` method for details.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
url: Full URL of the endpoint.
|
|
542
|
+
wallet_private_key: Wallet private key to use. If not provided,
|
|
543
|
+
uses the client's default wallet_private_key.
|
|
544
|
+
auto_decrypt: Whether to automatically decrypt encrypted responses.
|
|
545
|
+
Default: True.
|
|
546
|
+
**kwargs: Additional arguments to pass to httpx (e.g., json, data, params, etc.).
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Dict containing the response data.
|
|
550
|
+
|
|
551
|
+
Example:
|
|
552
|
+
```python
|
|
553
|
+
response = await client.post(
|
|
554
|
+
url="https://api.example.com/v1/chat",
|
|
555
|
+
json={"message": "Hello!"}
|
|
556
|
+
)
|
|
557
|
+
```
|
|
558
|
+
"""
|
|
559
|
+
try:
|
|
560
|
+
return await self.request(
|
|
561
|
+
method="POST",
|
|
562
|
+
url=url,
|
|
563
|
+
wallet_private_key=wallet_private_key,
|
|
564
|
+
auto_decrypt=auto_decrypt,
|
|
565
|
+
**kwargs,
|
|
566
|
+
)
|
|
567
|
+
except Exception as e:
|
|
568
|
+
if self.verbose:
|
|
569
|
+
logger.error(f"Error in POST request to {url}: {e}\n{traceback.format_exc()}")
|
|
570
|
+
else:
|
|
571
|
+
logger.error(f"Error in POST request to {url}: {e}")
|
|
572
|
+
raise
|
|
573
|
+
|
|
574
|
+
async def get(
|
|
575
|
+
self,
|
|
576
|
+
url: str,
|
|
577
|
+
wallet_private_key: Optional[str] = None,
|
|
578
|
+
auto_decrypt: bool = True,
|
|
579
|
+
**kwargs,
|
|
580
|
+
) -> Dict[str, Any]:
|
|
581
|
+
"""
|
|
582
|
+
Make a GET request to an ATP-protected endpoint.
|
|
583
|
+
|
|
584
|
+
Convenience method for GET requests. See `request` method for details.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
url: Full URL of the endpoint.
|
|
588
|
+
wallet_private_key: Wallet private key to use. If not provided,
|
|
589
|
+
uses the client's default wallet_private_key.
|
|
590
|
+
auto_decrypt: Whether to automatically decrypt encrypted responses.
|
|
591
|
+
Default: True.
|
|
592
|
+
**kwargs: Additional arguments to pass to httpx (e.g., params, headers, etc.).
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
Dict containing the response data.
|
|
596
|
+
|
|
597
|
+
Example:
|
|
598
|
+
```python
|
|
599
|
+
response = await client.get(
|
|
600
|
+
url="https://api.example.com/v1/status",
|
|
601
|
+
params={"id": "123"}
|
|
602
|
+
)
|
|
603
|
+
```
|
|
604
|
+
"""
|
|
605
|
+
try:
|
|
606
|
+
return await self.request(
|
|
607
|
+
method="GET",
|
|
608
|
+
url=url,
|
|
609
|
+
wallet_private_key=wallet_private_key,
|
|
610
|
+
auto_decrypt=auto_decrypt,
|
|
611
|
+
**kwargs,
|
|
612
|
+
)
|
|
613
|
+
except Exception as e:
|
|
614
|
+
if self.verbose:
|
|
615
|
+
logger.error(f"Error in GET request to {url}: {e}\n{traceback.format_exc()}")
|
|
616
|
+
else:
|
|
617
|
+
logger.error(f"Error in GET request to {url}: {e}")
|
|
618
|
+
raise
|
atp/config.py
CHANGED
|
@@ -15,6 +15,18 @@ load_dotenv()
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _bool_env(name: str, default: bool = False) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Retrieve a boolean value from an environment variable.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
name (str): The name of the environment variable.
|
|
23
|
+
default (bool, optional): The default value to return if the variable is not set.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
bool: True if the environment variable contains a truthy value
|
|
27
|
+
(one of "1", "true", "yes", "y", "on", case-insensitive),
|
|
28
|
+
otherwise False or the provided default.
|
|
29
|
+
"""
|
|
18
30
|
v = os.getenv(name)
|
|
19
31
|
if v is None:
|
|
20
32
|
return default
|
|
@@ -22,6 +34,15 @@ def _bool_env(name: str, default: bool = False) -> bool:
|
|
|
22
34
|
|
|
23
35
|
|
|
24
36
|
def _float_env(name: str) -> Optional[float]:
|
|
37
|
+
"""
|
|
38
|
+
Retrieve a float value from an environment variable.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
name (str): The name of the environment variable.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Optional[float]: The float value if set and valid, otherwise None.
|
|
45
|
+
"""
|
|
25
46
|
v = os.getenv(name)
|
|
26
47
|
if v is None:
|
|
27
48
|
return None
|
|
@@ -75,3 +96,8 @@ ATP_SOLANA_DEBUG = _bool_env("ATP_SOLANA_DEBUG", default=False)
|
|
|
75
96
|
ATP_SETTLEMENT_URL = os.getenv(
|
|
76
97
|
"ATP_SETTLEMENT_URL", "https://facilitator.swarms.world"
|
|
77
98
|
)
|
|
99
|
+
|
|
100
|
+
# Settlement Service Timeout (in seconds)
|
|
101
|
+
# Settlement operations can take longer due to blockchain confirmation times
|
|
102
|
+
# Default: 300 seconds (5 minutes) - can be overridden via environment variable or middleware parameter
|
|
103
|
+
ATP_SETTLEMENT_TIMEOUT = _float_env("ATP_SETTLEMENT_TIMEOUT") or 300.0
|