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.
- uvd_x402_sdk/__init__.py +169 -0
- uvd_x402_sdk/client.py +527 -0
- uvd_x402_sdk/config.py +249 -0
- uvd_x402_sdk/decorators.py +325 -0
- uvd_x402_sdk/exceptions.py +254 -0
- uvd_x402_sdk/integrations/__init__.py +74 -0
- uvd_x402_sdk/integrations/django_integration.py +237 -0
- uvd_x402_sdk/integrations/fastapi_integration.py +330 -0
- uvd_x402_sdk/integrations/flask_integration.py +259 -0
- uvd_x402_sdk/integrations/lambda_integration.py +320 -0
- uvd_x402_sdk/models.py +397 -0
- uvd_x402_sdk/networks/__init__.py +54 -0
- uvd_x402_sdk/networks/base.py +348 -0
- uvd_x402_sdk/networks/evm.py +235 -0
- uvd_x402_sdk/networks/near.py +397 -0
- uvd_x402_sdk/networks/solana.py +269 -0
- uvd_x402_sdk/networks/stellar.py +129 -0
- uvd_x402_sdk/response.py +439 -0
- uvd_x402_sdk-0.2.0.dist-info/LICENSE +21 -0
- uvd_x402_sdk-0.2.0.dist-info/METADATA +776 -0
- uvd_x402_sdk-0.2.0.dist-info/RECORD +23 -0
- uvd_x402_sdk-0.2.0.dist-info/WHEEL +5 -0
- uvd_x402_sdk-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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
|