uvd-x402-sdk 0.2.2__py3-none-any.whl → 0.2.3__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.
- uvd_x402_sdk/__init__.py +169 -169
- uvd_x402_sdk/client.py +527 -527
- uvd_x402_sdk/config.py +248 -248
- uvd_x402_sdk/decorators.py +325 -325
- uvd_x402_sdk/exceptions.py +254 -254
- uvd_x402_sdk/integrations/__init__.py +74 -74
- uvd_x402_sdk/integrations/django_integration.py +237 -237
- uvd_x402_sdk/integrations/fastapi_integration.py +330 -330
- uvd_x402_sdk/integrations/flask_integration.py +259 -259
- uvd_x402_sdk/integrations/lambda_integration.py +320 -320
- uvd_x402_sdk/models.py +397 -397
- uvd_x402_sdk/networks/__init__.py +54 -54
- uvd_x402_sdk/networks/base.py +347 -347
- uvd_x402_sdk/networks/evm.py +215 -215
- uvd_x402_sdk/networks/near.py +397 -397
- uvd_x402_sdk/networks/solana.py +282 -282
- uvd_x402_sdk/networks/stellar.py +129 -129
- uvd_x402_sdk/response.py +439 -439
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.2.3.dist-info}/LICENSE +21 -21
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.2.3.dist-info}/METADATA +814 -778
- uvd_x402_sdk-0.2.3.dist-info/RECORD +23 -0
- uvd_x402_sdk-0.2.2.dist-info/RECORD +0 -23
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.2.3.dist-info}/WHEEL +0 -0
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.2.3.dist-info}/top_level.txt +0 -0
uvd_x402_sdk/decorators.py
CHANGED
|
@@ -1,325 +1,325 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Decorators for protecting endpoints with x402 payments.
|
|
3
|
-
|
|
4
|
-
These decorators provide a clean, declarative way to require payment
|
|
5
|
-
for specific endpoints across different web frameworks.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from decimal import Decimal
|
|
9
|
-
from functools import wraps
|
|
10
|
-
from typing import Any, Callable, Optional, TypeVar, Union, Dict
|
|
11
|
-
|
|
12
|
-
from uvd_x402_sdk.config import X402Config
|
|
13
|
-
from uvd_x402_sdk.client import X402Client
|
|
14
|
-
from uvd_x402_sdk.exceptions import (
|
|
15
|
-
X402Error,
|
|
16
|
-
PaymentRequiredError,
|
|
17
|
-
InvalidPayloadError,
|
|
18
|
-
)
|
|
19
|
-
from uvd_x402_sdk.response import create_402_response, create_402_headers
|
|
20
|
-
|
|
21
|
-
F = TypeVar("F", bound=Callable[..., Any])
|
|
22
|
-
|
|
23
|
-
# Global client instance (set by configure_x402)
|
|
24
|
-
_global_client: Optional[X402Client] = None
|
|
25
|
-
_global_config: Optional[X402Config] = None
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def configure_x402(
|
|
29
|
-
config: Optional[X402Config] = None,
|
|
30
|
-
recipient_address: Optional[str] = None,
|
|
31
|
-
facilitator_url: str = "https://facilitator.ultravioletadao.xyz",
|
|
32
|
-
**kwargs: Any,
|
|
33
|
-
) -> None:
|
|
34
|
-
"""
|
|
35
|
-
Configure the global x402 client for decorator usage.
|
|
36
|
-
|
|
37
|
-
Call this once during application startup to set up the x402 client
|
|
38
|
-
that decorators will use.
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
config: Full X402Config object
|
|
42
|
-
recipient_address: Default recipient for EVM chains
|
|
43
|
-
facilitator_url: Facilitator service URL
|
|
44
|
-
**kwargs: Additional config parameters
|
|
45
|
-
|
|
46
|
-
Example:
|
|
47
|
-
>>> from uvd_x402_sdk import configure_x402
|
|
48
|
-
>>> configure_x402(
|
|
49
|
-
... recipient_address="0xYourWallet...",
|
|
50
|
-
... facilitator_url="https://facilitator.ultravioletadao.xyz"
|
|
51
|
-
... )
|
|
52
|
-
"""
|
|
53
|
-
global _global_client, _global_config
|
|
54
|
-
|
|
55
|
-
if config:
|
|
56
|
-
_global_config = config
|
|
57
|
-
else:
|
|
58
|
-
_global_config = X402Config(
|
|
59
|
-
facilitator_url=facilitator_url,
|
|
60
|
-
recipient_evm=recipient_address or "",
|
|
61
|
-
**kwargs,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
_global_client = X402Client(config=_global_config)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def get_x402_client() -> X402Client:
|
|
68
|
-
"""Get the global x402 client instance."""
|
|
69
|
-
if _global_client is None:
|
|
70
|
-
raise RuntimeError(
|
|
71
|
-
"x402 not configured. Call configure_x402() during application startup."
|
|
72
|
-
)
|
|
73
|
-
return _global_client
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def get_x402_config() -> X402Config:
|
|
77
|
-
"""Get the global x402 config instance."""
|
|
78
|
-
if _global_config is None:
|
|
79
|
-
raise RuntimeError(
|
|
80
|
-
"x402 not configured. Call configure_x402() during application startup."
|
|
81
|
-
)
|
|
82
|
-
return _global_config
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def require_payment(
|
|
86
|
-
amount_usd: Union[Decimal, float, str],
|
|
87
|
-
amount_callback: Optional[Callable[..., Decimal]] = None,
|
|
88
|
-
networks: Optional[list] = None,
|
|
89
|
-
message: Optional[str] = None,
|
|
90
|
-
inject_result: bool = True,
|
|
91
|
-
header_name: str = "X-PAYMENT",
|
|
92
|
-
) -> Callable[[F], F]:
|
|
93
|
-
"""
|
|
94
|
-
Decorator that requires x402 payment for an endpoint.
|
|
95
|
-
|
|
96
|
-
This is the main decorator for protecting endpoints with payments.
|
|
97
|
-
It handles the complete payment flow:
|
|
98
|
-
1. Check for X-PAYMENT header
|
|
99
|
-
2. If missing, return 402 Payment Required
|
|
100
|
-
3. If present, verify and settle payment
|
|
101
|
-
4. Optionally inject PaymentResult into function
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
amount_usd: Fixed payment amount in USD (or use amount_callback)
|
|
105
|
-
amount_callback: Callable that returns dynamic amount based on request
|
|
106
|
-
networks: Limit to specific networks (default: all enabled)
|
|
107
|
-
message: Custom message for 402 response
|
|
108
|
-
inject_result: Whether to inject PaymentResult as 'payment_result' kwarg
|
|
109
|
-
header_name: Header name for payment (default: X-PAYMENT)
|
|
110
|
-
|
|
111
|
-
Returns:
|
|
112
|
-
Decorated function
|
|
113
|
-
|
|
114
|
-
Example (Flask):
|
|
115
|
-
>>> @app.route("/api/premium")
|
|
116
|
-
>>> @require_payment(amount_usd=Decimal("1.00"))
|
|
117
|
-
>>> def premium_endpoint(payment_result=None):
|
|
118
|
-
... return {"message": f"Paid by {payment_result.payer_address}"}
|
|
119
|
-
|
|
120
|
-
Example (Dynamic pricing):
|
|
121
|
-
>>> def calculate_price(request):
|
|
122
|
-
... items = request.json.get("items", 1)
|
|
123
|
-
... return Decimal(str(items * 0.10))
|
|
124
|
-
>>>
|
|
125
|
-
>>> @require_payment(amount_callback=calculate_price)
|
|
126
|
-
>>> def dynamic_endpoint(payment_result=None):
|
|
127
|
-
... return {"message": "Success"}
|
|
128
|
-
"""
|
|
129
|
-
|
|
130
|
-
def decorator(func: F) -> F:
|
|
131
|
-
@wraps(func)
|
|
132
|
-
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
133
|
-
# Get client and config
|
|
134
|
-
client = get_x402_client()
|
|
135
|
-
config = get_x402_config()
|
|
136
|
-
|
|
137
|
-
# Framework-agnostic request extraction
|
|
138
|
-
# This will be overridden by framework-specific integrations
|
|
139
|
-
request = _extract_request(*args, **kwargs)
|
|
140
|
-
payment_header = _get_header(request, header_name)
|
|
141
|
-
|
|
142
|
-
# Determine amount
|
|
143
|
-
if amount_callback:
|
|
144
|
-
required_amount = Decimal(str(amount_callback(request)))
|
|
145
|
-
else:
|
|
146
|
-
required_amount = Decimal(str(amount_usd))
|
|
147
|
-
|
|
148
|
-
# No payment header - return 402
|
|
149
|
-
if not payment_header:
|
|
150
|
-
response_body = create_402_response(
|
|
151
|
-
amount_usd=required_amount,
|
|
152
|
-
config=config,
|
|
153
|
-
message=message,
|
|
154
|
-
)
|
|
155
|
-
return _create_402_response(response_body)
|
|
156
|
-
|
|
157
|
-
# Process payment
|
|
158
|
-
try:
|
|
159
|
-
result = client.process_payment(
|
|
160
|
-
x_payment_header=payment_header,
|
|
161
|
-
expected_amount_usd=required_amount,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# Inject result if requested
|
|
165
|
-
if inject_result:
|
|
166
|
-
kwargs["payment_result"] = result
|
|
167
|
-
|
|
168
|
-
return func(*args, **kwargs)
|
|
169
|
-
|
|
170
|
-
except X402Error as e:
|
|
171
|
-
# Return appropriate error response
|
|
172
|
-
return _create_error_response(e, required_amount, config)
|
|
173
|
-
|
|
174
|
-
return wrapper # type: ignore
|
|
175
|
-
|
|
176
|
-
return decorator
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
# Alias for backward compatibility and preference
|
|
180
|
-
x402_required = require_payment
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def _extract_request(*args: Any, **kwargs: Any) -> Any:
|
|
184
|
-
"""
|
|
185
|
-
Extract request object from function arguments.
|
|
186
|
-
|
|
187
|
-
This is a framework-agnostic implementation that works with:
|
|
188
|
-
- Flask (request from flask.globals)
|
|
189
|
-
- FastAPI/Starlette (request in kwargs)
|
|
190
|
-
- Django (request as first arg)
|
|
191
|
-
- AWS Lambda (event dict as first arg)
|
|
192
|
-
"""
|
|
193
|
-
# Check kwargs first
|
|
194
|
-
if "request" in kwargs:
|
|
195
|
-
return kwargs["request"]
|
|
196
|
-
|
|
197
|
-
# Check first positional arg
|
|
198
|
-
if args:
|
|
199
|
-
return args[0]
|
|
200
|
-
|
|
201
|
-
# Try Flask global
|
|
202
|
-
try:
|
|
203
|
-
from flask import request
|
|
204
|
-
return request
|
|
205
|
-
except ImportError:
|
|
206
|
-
pass
|
|
207
|
-
|
|
208
|
-
# No request found
|
|
209
|
-
return None
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def _get_header(request: Any, header_name: str) -> Optional[str]:
|
|
213
|
-
"""
|
|
214
|
-
Get header value from request object.
|
|
215
|
-
|
|
216
|
-
Handles different frameworks' request objects.
|
|
217
|
-
"""
|
|
218
|
-
if request is None:
|
|
219
|
-
return None
|
|
220
|
-
|
|
221
|
-
# Dict-like (AWS Lambda event)
|
|
222
|
-
if isinstance(request, dict):
|
|
223
|
-
headers = request.get("headers", {})
|
|
224
|
-
# Lambda headers can be case-sensitive or not
|
|
225
|
-
return headers.get(header_name) or headers.get(header_name.lower())
|
|
226
|
-
|
|
227
|
-
# Flask/Werkzeug
|
|
228
|
-
if hasattr(request, "headers"):
|
|
229
|
-
headers = request.headers
|
|
230
|
-
if hasattr(headers, "get"):
|
|
231
|
-
return headers.get(header_name)
|
|
232
|
-
|
|
233
|
-
# Django
|
|
234
|
-
if hasattr(request, "META"):
|
|
235
|
-
# Django uses HTTP_X_PAYMENT format
|
|
236
|
-
django_header = f"HTTP_{header_name.replace('-', '_').upper()}"
|
|
237
|
-
return request.META.get(django_header)
|
|
238
|
-
|
|
239
|
-
return None
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def _create_402_response(body: Dict[str, Any]) -> Any:
|
|
243
|
-
"""
|
|
244
|
-
Create a 402 response appropriate for the current framework.
|
|
245
|
-
|
|
246
|
-
This is a fallback that returns a tuple. Framework-specific
|
|
247
|
-
integrations override this.
|
|
248
|
-
"""
|
|
249
|
-
# Try Flask
|
|
250
|
-
try:
|
|
251
|
-
from flask import jsonify, make_response
|
|
252
|
-
response = make_response(jsonify(body), 402)
|
|
253
|
-
for key, value in create_402_headers().items():
|
|
254
|
-
response.headers[key] = value
|
|
255
|
-
return response
|
|
256
|
-
except ImportError:
|
|
257
|
-
pass
|
|
258
|
-
|
|
259
|
-
# Try FastAPI/Starlette
|
|
260
|
-
try:
|
|
261
|
-
from starlette.responses import JSONResponse
|
|
262
|
-
return JSONResponse(
|
|
263
|
-
status_code=402,
|
|
264
|
-
content=body,
|
|
265
|
-
headers=create_402_headers(),
|
|
266
|
-
)
|
|
267
|
-
except ImportError:
|
|
268
|
-
pass
|
|
269
|
-
|
|
270
|
-
# Try Django
|
|
271
|
-
try:
|
|
272
|
-
from django.http import JsonResponse
|
|
273
|
-
response = JsonResponse(body, status=402)
|
|
274
|
-
for key, value in create_402_headers().items():
|
|
275
|
-
response[key] = value
|
|
276
|
-
return response
|
|
277
|
-
except ImportError:
|
|
278
|
-
pass
|
|
279
|
-
|
|
280
|
-
# Fallback: return dict (for AWS Lambda or raw WSGI)
|
|
281
|
-
return {
|
|
282
|
-
"statusCode": 402,
|
|
283
|
-
"headers": {
|
|
284
|
-
"Content-Type": "application/json",
|
|
285
|
-
**create_402_headers(),
|
|
286
|
-
},
|
|
287
|
-
"body": body,
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def _create_error_response(
|
|
292
|
-
error: X402Error,
|
|
293
|
-
amount: Decimal,
|
|
294
|
-
config: X402Config,
|
|
295
|
-
) -> Any:
|
|
296
|
-
"""Create an error response for x402 errors."""
|
|
297
|
-
# Payment verification/settlement failed - return 402
|
|
298
|
-
if isinstance(error, (PaymentRequiredError, InvalidPayloadError)):
|
|
299
|
-
body = create_402_response(amount_usd=amount, config=config)
|
|
300
|
-
body["error"] = error.message
|
|
301
|
-
body["details"] = error.details
|
|
302
|
-
return _create_402_response(body)
|
|
303
|
-
|
|
304
|
-
# Other errors - return 400
|
|
305
|
-
body = error.to_dict()
|
|
306
|
-
|
|
307
|
-
try:
|
|
308
|
-
from flask import jsonify, make_response
|
|
309
|
-
return make_response(jsonify(body), 400)
|
|
310
|
-
except ImportError:
|
|
311
|
-
pass
|
|
312
|
-
|
|
313
|
-
try:
|
|
314
|
-
from starlette.responses import JSONResponse
|
|
315
|
-
return JSONResponse(status_code=400, content=body)
|
|
316
|
-
except ImportError:
|
|
317
|
-
pass
|
|
318
|
-
|
|
319
|
-
try:
|
|
320
|
-
from django.http import JsonResponse
|
|
321
|
-
return JsonResponse(body, status=400)
|
|
322
|
-
except ImportError:
|
|
323
|
-
pass
|
|
324
|
-
|
|
325
|
-
return {"statusCode": 400, "body": body}
|
|
1
|
+
"""
|
|
2
|
+
Decorators for protecting endpoints with x402 payments.
|
|
3
|
+
|
|
4
|
+
These decorators provide a clean, declarative way to require payment
|
|
5
|
+
for specific endpoints across different web frameworks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from decimal import Decimal
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Any, Callable, Optional, TypeVar, Union, Dict
|
|
11
|
+
|
|
12
|
+
from uvd_x402_sdk.config import X402Config
|
|
13
|
+
from uvd_x402_sdk.client import X402Client
|
|
14
|
+
from uvd_x402_sdk.exceptions import (
|
|
15
|
+
X402Error,
|
|
16
|
+
PaymentRequiredError,
|
|
17
|
+
InvalidPayloadError,
|
|
18
|
+
)
|
|
19
|
+
from uvd_x402_sdk.response import create_402_response, create_402_headers
|
|
20
|
+
|
|
21
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
22
|
+
|
|
23
|
+
# Global client instance (set by configure_x402)
|
|
24
|
+
_global_client: Optional[X402Client] = None
|
|
25
|
+
_global_config: Optional[X402Config] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def configure_x402(
|
|
29
|
+
config: Optional[X402Config] = None,
|
|
30
|
+
recipient_address: Optional[str] = None,
|
|
31
|
+
facilitator_url: str = "https://facilitator.ultravioletadao.xyz",
|
|
32
|
+
**kwargs: Any,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Configure the global x402 client for decorator usage.
|
|
36
|
+
|
|
37
|
+
Call this once during application startup to set up the x402 client
|
|
38
|
+
that decorators will use.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
config: Full X402Config object
|
|
42
|
+
recipient_address: Default recipient for EVM chains
|
|
43
|
+
facilitator_url: Facilitator service URL
|
|
44
|
+
**kwargs: Additional config parameters
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> from uvd_x402_sdk import configure_x402
|
|
48
|
+
>>> configure_x402(
|
|
49
|
+
... recipient_address="0xYourWallet...",
|
|
50
|
+
... facilitator_url="https://facilitator.ultravioletadao.xyz"
|
|
51
|
+
... )
|
|
52
|
+
"""
|
|
53
|
+
global _global_client, _global_config
|
|
54
|
+
|
|
55
|
+
if config:
|
|
56
|
+
_global_config = config
|
|
57
|
+
else:
|
|
58
|
+
_global_config = X402Config(
|
|
59
|
+
facilitator_url=facilitator_url,
|
|
60
|
+
recipient_evm=recipient_address or "",
|
|
61
|
+
**kwargs,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
_global_client = X402Client(config=_global_config)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_x402_client() -> X402Client:
|
|
68
|
+
"""Get the global x402 client instance."""
|
|
69
|
+
if _global_client is None:
|
|
70
|
+
raise RuntimeError(
|
|
71
|
+
"x402 not configured. Call configure_x402() during application startup."
|
|
72
|
+
)
|
|
73
|
+
return _global_client
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_x402_config() -> X402Config:
|
|
77
|
+
"""Get the global x402 config instance."""
|
|
78
|
+
if _global_config is None:
|
|
79
|
+
raise RuntimeError(
|
|
80
|
+
"x402 not configured. Call configure_x402() during application startup."
|
|
81
|
+
)
|
|
82
|
+
return _global_config
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def require_payment(
|
|
86
|
+
amount_usd: Union[Decimal, float, str],
|
|
87
|
+
amount_callback: Optional[Callable[..., Decimal]] = None,
|
|
88
|
+
networks: Optional[list] = None,
|
|
89
|
+
message: Optional[str] = None,
|
|
90
|
+
inject_result: bool = True,
|
|
91
|
+
header_name: str = "X-PAYMENT",
|
|
92
|
+
) -> Callable[[F], F]:
|
|
93
|
+
"""
|
|
94
|
+
Decorator that requires x402 payment for an endpoint.
|
|
95
|
+
|
|
96
|
+
This is the main decorator for protecting endpoints with payments.
|
|
97
|
+
It handles the complete payment flow:
|
|
98
|
+
1. Check for X-PAYMENT header
|
|
99
|
+
2. If missing, return 402 Payment Required
|
|
100
|
+
3. If present, verify and settle payment
|
|
101
|
+
4. Optionally inject PaymentResult into function
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
amount_usd: Fixed payment amount in USD (or use amount_callback)
|
|
105
|
+
amount_callback: Callable that returns dynamic amount based on request
|
|
106
|
+
networks: Limit to specific networks (default: all enabled)
|
|
107
|
+
message: Custom message for 402 response
|
|
108
|
+
inject_result: Whether to inject PaymentResult as 'payment_result' kwarg
|
|
109
|
+
header_name: Header name for payment (default: X-PAYMENT)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Decorated function
|
|
113
|
+
|
|
114
|
+
Example (Flask):
|
|
115
|
+
>>> @app.route("/api/premium")
|
|
116
|
+
>>> @require_payment(amount_usd=Decimal("1.00"))
|
|
117
|
+
>>> def premium_endpoint(payment_result=None):
|
|
118
|
+
... return {"message": f"Paid by {payment_result.payer_address}"}
|
|
119
|
+
|
|
120
|
+
Example (Dynamic pricing):
|
|
121
|
+
>>> def calculate_price(request):
|
|
122
|
+
... items = request.json.get("items", 1)
|
|
123
|
+
... return Decimal(str(items * 0.10))
|
|
124
|
+
>>>
|
|
125
|
+
>>> @require_payment(amount_callback=calculate_price)
|
|
126
|
+
>>> def dynamic_endpoint(payment_result=None):
|
|
127
|
+
... return {"message": "Success"}
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def decorator(func: F) -> F:
|
|
131
|
+
@wraps(func)
|
|
132
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
133
|
+
# Get client and config
|
|
134
|
+
client = get_x402_client()
|
|
135
|
+
config = get_x402_config()
|
|
136
|
+
|
|
137
|
+
# Framework-agnostic request extraction
|
|
138
|
+
# This will be overridden by framework-specific integrations
|
|
139
|
+
request = _extract_request(*args, **kwargs)
|
|
140
|
+
payment_header = _get_header(request, header_name)
|
|
141
|
+
|
|
142
|
+
# Determine amount
|
|
143
|
+
if amount_callback:
|
|
144
|
+
required_amount = Decimal(str(amount_callback(request)))
|
|
145
|
+
else:
|
|
146
|
+
required_amount = Decimal(str(amount_usd))
|
|
147
|
+
|
|
148
|
+
# No payment header - return 402
|
|
149
|
+
if not payment_header:
|
|
150
|
+
response_body = create_402_response(
|
|
151
|
+
amount_usd=required_amount,
|
|
152
|
+
config=config,
|
|
153
|
+
message=message,
|
|
154
|
+
)
|
|
155
|
+
return _create_402_response(response_body)
|
|
156
|
+
|
|
157
|
+
# Process payment
|
|
158
|
+
try:
|
|
159
|
+
result = client.process_payment(
|
|
160
|
+
x_payment_header=payment_header,
|
|
161
|
+
expected_amount_usd=required_amount,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Inject result if requested
|
|
165
|
+
if inject_result:
|
|
166
|
+
kwargs["payment_result"] = result
|
|
167
|
+
|
|
168
|
+
return func(*args, **kwargs)
|
|
169
|
+
|
|
170
|
+
except X402Error as e:
|
|
171
|
+
# Return appropriate error response
|
|
172
|
+
return _create_error_response(e, required_amount, config)
|
|
173
|
+
|
|
174
|
+
return wrapper # type: ignore
|
|
175
|
+
|
|
176
|
+
return decorator
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Alias for backward compatibility and preference
|
|
180
|
+
x402_required = require_payment
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _extract_request(*args: Any, **kwargs: Any) -> Any:
|
|
184
|
+
"""
|
|
185
|
+
Extract request object from function arguments.
|
|
186
|
+
|
|
187
|
+
This is a framework-agnostic implementation that works with:
|
|
188
|
+
- Flask (request from flask.globals)
|
|
189
|
+
- FastAPI/Starlette (request in kwargs)
|
|
190
|
+
- Django (request as first arg)
|
|
191
|
+
- AWS Lambda (event dict as first arg)
|
|
192
|
+
"""
|
|
193
|
+
# Check kwargs first
|
|
194
|
+
if "request" in kwargs:
|
|
195
|
+
return kwargs["request"]
|
|
196
|
+
|
|
197
|
+
# Check first positional arg
|
|
198
|
+
if args:
|
|
199
|
+
return args[0]
|
|
200
|
+
|
|
201
|
+
# Try Flask global
|
|
202
|
+
try:
|
|
203
|
+
from flask import request
|
|
204
|
+
return request
|
|
205
|
+
except ImportError:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
# No request found
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _get_header(request: Any, header_name: str) -> Optional[str]:
|
|
213
|
+
"""
|
|
214
|
+
Get header value from request object.
|
|
215
|
+
|
|
216
|
+
Handles different frameworks' request objects.
|
|
217
|
+
"""
|
|
218
|
+
if request is None:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
# Dict-like (AWS Lambda event)
|
|
222
|
+
if isinstance(request, dict):
|
|
223
|
+
headers = request.get("headers", {})
|
|
224
|
+
# Lambda headers can be case-sensitive or not
|
|
225
|
+
return headers.get(header_name) or headers.get(header_name.lower())
|
|
226
|
+
|
|
227
|
+
# Flask/Werkzeug
|
|
228
|
+
if hasattr(request, "headers"):
|
|
229
|
+
headers = request.headers
|
|
230
|
+
if hasattr(headers, "get"):
|
|
231
|
+
return headers.get(header_name)
|
|
232
|
+
|
|
233
|
+
# Django
|
|
234
|
+
if hasattr(request, "META"):
|
|
235
|
+
# Django uses HTTP_X_PAYMENT format
|
|
236
|
+
django_header = f"HTTP_{header_name.replace('-', '_').upper()}"
|
|
237
|
+
return request.META.get(django_header)
|
|
238
|
+
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _create_402_response(body: Dict[str, Any]) -> Any:
|
|
243
|
+
"""
|
|
244
|
+
Create a 402 response appropriate for the current framework.
|
|
245
|
+
|
|
246
|
+
This is a fallback that returns a tuple. Framework-specific
|
|
247
|
+
integrations override this.
|
|
248
|
+
"""
|
|
249
|
+
# Try Flask
|
|
250
|
+
try:
|
|
251
|
+
from flask import jsonify, make_response
|
|
252
|
+
response = make_response(jsonify(body), 402)
|
|
253
|
+
for key, value in create_402_headers().items():
|
|
254
|
+
response.headers[key] = value
|
|
255
|
+
return response
|
|
256
|
+
except ImportError:
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
# Try FastAPI/Starlette
|
|
260
|
+
try:
|
|
261
|
+
from starlette.responses import JSONResponse
|
|
262
|
+
return JSONResponse(
|
|
263
|
+
status_code=402,
|
|
264
|
+
content=body,
|
|
265
|
+
headers=create_402_headers(),
|
|
266
|
+
)
|
|
267
|
+
except ImportError:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
# Try Django
|
|
271
|
+
try:
|
|
272
|
+
from django.http import JsonResponse
|
|
273
|
+
response = JsonResponse(body, status=402)
|
|
274
|
+
for key, value in create_402_headers().items():
|
|
275
|
+
response[key] = value
|
|
276
|
+
return response
|
|
277
|
+
except ImportError:
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
# Fallback: return dict (for AWS Lambda or raw WSGI)
|
|
281
|
+
return {
|
|
282
|
+
"statusCode": 402,
|
|
283
|
+
"headers": {
|
|
284
|
+
"Content-Type": "application/json",
|
|
285
|
+
**create_402_headers(),
|
|
286
|
+
},
|
|
287
|
+
"body": body,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _create_error_response(
|
|
292
|
+
error: X402Error,
|
|
293
|
+
amount: Decimal,
|
|
294
|
+
config: X402Config,
|
|
295
|
+
) -> Any:
|
|
296
|
+
"""Create an error response for x402 errors."""
|
|
297
|
+
# Payment verification/settlement failed - return 402
|
|
298
|
+
if isinstance(error, (PaymentRequiredError, InvalidPayloadError)):
|
|
299
|
+
body = create_402_response(amount_usd=amount, config=config)
|
|
300
|
+
body["error"] = error.message
|
|
301
|
+
body["details"] = error.details
|
|
302
|
+
return _create_402_response(body)
|
|
303
|
+
|
|
304
|
+
# Other errors - return 400
|
|
305
|
+
body = error.to_dict()
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
from flask import jsonify, make_response
|
|
309
|
+
return make_response(jsonify(body), 400)
|
|
310
|
+
except ImportError:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
from starlette.responses import JSONResponse
|
|
315
|
+
return JSONResponse(status_code=400, content=body)
|
|
316
|
+
except ImportError:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
from django.http import JsonResponse
|
|
321
|
+
return JsonResponse(body, status=400)
|
|
322
|
+
except ImportError:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
return {"statusCode": 400, "body": body}
|