t402 1.9.1__py3-none-any.whl → 1.10.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.
- t402/__init__.py +1 -1
- t402/a2a/__init__.py +73 -0
- t402/a2a/helpers.py +158 -0
- t402/a2a/types.py +145 -0
- t402/bridge/constants.py +1 -1
- t402/django/__init__.py +42 -0
- t402/django/middleware.py +596 -0
- t402/errors.py +213 -0
- t402/facilitator.py +125 -0
- t402/mcp/constants.py +3 -6
- t402/mcp/server.py +428 -44
- t402/mcp/web3_utils.py +493 -0
- t402/multisig/__init__.py +120 -0
- t402/multisig/constants.py +54 -0
- t402/multisig/safe.py +441 -0
- t402/multisig/signature.py +228 -0
- t402/multisig/transaction.py +238 -0
- t402/multisig/types.py +108 -0
- t402/multisig/utils.py +77 -0
- t402/schemes/__init__.py +19 -0
- t402/schemes/cosmos/__init__.py +114 -0
- t402/schemes/cosmos/constants.py +211 -0
- t402/schemes/cosmos/exact_direct/__init__.py +21 -0
- t402/schemes/cosmos/exact_direct/client.py +198 -0
- t402/schemes/cosmos/exact_direct/facilitator.py +493 -0
- t402/schemes/cosmos/exact_direct/server.py +315 -0
- t402/schemes/cosmos/types.py +501 -0
- t402/schemes/evm/__init__.py +1 -1
- t402/schemes/evm/exact_legacy/server.py +1 -1
- t402/schemes/near/__init__.py +25 -0
- t402/schemes/near/upto/__init__.py +54 -0
- t402/schemes/near/upto/types.py +272 -0
- t402/schemes/svm/__init__.py +15 -0
- t402/schemes/svm/upto/__init__.py +23 -0
- t402/schemes/svm/upto/types.py +193 -0
- t402/schemes/ton/__init__.py +15 -0
- t402/schemes/ton/upto/__init__.py +31 -0
- t402/schemes/ton/upto/types.py +215 -0
- t402/schemes/tron/__init__.py +21 -4
- t402/schemes/tron/upto/__init__.py +30 -0
- t402/schemes/tron/upto/types.py +213 -0
- t402/starlette/__init__.py +38 -0
- t402/starlette/middleware.py +522 -0
- t402/ton.py +1 -1
- t402/ton_paywall_template.py +1 -1
- t402/types.py +100 -2
- t402/wdk/chains.py +1 -1
- {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/METADATA +3 -3
- {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/RECORD +51 -20
- {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/WHEEL +0 -0
- {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"""Django Middleware for T402 Payment Protocol.
|
|
2
|
+
|
|
3
|
+
This module provides middleware for integrating T402 payments
|
|
4
|
+
with Django applications, supporting both V1 and V2 protocols.
|
|
5
|
+
|
|
6
|
+
V1 Protocol:
|
|
7
|
+
- X-PAYMENT header for payment signature
|
|
8
|
+
- X-PAYMENT-RESPONSE header for settlement
|
|
9
|
+
- Response body contains PaymentRequired
|
|
10
|
+
|
|
11
|
+
V2 Protocol:
|
|
12
|
+
- PAYMENT-SIGNATURE header for payment signature
|
|
13
|
+
- PAYMENT-REQUIRED header for 402 responses
|
|
14
|
+
- PAYMENT-RESPONSE header for settlement
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
```python
|
|
18
|
+
# settings.py
|
|
19
|
+
MIDDLEWARE = [
|
|
20
|
+
...
|
|
21
|
+
"t402.django.PaymentMiddleware",
|
|
22
|
+
...
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
T402_PAYMENT_CONFIGS = [
|
|
26
|
+
{
|
|
27
|
+
"path": "/api/premium/*",
|
|
28
|
+
"price": "$0.10",
|
|
29
|
+
"pay_to_address": "0x1234...",
|
|
30
|
+
"network": "eip155:8453",
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Or configure programmatically:
|
|
35
|
+
from t402.django import PaymentMiddleware
|
|
36
|
+
PaymentMiddleware.configure([
|
|
37
|
+
PaymentConfig(
|
|
38
|
+
price="$0.10",
|
|
39
|
+
pay_to_address="0x...",
|
|
40
|
+
path="/api/*",
|
|
41
|
+
network="eip155:8453",
|
|
42
|
+
),
|
|
43
|
+
])
|
|
44
|
+
```
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import asyncio
|
|
50
|
+
import logging
|
|
51
|
+
from typing import Any, Callable, Dict, List, Optional, Union, cast
|
|
52
|
+
|
|
53
|
+
from django.http import HttpRequest, HttpResponse, JsonResponse
|
|
54
|
+
|
|
55
|
+
from t402.common import (
|
|
56
|
+
process_price_to_atomic_amount,
|
|
57
|
+
find_matching_payment_requirements,
|
|
58
|
+
)
|
|
59
|
+
from t402.encoding import (
|
|
60
|
+
encode_payment_required_header,
|
|
61
|
+
encode_payment_response_header,
|
|
62
|
+
extract_payment_from_headers,
|
|
63
|
+
decode_payment_signature_header,
|
|
64
|
+
HEADER_PAYMENT_REQUIRED,
|
|
65
|
+
HEADER_PAYMENT_RESPONSE,
|
|
66
|
+
HEADER_X_PAYMENT_RESPONSE,
|
|
67
|
+
)
|
|
68
|
+
from t402.facilitator import FacilitatorClient, FacilitatorConfig
|
|
69
|
+
from t402.networks import get_all_supported_networks, SupportedNetworks
|
|
70
|
+
from t402.path import path_is_match
|
|
71
|
+
from t402.paywall import is_browser_request, get_paywall_html
|
|
72
|
+
from t402.types import (
|
|
73
|
+
PaymentPayload,
|
|
74
|
+
PaymentRequirements,
|
|
75
|
+
PaymentRequirementsV2,
|
|
76
|
+
PaymentRequiredV2,
|
|
77
|
+
ResourceInfo,
|
|
78
|
+
Price,
|
|
79
|
+
t402PaymentRequiredResponse,
|
|
80
|
+
PaywallConfig,
|
|
81
|
+
HTTPInputSchema,
|
|
82
|
+
T402_VERSION_V1,
|
|
83
|
+
T402_VERSION_V2,
|
|
84
|
+
VerifyResponse,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
logger = logging.getLogger(__name__)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PaymentDetails:
|
|
91
|
+
"""Payment details stored in request after verification."""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
requirements: Union[PaymentRequirements, PaymentRequirementsV2],
|
|
96
|
+
verify_response: VerifyResponse,
|
|
97
|
+
protocol_version: int,
|
|
98
|
+
):
|
|
99
|
+
self.requirements = requirements
|
|
100
|
+
self.verify_response = verify_response
|
|
101
|
+
self.protocol_version = protocol_version
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def is_verified(self) -> bool:
|
|
105
|
+
"""Check if payment was verified."""
|
|
106
|
+
return self.verify_response.is_valid
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def payer_address(self) -> Optional[str]:
|
|
110
|
+
"""Get payer address from verify response."""
|
|
111
|
+
return getattr(self.verify_response, "payer", None)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class PaymentConfig:
|
|
115
|
+
"""Configuration for a payment-protected route."""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
price: Price,
|
|
120
|
+
pay_to_address: str,
|
|
121
|
+
path: Union[str, List[str]] = "*",
|
|
122
|
+
description: str = "",
|
|
123
|
+
mime_type: str = "",
|
|
124
|
+
max_timeout_seconds: int = 60,
|
|
125
|
+
input_schema: Optional[HTTPInputSchema] = None,
|
|
126
|
+
output_schema: Optional[Any] = None,
|
|
127
|
+
discoverable: bool = True,
|
|
128
|
+
facilitator_config: Optional[FacilitatorConfig] = None,
|
|
129
|
+
network: str = "eip155:8453",
|
|
130
|
+
resource: Optional[str] = None,
|
|
131
|
+
paywall_config: Optional[PaywallConfig] = None,
|
|
132
|
+
custom_paywall_html: Optional[str] = None,
|
|
133
|
+
protocol_version: int = T402_VERSION_V2,
|
|
134
|
+
):
|
|
135
|
+
self.price = price
|
|
136
|
+
self.pay_to_address = pay_to_address
|
|
137
|
+
self.path = path
|
|
138
|
+
self.description = description
|
|
139
|
+
self.mime_type = mime_type
|
|
140
|
+
self.max_timeout_seconds = max_timeout_seconds
|
|
141
|
+
self.input_schema = input_schema
|
|
142
|
+
self.output_schema = output_schema
|
|
143
|
+
self.discoverable = discoverable
|
|
144
|
+
self.facilitator_config = facilitator_config
|
|
145
|
+
self.network = network
|
|
146
|
+
self.resource = resource
|
|
147
|
+
self.paywall_config = paywall_config
|
|
148
|
+
self.custom_paywall_html = custom_paywall_html
|
|
149
|
+
self.protocol_version = protocol_version
|
|
150
|
+
|
|
151
|
+
# Validate and process price
|
|
152
|
+
self._validate()
|
|
153
|
+
|
|
154
|
+
def _validate(self):
|
|
155
|
+
"""Validate configuration."""
|
|
156
|
+
supported_networks = get_all_supported_networks()
|
|
157
|
+
if self.network not in supported_networks:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"Unsupported network: {self.network}. Must be one of: {supported_networks}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
self.max_amount_required, self.asset_address, self.eip712_domain = (
|
|
164
|
+
process_price_to_atomic_amount(self.price, self.network)
|
|
165
|
+
)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
raise ValueError(f"Invalid price: {self.price}. Error: {e}")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class PaymentMiddleware:
|
|
171
|
+
"""Django middleware for T402 payment requirements.
|
|
172
|
+
|
|
173
|
+
This class provides Django-compatible middleware for protecting routes
|
|
174
|
+
with T402 payment requirements. It supports multiple configurations
|
|
175
|
+
with different path patterns and settings.
|
|
176
|
+
|
|
177
|
+
Configuration via Django settings:
|
|
178
|
+
```python
|
|
179
|
+
# settings.py
|
|
180
|
+
MIDDLEWARE = [
|
|
181
|
+
...
|
|
182
|
+
"t402.django.PaymentMiddleware",
|
|
183
|
+
...
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
T402_PAYMENT_CONFIGS = [
|
|
187
|
+
{
|
|
188
|
+
"path": "/api/premium/*",
|
|
189
|
+
"price": "$0.10",
|
|
190
|
+
"pay_to_address": "0x1234...",
|
|
191
|
+
"network": "eip155:8453",
|
|
192
|
+
},
|
|
193
|
+
]
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Programmatic configuration:
|
|
197
|
+
```python
|
|
198
|
+
PaymentMiddleware.configure([
|
|
199
|
+
PaymentConfig(price="$0.10", pay_to_address="0x...", path="/api/*"),
|
|
200
|
+
])
|
|
201
|
+
```
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
# Class-level storage for payment configs
|
|
205
|
+
_configs: List[PaymentConfig] = []
|
|
206
|
+
|
|
207
|
+
def __init__(self, get_response: Callable):
|
|
208
|
+
"""Initialize the Django middleware.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
get_response: The next middleware or view callable in the chain
|
|
212
|
+
"""
|
|
213
|
+
self.get_response = get_response
|
|
214
|
+
|
|
215
|
+
# Load configs from Django settings if no programmatic configs
|
|
216
|
+
if not PaymentMiddleware._configs:
|
|
217
|
+
self._load_from_settings()
|
|
218
|
+
|
|
219
|
+
def _load_from_settings(self):
|
|
220
|
+
"""Load payment configs from Django settings."""
|
|
221
|
+
try:
|
|
222
|
+
from django.conf import settings
|
|
223
|
+
|
|
224
|
+
configs_data = getattr(settings, "T402_PAYMENT_CONFIGS", [])
|
|
225
|
+
for config_dict in configs_data:
|
|
226
|
+
config = PaymentConfig(**config_dict)
|
|
227
|
+
PaymentMiddleware._configs.append(config)
|
|
228
|
+
except Exception:
|
|
229
|
+
# Settings may not be configured in all environments
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def configure(cls, configs: List[PaymentConfig]) -> None:
|
|
234
|
+
"""Configure payment middleware with a list of PaymentConfig objects.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
configs: List of PaymentConfig objects
|
|
238
|
+
"""
|
|
239
|
+
cls._configs = list(configs)
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def add(
|
|
243
|
+
cls,
|
|
244
|
+
price: Price,
|
|
245
|
+
pay_to_address: str,
|
|
246
|
+
path: Union[str, List[str]] = "*",
|
|
247
|
+
description: str = "",
|
|
248
|
+
mime_type: str = "",
|
|
249
|
+
max_timeout_seconds: int = 60,
|
|
250
|
+
input_schema: Optional[HTTPInputSchema] = None,
|
|
251
|
+
output_schema: Optional[Any] = None,
|
|
252
|
+
discoverable: bool = True,
|
|
253
|
+
facilitator_config: Optional[FacilitatorConfig] = None,
|
|
254
|
+
network: str = "eip155:8453",
|
|
255
|
+
resource: Optional[str] = None,
|
|
256
|
+
paywall_config: Optional[PaywallConfig] = None,
|
|
257
|
+
custom_paywall_html: Optional[str] = None,
|
|
258
|
+
protocol_version: int = T402_VERSION_V2,
|
|
259
|
+
) -> type:
|
|
260
|
+
"""Add a payment requirement configuration.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
price: Payment price (USD string or TokenAmount dict)
|
|
264
|
+
pay_to_address: Address to receive payment
|
|
265
|
+
path: Path pattern(s) to protect
|
|
266
|
+
description: Resource description
|
|
267
|
+
mime_type: Resource MIME type
|
|
268
|
+
max_timeout_seconds: Maximum payment timeout
|
|
269
|
+
input_schema: HTTP input schema
|
|
270
|
+
output_schema: Response schema
|
|
271
|
+
discoverable: Whether route is discoverable
|
|
272
|
+
facilitator_config: Facilitator configuration
|
|
273
|
+
network: Network identifier (CAIP-2 format)
|
|
274
|
+
resource: Explicit resource URL
|
|
275
|
+
paywall_config: Paywall UI configuration
|
|
276
|
+
custom_paywall_html: Custom paywall HTML
|
|
277
|
+
protocol_version: T402 protocol version (1 or 2)
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The class for chaining
|
|
281
|
+
"""
|
|
282
|
+
config = PaymentConfig(
|
|
283
|
+
price=price,
|
|
284
|
+
pay_to_address=pay_to_address,
|
|
285
|
+
path=path,
|
|
286
|
+
description=description,
|
|
287
|
+
mime_type=mime_type,
|
|
288
|
+
max_timeout_seconds=max_timeout_seconds,
|
|
289
|
+
input_schema=input_schema,
|
|
290
|
+
output_schema=output_schema,
|
|
291
|
+
discoverable=discoverable,
|
|
292
|
+
facilitator_config=facilitator_config,
|
|
293
|
+
network=network,
|
|
294
|
+
resource=resource,
|
|
295
|
+
paywall_config=paywall_config,
|
|
296
|
+
custom_paywall_html=custom_paywall_html,
|
|
297
|
+
protocol_version=protocol_version,
|
|
298
|
+
)
|
|
299
|
+
cls._configs.append(config)
|
|
300
|
+
return cls
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def reset(cls) -> None:
|
|
304
|
+
"""Reset all payment configs. Useful for testing."""
|
|
305
|
+
cls._configs = []
|
|
306
|
+
|
|
307
|
+
def __call__(self, request: HttpRequest) -> HttpResponse:
|
|
308
|
+
"""Process the request through payment middleware.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
request: Incoming Django HttpRequest
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
HttpResponse object
|
|
315
|
+
"""
|
|
316
|
+
# Find matching config
|
|
317
|
+
config = self._find_matching_config(request.path)
|
|
318
|
+
if not config:
|
|
319
|
+
return self.get_response(request)
|
|
320
|
+
|
|
321
|
+
# Create facilitator client
|
|
322
|
+
facilitator = FacilitatorClient(config.facilitator_config)
|
|
323
|
+
|
|
324
|
+
# Get resource URL
|
|
325
|
+
resource_url = config.resource or request.build_absolute_uri()
|
|
326
|
+
|
|
327
|
+
# Build request headers dict for protocol detection
|
|
328
|
+
request_headers = self._get_headers_dict(request)
|
|
329
|
+
|
|
330
|
+
# Build payment requirements
|
|
331
|
+
requirements = self._build_requirements(config, request, resource_url)
|
|
332
|
+
|
|
333
|
+
# Create 402 response helper
|
|
334
|
+
def create_402_response(error: str) -> HttpResponse:
|
|
335
|
+
return self._create_402_response(
|
|
336
|
+
error=error,
|
|
337
|
+
requirements=[requirements],
|
|
338
|
+
request_headers=request_headers,
|
|
339
|
+
protocol_version=config.protocol_version,
|
|
340
|
+
paywall_config=config.paywall_config,
|
|
341
|
+
custom_paywall_html=config.custom_paywall_html,
|
|
342
|
+
resource_url=resource_url,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Extract payment from headers
|
|
346
|
+
version, payment_header = extract_payment_from_headers(request_headers)
|
|
347
|
+
|
|
348
|
+
if not payment_header:
|
|
349
|
+
return create_402_response("No payment header provided")
|
|
350
|
+
|
|
351
|
+
# Decode payment
|
|
352
|
+
try:
|
|
353
|
+
payment_dict = decode_payment_signature_header(payment_header)
|
|
354
|
+
payment = PaymentPayload(**payment_dict)
|
|
355
|
+
except Exception as e:
|
|
356
|
+
client_ip = request.META.get("REMOTE_ADDR", "unknown")
|
|
357
|
+
logger.warning(f"Invalid payment header from {client_ip}: {e}")
|
|
358
|
+
return create_402_response("Invalid payment header format")
|
|
359
|
+
|
|
360
|
+
# Find matching requirements
|
|
361
|
+
selected_requirements = find_matching_payment_requirements(
|
|
362
|
+
[requirements], payment
|
|
363
|
+
)
|
|
364
|
+
if not selected_requirements:
|
|
365
|
+
return create_402_response("No matching payment requirements found")
|
|
366
|
+
|
|
367
|
+
# Verify payment (async in sync context)
|
|
368
|
+
try:
|
|
369
|
+
loop = asyncio.new_event_loop()
|
|
370
|
+
asyncio.set_event_loop(loop)
|
|
371
|
+
try:
|
|
372
|
+
verify_response = loop.run_until_complete(
|
|
373
|
+
facilitator.verify(payment, selected_requirements)
|
|
374
|
+
)
|
|
375
|
+
finally:
|
|
376
|
+
loop.close()
|
|
377
|
+
except Exception as e:
|
|
378
|
+
logger.error(f"Payment verification failed: {e}")
|
|
379
|
+
return create_402_response(f"Payment verification failed: {e}")
|
|
380
|
+
|
|
381
|
+
if not verify_response.is_valid:
|
|
382
|
+
error_reason = verify_response.invalid_reason or "Unknown error"
|
|
383
|
+
return create_402_response(f"Invalid payment: {error_reason}")
|
|
384
|
+
|
|
385
|
+
# Store payment details on the request
|
|
386
|
+
request.payment_details = PaymentDetails(
|
|
387
|
+
requirements=selected_requirements,
|
|
388
|
+
verify_response=verify_response,
|
|
389
|
+
protocol_version=version,
|
|
390
|
+
)
|
|
391
|
+
request.verify_response = verify_response
|
|
392
|
+
|
|
393
|
+
# Process request through the rest of the middleware chain / view
|
|
394
|
+
response = self.get_response(request)
|
|
395
|
+
|
|
396
|
+
# Skip settlement for non-2xx responses
|
|
397
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
398
|
+
return response
|
|
399
|
+
|
|
400
|
+
# Settle payment
|
|
401
|
+
try:
|
|
402
|
+
loop = asyncio.new_event_loop()
|
|
403
|
+
asyncio.set_event_loop(loop)
|
|
404
|
+
try:
|
|
405
|
+
settle_response = loop.run_until_complete(
|
|
406
|
+
facilitator.settle(payment, selected_requirements)
|
|
407
|
+
)
|
|
408
|
+
finally:
|
|
409
|
+
loop.close()
|
|
410
|
+
|
|
411
|
+
if settle_response.success:
|
|
412
|
+
# Add settlement header based on version
|
|
413
|
+
header_name = (
|
|
414
|
+
HEADER_PAYMENT_RESPONSE
|
|
415
|
+
if version == T402_VERSION_V2
|
|
416
|
+
else HEADER_X_PAYMENT_RESPONSE
|
|
417
|
+
)
|
|
418
|
+
header_value = encode_payment_response_header(settle_response)
|
|
419
|
+
response[header_name] = header_value
|
|
420
|
+
else:
|
|
421
|
+
return create_402_response(
|
|
422
|
+
f"Settlement failed: {settle_response.error_reason or 'Unknown error'}"
|
|
423
|
+
)
|
|
424
|
+
except Exception as e:
|
|
425
|
+
logger.error(f"Settlement failed: {e}")
|
|
426
|
+
return create_402_response(f"Settlement failed: {e}")
|
|
427
|
+
|
|
428
|
+
return response
|
|
429
|
+
|
|
430
|
+
def _find_matching_config(self, path: str) -> Optional[PaymentConfig]:
|
|
431
|
+
"""Find a matching payment config for the given path.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
path: Request path
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Matching PaymentConfig or None
|
|
438
|
+
"""
|
|
439
|
+
for config in PaymentMiddleware._configs:
|
|
440
|
+
if path_is_match(config.path, path):
|
|
441
|
+
return config
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
def _get_headers_dict(self, request: HttpRequest) -> Dict[str, str]:
|
|
445
|
+
"""Extract headers from Django request into a flat dict.
|
|
446
|
+
|
|
447
|
+
Django stores headers in META with HTTP_ prefix and uppercase names.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
request: Django HttpRequest
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Dictionary of header name to value
|
|
454
|
+
"""
|
|
455
|
+
headers = {}
|
|
456
|
+
for key, value in request.META.items():
|
|
457
|
+
if key.startswith("HTTP_"):
|
|
458
|
+
# Convert HTTP_PAYMENT_SIGNATURE to payment-signature
|
|
459
|
+
header_name = key[5:].replace("_", "-").lower()
|
|
460
|
+
headers[header_name] = value
|
|
461
|
+
elif key == "CONTENT_TYPE":
|
|
462
|
+
headers["content-type"] = value
|
|
463
|
+
elif key == "CONTENT_LENGTH":
|
|
464
|
+
headers["content-length"] = value
|
|
465
|
+
return headers
|
|
466
|
+
|
|
467
|
+
def _build_requirements(
|
|
468
|
+
self,
|
|
469
|
+
config: PaymentConfig,
|
|
470
|
+
request: HttpRequest,
|
|
471
|
+
resource_url: str,
|
|
472
|
+
) -> PaymentRequirements:
|
|
473
|
+
"""Build payment requirements from config.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
config: Payment configuration
|
|
477
|
+
request: Incoming request
|
|
478
|
+
resource_url: Resource URL
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
PaymentRequirements object
|
|
482
|
+
"""
|
|
483
|
+
return PaymentRequirements(
|
|
484
|
+
scheme="exact",
|
|
485
|
+
network=cast(SupportedNetworks, config.network),
|
|
486
|
+
asset=config.asset_address,
|
|
487
|
+
max_amount_required=config.max_amount_required,
|
|
488
|
+
resource=resource_url,
|
|
489
|
+
description=config.description,
|
|
490
|
+
mime_type=config.mime_type,
|
|
491
|
+
pay_to=config.pay_to_address,
|
|
492
|
+
max_timeout_seconds=config.max_timeout_seconds,
|
|
493
|
+
output_schema={
|
|
494
|
+
"input": {
|
|
495
|
+
"type": "http",
|
|
496
|
+
"method": request.method.upper(),
|
|
497
|
+
"discoverable": config.discoverable,
|
|
498
|
+
**(config.input_schema.model_dump() if config.input_schema else {}),
|
|
499
|
+
},
|
|
500
|
+
"output": config.output_schema,
|
|
501
|
+
},
|
|
502
|
+
extra=config.eip712_domain,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
def _create_402_response(
|
|
506
|
+
self,
|
|
507
|
+
error: str,
|
|
508
|
+
requirements: List[PaymentRequirements],
|
|
509
|
+
request_headers: Dict[str, str],
|
|
510
|
+
protocol_version: int,
|
|
511
|
+
paywall_config: Optional[PaywallConfig],
|
|
512
|
+
custom_paywall_html: Optional[str],
|
|
513
|
+
resource_url: str,
|
|
514
|
+
) -> HttpResponse:
|
|
515
|
+
"""Create a 402 Payment Required response.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
error: Error message
|
|
519
|
+
requirements: Payment requirements
|
|
520
|
+
request_headers: Request headers
|
|
521
|
+
protocol_version: Protocol version
|
|
522
|
+
paywall_config: Paywall configuration
|
|
523
|
+
custom_paywall_html: Custom HTML
|
|
524
|
+
resource_url: Resource URL
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
402 HttpResponse
|
|
528
|
+
"""
|
|
529
|
+
status_code = 402
|
|
530
|
+
|
|
531
|
+
# Browser request - return HTML paywall
|
|
532
|
+
if is_browser_request(request_headers):
|
|
533
|
+
html_content = custom_paywall_html or get_paywall_html(
|
|
534
|
+
error, requirements, paywall_config
|
|
535
|
+
)
|
|
536
|
+
response = HttpResponse(
|
|
537
|
+
content=html_content,
|
|
538
|
+
status=status_code,
|
|
539
|
+
content_type="text/html; charset=utf-8",
|
|
540
|
+
)
|
|
541
|
+
return response
|
|
542
|
+
|
|
543
|
+
# API request - return JSON with appropriate headers
|
|
544
|
+
if protocol_version == T402_VERSION_V2:
|
|
545
|
+
# V2: Use PAYMENT-REQUIRED header
|
|
546
|
+
resource_info = ResourceInfo(
|
|
547
|
+
url=resource_url,
|
|
548
|
+
description=requirements[0].description if requirements else "",
|
|
549
|
+
mime_type=requirements[0].mime_type if requirements else "",
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# Convert V1 requirements to V2 format
|
|
553
|
+
accepts_v2 = []
|
|
554
|
+
for req in requirements:
|
|
555
|
+
accepts_v2.append(
|
|
556
|
+
PaymentRequirementsV2(
|
|
557
|
+
scheme=req.scheme,
|
|
558
|
+
network=req.network,
|
|
559
|
+
asset=req.asset,
|
|
560
|
+
amount=req.max_amount_required,
|
|
561
|
+
pay_to=req.pay_to,
|
|
562
|
+
max_timeout_seconds=req.max_timeout_seconds,
|
|
563
|
+
extra=req.extra or {},
|
|
564
|
+
)
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
payment_required = PaymentRequiredV2(
|
|
568
|
+
t402_version=T402_VERSION_V2,
|
|
569
|
+
resource=resource_info,
|
|
570
|
+
accepts=accepts_v2,
|
|
571
|
+
error=error,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
header_value = encode_payment_required_header(payment_required)
|
|
575
|
+
|
|
576
|
+
response = JsonResponse(
|
|
577
|
+
data=payment_required.model_dump(by_alias=True),
|
|
578
|
+
status=status_code,
|
|
579
|
+
)
|
|
580
|
+
response["Content-Type"] = "application/json"
|
|
581
|
+
response[HEADER_PAYMENT_REQUIRED] = header_value
|
|
582
|
+
return response
|
|
583
|
+
else:
|
|
584
|
+
# V1: Return body only
|
|
585
|
+
response_data = t402PaymentRequiredResponse(
|
|
586
|
+
t402_version=T402_VERSION_V1,
|
|
587
|
+
accepts=requirements,
|
|
588
|
+
error=error,
|
|
589
|
+
).model_dump(by_alias=True)
|
|
590
|
+
|
|
591
|
+
response = JsonResponse(
|
|
592
|
+
data=response_data,
|
|
593
|
+
status=status_code,
|
|
594
|
+
)
|
|
595
|
+
response["Content-Type"] = "application/json"
|
|
596
|
+
return response
|