uvd-x402-sdk 0.2.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.
@@ -0,0 +1,254 @@
1
+ """
2
+ Custom exceptions for the x402 SDK.
3
+
4
+ These exceptions provide clear, actionable error messages for different
5
+ failure scenarios in the payment flow.
6
+ """
7
+
8
+ from typing import Optional, List, Dict, Any
9
+
10
+
11
+ class X402Error(Exception):
12
+ """Base exception for all x402 SDK errors."""
13
+
14
+ def __init__(
15
+ self,
16
+ message: str,
17
+ code: Optional[str] = None,
18
+ details: Optional[Dict[str, Any]] = None,
19
+ ) -> None:
20
+ self.message = message
21
+ self.code = code or "X402_ERROR"
22
+ self.details = details or {}
23
+ super().__init__(self.message)
24
+
25
+ def to_dict(self) -> Dict[str, Any]:
26
+ """Convert exception to dictionary for API responses."""
27
+ return {
28
+ "error": self.code,
29
+ "message": self.message,
30
+ "details": self.details,
31
+ }
32
+
33
+
34
+ class PaymentRequiredError(X402Error):
35
+ """
36
+ Raised when a payment is required but not provided.
37
+
38
+ This should trigger a 402 Payment Required response.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ message: str = "Payment required",
44
+ amount_usd: Optional[str] = None,
45
+ recipient: Optional[str] = None,
46
+ supported_networks: Optional[List[str]] = None,
47
+ ) -> None:
48
+ details = {}
49
+ if amount_usd:
50
+ details["amount"] = amount_usd
51
+ if recipient:
52
+ details["recipient"] = recipient
53
+ if supported_networks:
54
+ details["supportedNetworks"] = supported_networks
55
+
56
+ super().__init__(
57
+ message=message,
58
+ code="PAYMENT_REQUIRED",
59
+ details=details,
60
+ )
61
+ self.amount_usd = amount_usd
62
+ self.recipient = recipient
63
+ self.supported_networks = supported_networks
64
+
65
+
66
+ class PaymentVerificationError(X402Error):
67
+ """
68
+ Raised when payment verification fails.
69
+
70
+ Common causes:
71
+ - Invalid signature
72
+ - Amount mismatch
73
+ - Wrong recipient
74
+ - Expired payment authorization
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ message: str,
80
+ reason: Optional[str] = None,
81
+ errors: Optional[List[str]] = None,
82
+ ) -> None:
83
+ details = {}
84
+ if reason:
85
+ details["reason"] = reason
86
+ if errors:
87
+ details["errors"] = errors
88
+
89
+ super().__init__(
90
+ message=message,
91
+ code="PAYMENT_VERIFICATION_FAILED",
92
+ details=details,
93
+ )
94
+ self.reason = reason
95
+ self.errors = errors or []
96
+
97
+
98
+ class PaymentSettlementError(X402Error):
99
+ """
100
+ Raised when payment settlement fails on-chain.
101
+
102
+ Common causes:
103
+ - Insufficient USDC balance
104
+ - Nonce already used
105
+ - Authorization expired
106
+ - Network congestion/timeout
107
+ """
108
+
109
+ def __init__(
110
+ self,
111
+ message: str,
112
+ network: Optional[str] = None,
113
+ tx_hash: Optional[str] = None,
114
+ reason: Optional[str] = None,
115
+ ) -> None:
116
+ details = {}
117
+ if network:
118
+ details["network"] = network
119
+ if tx_hash:
120
+ details["transactionHash"] = tx_hash
121
+ if reason:
122
+ details["reason"] = reason
123
+
124
+ super().__init__(
125
+ message=message,
126
+ code="PAYMENT_SETTLEMENT_FAILED",
127
+ details=details,
128
+ )
129
+ self.network = network
130
+ self.tx_hash = tx_hash
131
+ self.reason = reason
132
+
133
+
134
+ class UnsupportedNetworkError(X402Error):
135
+ """
136
+ Raised when an unsupported network is specified.
137
+
138
+ Use `register_network()` to add custom network support.
139
+ """
140
+
141
+ def __init__(
142
+ self,
143
+ network: str,
144
+ supported_networks: Optional[List[str]] = None,
145
+ ) -> None:
146
+ super().__init__(
147
+ message=f"Unsupported network: {network}",
148
+ code="UNSUPPORTED_NETWORK",
149
+ details={
150
+ "requestedNetwork": network,
151
+ "supportedNetworks": supported_networks or [],
152
+ },
153
+ )
154
+ self.network = network
155
+ self.supported_networks = supported_networks or []
156
+
157
+
158
+ class InvalidPayloadError(X402Error):
159
+ """
160
+ Raised when the X-PAYMENT header payload is invalid.
161
+
162
+ Common causes:
163
+ - Invalid base64 encoding
164
+ - Invalid JSON format
165
+ - Missing required fields
166
+ - Invalid x402 version
167
+ """
168
+
169
+ def __init__(
170
+ self,
171
+ message: str,
172
+ field: Optional[str] = None,
173
+ expected: Optional[str] = None,
174
+ received: Optional[str] = None,
175
+ ) -> None:
176
+ details = {}
177
+ if field:
178
+ details["field"] = field
179
+ if expected:
180
+ details["expected"] = expected
181
+ if received:
182
+ details["received"] = received
183
+
184
+ super().__init__(
185
+ message=message,
186
+ code="INVALID_PAYLOAD",
187
+ details=details,
188
+ )
189
+ self.field = field
190
+ self.expected = expected
191
+ self.received = received
192
+
193
+
194
+ class ConfigurationError(X402Error):
195
+ """
196
+ Raised when SDK configuration is invalid or missing.
197
+ """
198
+
199
+ def __init__(self, message: str, config_key: Optional[str] = None) -> None:
200
+ super().__init__(
201
+ message=message,
202
+ code="CONFIGURATION_ERROR",
203
+ details={"configKey": config_key} if config_key else {},
204
+ )
205
+ self.config_key = config_key
206
+
207
+
208
+ class FacilitatorError(X402Error):
209
+ """
210
+ Raised when the facilitator returns an error.
211
+
212
+ Contains the raw error response from the facilitator for debugging.
213
+ """
214
+
215
+ def __init__(
216
+ self,
217
+ message: str,
218
+ status_code: Optional[int] = None,
219
+ response_body: Optional[str] = None,
220
+ ) -> None:
221
+ super().__init__(
222
+ message=message,
223
+ code="FACILITATOR_ERROR",
224
+ details={
225
+ "statusCode": status_code,
226
+ "response": response_body,
227
+ },
228
+ )
229
+ self.status_code = status_code
230
+ self.response_body = response_body
231
+
232
+
233
+ class TimeoutError(X402Error):
234
+ """
235
+ Raised when a facilitator request times out.
236
+
237
+ Settlement operations can take up to 55 seconds on congested networks.
238
+ """
239
+
240
+ def __init__(
241
+ self,
242
+ operation: str,
243
+ timeout_seconds: float,
244
+ ) -> None:
245
+ super().__init__(
246
+ message=f"{operation} timed out after {timeout_seconds}s",
247
+ code="TIMEOUT",
248
+ details={
249
+ "operation": operation,
250
+ "timeoutSeconds": timeout_seconds,
251
+ },
252
+ )
253
+ self.operation = operation
254
+ self.timeout_seconds = timeout_seconds
@@ -0,0 +1,74 @@
1
+ """
2
+ Framework integrations for the x402 SDK.
3
+
4
+ This module provides ready-to-use integrations for popular Python web frameworks:
5
+
6
+ - **Flask**: Decorator and middleware for Flask apps
7
+ - **FastAPI**: Dependency and middleware for FastAPI/Starlette apps
8
+ - **Django**: Middleware and decorator for Django apps
9
+ - **AWS Lambda**: Handler wrapper for Lambda functions
10
+
11
+ Each integration provides the same core functionality but adapts to the
12
+ idioms and patterns of its target framework.
13
+ """
14
+
15
+ # Framework availability flags
16
+ FLASK_AVAILABLE = False
17
+ FASTAPI_AVAILABLE = False
18
+ DJANGO_AVAILABLE = False
19
+
20
+ try:
21
+ from uvd_x402_sdk.integrations.flask_integration import (
22
+ FlaskX402,
23
+ flask_require_payment,
24
+ )
25
+ FLASK_AVAILABLE = True
26
+ except ImportError:
27
+ FlaskX402 = None # type: ignore
28
+ flask_require_payment = None # type: ignore
29
+
30
+ try:
31
+ from uvd_x402_sdk.integrations.fastapi_integration import (
32
+ FastAPIX402,
33
+ X402Depends,
34
+ fastapi_require_payment,
35
+ )
36
+ FASTAPI_AVAILABLE = True
37
+ except ImportError:
38
+ FastAPIX402 = None # type: ignore
39
+ X402Depends = None # type: ignore
40
+ fastapi_require_payment = None # type: ignore
41
+
42
+ try:
43
+ from uvd_x402_sdk.integrations.django_integration import (
44
+ DjangoX402Middleware,
45
+ django_require_payment,
46
+ )
47
+ DJANGO_AVAILABLE = True
48
+ except ImportError:
49
+ DjangoX402Middleware = None # type: ignore
50
+ django_require_payment = None # type: ignore
51
+
52
+ from uvd_x402_sdk.integrations.lambda_integration import (
53
+ LambdaX402,
54
+ lambda_handler,
55
+ )
56
+
57
+ __all__ = [
58
+ # Flask
59
+ "FlaskX402",
60
+ "flask_require_payment",
61
+ "FLASK_AVAILABLE",
62
+ # FastAPI
63
+ "FastAPIX402",
64
+ "X402Depends",
65
+ "fastapi_require_payment",
66
+ "FASTAPI_AVAILABLE",
67
+ # Django
68
+ "DjangoX402Middleware",
69
+ "django_require_payment",
70
+ "DJANGO_AVAILABLE",
71
+ # AWS Lambda
72
+ "LambdaX402",
73
+ "lambda_handler",
74
+ ]
@@ -0,0 +1,237 @@
1
+ """
2
+ Django integration for x402 payments.
3
+
4
+ Provides:
5
+ - DjangoX402Middleware: Middleware for protecting views
6
+ - django_require_payment: Decorator for view functions
7
+ """
8
+
9
+ from decimal import Decimal
10
+ from functools import wraps
11
+ from typing import Any, Callable, Dict, List, Optional, TypeVar, Union
12
+ import json
13
+
14
+ try:
15
+ from django.http import JsonResponse, HttpRequest, HttpResponse
16
+ from django.conf import settings
17
+ except ImportError:
18
+ raise ImportError(
19
+ "Django is required for Django integration. "
20
+ "Install with: pip install uvd-x402-sdk[django]"
21
+ )
22
+
23
+ from uvd_x402_sdk.client import X402Client
24
+ from uvd_x402_sdk.config import X402Config
25
+ from uvd_x402_sdk.exceptions import X402Error
26
+ from uvd_x402_sdk.models import PaymentResult
27
+ from uvd_x402_sdk.response import create_402_response, create_402_headers
28
+
29
+ F = TypeVar("F", bound=Callable[..., Any])
30
+
31
+
32
+ def _get_config_from_settings() -> X402Config:
33
+ """Get x402 configuration from Django settings."""
34
+ return X402Config(
35
+ facilitator_url=getattr(
36
+ settings,
37
+ "X402_FACILITATOR_URL",
38
+ "https://facilitator.ultravioletadao.xyz",
39
+ ),
40
+ recipient_evm=getattr(settings, "X402_RECIPIENT_EVM", ""),
41
+ recipient_solana=getattr(settings, "X402_RECIPIENT_SOLANA", ""),
42
+ recipient_near=getattr(settings, "X402_RECIPIENT_NEAR", ""),
43
+ recipient_stellar=getattr(settings, "X402_RECIPIENT_STELLAR", ""),
44
+ facilitator_solana=getattr(
45
+ settings,
46
+ "X402_FACILITATOR_SOLANA",
47
+ "F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq",
48
+ ),
49
+ )
50
+
51
+
52
+ class DjangoX402Middleware:
53
+ """
54
+ Django middleware for x402 payment protection.
55
+
56
+ Configure protected paths in Django settings:
57
+
58
+ # settings.py
59
+ X402_RECIPIENT_EVM = "0xYourWallet..."
60
+ X402_PROTECTED_PATHS = {
61
+ "/api/premium/": "5.00",
62
+ "/api/basic/": "1.00",
63
+ }
64
+
65
+ Example:
66
+ # settings.py
67
+ MIDDLEWARE = [
68
+ ...
69
+ 'uvd_x402_sdk.integrations.django_integration.DjangoX402Middleware',
70
+ ]
71
+ """
72
+
73
+ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
74
+ self.get_response = get_response
75
+ self._config = _get_config_from_settings()
76
+ self._client = X402Client(config=self._config)
77
+ self._protected_paths: Dict[str, Decimal] = {}
78
+
79
+ # Load protected paths from settings
80
+ paths_setting = getattr(settings, "X402_PROTECTED_PATHS", {})
81
+ for path, amount in paths_setting.items():
82
+ self._protected_paths[path] = Decimal(str(amount))
83
+
84
+ def __call__(self, request: HttpRequest) -> HttpResponse:
85
+ # Check if path is protected
86
+ path = request.path
87
+
88
+ # Try exact match first, then prefix match
89
+ required_amount = None
90
+ for protected_path, amount in self._protected_paths.items():
91
+ if path == protected_path or path.startswith(protected_path):
92
+ required_amount = amount
93
+ break
94
+
95
+ if required_amount is None:
96
+ return self.get_response(request)
97
+
98
+ # Get payment header (Django uses HTTP_X_PAYMENT format)
99
+ payment_header = request.META.get("HTTP_X_PAYMENT")
100
+
101
+ if not payment_header:
102
+ response_body = create_402_response(
103
+ amount_usd=required_amount,
104
+ config=self._config,
105
+ )
106
+ response = JsonResponse(response_body, status=402)
107
+ for key, value in create_402_headers().items():
108
+ response[key] = value
109
+ return response
110
+
111
+ try:
112
+ result = self._client.process_payment(
113
+ x_payment_header=payment_header,
114
+ expected_amount_usd=required_amount,
115
+ )
116
+ # Store result on request for view access
117
+ request.payment_result = result # type: ignore
118
+ return self.get_response(request)
119
+
120
+ except X402Error as e:
121
+ response = JsonResponse(e.to_dict(), status=402)
122
+ for key, value in create_402_headers().items():
123
+ response[key] = value
124
+ return response
125
+
126
+
127
+ def django_require_payment(
128
+ amount_usd: Union[Decimal, float, str],
129
+ config: Optional[X402Config] = None,
130
+ message: Optional[str] = None,
131
+ ) -> Callable[[F], F]:
132
+ """
133
+ Decorator that requires payment for a Django view.
134
+
135
+ Args:
136
+ amount_usd: Required payment amount in USD
137
+ config: X402Config (uses Django settings if not provided)
138
+ message: Custom message for 402 response
139
+
140
+ Example:
141
+ >>> from uvd_x402_sdk.integrations import django_require_payment
142
+ >>>
143
+ >>> @django_require_payment(amount_usd="1.00")
144
+ >>> def my_view(request):
145
+ ... # Access payment result via request.payment_result
146
+ ... return JsonResponse({"payer": request.payment_result.payer_address})
147
+ """
148
+ required_amount = Decimal(str(amount_usd))
149
+ _config = config or _get_config_from_settings()
150
+ _client = X402Client(config=_config)
151
+
152
+ def decorator(func: F) -> F:
153
+ @wraps(func)
154
+ def wrapper(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
155
+ # Get payment header
156
+ payment_header = request.META.get("HTTP_X_PAYMENT")
157
+
158
+ if not payment_header:
159
+ response_body = create_402_response(
160
+ amount_usd=required_amount,
161
+ config=_config,
162
+ message=message,
163
+ )
164
+ response = JsonResponse(response_body, status=402)
165
+ for key, value in create_402_headers().items():
166
+ response[key] = value
167
+ return response
168
+
169
+ try:
170
+ result = _client.process_payment(
171
+ x_payment_header=payment_header,
172
+ expected_amount_usd=required_amount,
173
+ )
174
+ # Store result on request
175
+ request.payment_result = result # type: ignore
176
+ return func(request, *args, **kwargs)
177
+
178
+ except X402Error as e:
179
+ response = JsonResponse(e.to_dict(), status=402)
180
+ for key, value in create_402_headers().items():
181
+ response[key] = value
182
+ return response
183
+
184
+ return wrapper # type: ignore
185
+
186
+ return decorator
187
+
188
+
189
+ class X402PaymentView:
190
+ """
191
+ Mixin for Django class-based views requiring payment.
192
+
193
+ Example:
194
+ >>> from django.views import View
195
+ >>> from uvd_x402_sdk.integrations.django_integration import X402PaymentView
196
+ >>>
197
+ >>> class PremiumAPIView(X402PaymentView, View):
198
+ ... x402_amount = Decimal("5.00")
199
+ ...
200
+ ... def get(self, request):
201
+ ... return JsonResponse({"payer": request.payment_result.payer_address})
202
+ """
203
+
204
+ x402_amount: Decimal = Decimal("1.00")
205
+ x402_message: Optional[str] = None
206
+ x402_config: Optional[X402Config] = None
207
+
208
+ def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
209
+ config = self.x402_config or _get_config_from_settings()
210
+ client = X402Client(config=config)
211
+
212
+ payment_header = request.META.get("HTTP_X_PAYMENT")
213
+
214
+ if not payment_header:
215
+ response_body = create_402_response(
216
+ amount_usd=self.x402_amount,
217
+ config=config,
218
+ message=self.x402_message,
219
+ )
220
+ response = JsonResponse(response_body, status=402)
221
+ for key, value in create_402_headers().items():
222
+ response[key] = value
223
+ return response
224
+
225
+ try:
226
+ result = client.process_payment(
227
+ x_payment_header=payment_header,
228
+ expected_amount_usd=self.x402_amount,
229
+ )
230
+ request.payment_result = result # type: ignore
231
+ return super().dispatch(request, *args, **kwargs) # type: ignore
232
+
233
+ except X402Error as e:
234
+ response = JsonResponse(e.to_dict(), status=402)
235
+ for key, value in create_402_headers().items():
236
+ response[key] = value
237
+ return response