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,522 @@
|
|
|
1
|
+
"""Starlette Middleware for T402 Payment Protocol.
|
|
2
|
+
|
|
3
|
+
This module provides ASGI middleware for integrating T402 payments
|
|
4
|
+
with Starlette 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
|
+
from starlette.applications import Starlette
|
|
19
|
+
from starlette.routing import Route
|
|
20
|
+
from t402.starlette import PaymentMiddleware
|
|
21
|
+
|
|
22
|
+
app = Starlette(routes=[
|
|
23
|
+
Route("/api/data", endpoint=my_handler),
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
payment = PaymentMiddleware(app)
|
|
27
|
+
payment.add(
|
|
28
|
+
path="/api/*",
|
|
29
|
+
price="$0.10",
|
|
30
|
+
pay_to_address="0x...",
|
|
31
|
+
network="eip155:8453",
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import logging
|
|
39
|
+
from typing import Any, Callable, Dict, List, Optional, Union, cast
|
|
40
|
+
|
|
41
|
+
from starlette.applications import Starlette
|
|
42
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
43
|
+
from starlette.requests import Request
|
|
44
|
+
from starlette.responses import HTMLResponse, JSONResponse, Response
|
|
45
|
+
|
|
46
|
+
from t402.common import (
|
|
47
|
+
process_price_to_atomic_amount,
|
|
48
|
+
find_matching_payment_requirements,
|
|
49
|
+
)
|
|
50
|
+
from t402.encoding import (
|
|
51
|
+
encode_payment_required_header,
|
|
52
|
+
encode_payment_response_header,
|
|
53
|
+
detect_protocol_version_from_headers,
|
|
54
|
+
extract_payment_from_headers,
|
|
55
|
+
decode_payment_signature_header,
|
|
56
|
+
HEADER_PAYMENT_REQUIRED,
|
|
57
|
+
HEADER_PAYMENT_RESPONSE,
|
|
58
|
+
HEADER_X_PAYMENT_RESPONSE,
|
|
59
|
+
)
|
|
60
|
+
from t402.facilitator import FacilitatorClient, FacilitatorConfig
|
|
61
|
+
from t402.networks import get_all_supported_networks, SupportedNetworks
|
|
62
|
+
from t402.path import path_is_match
|
|
63
|
+
from t402.paywall import is_browser_request, get_paywall_html
|
|
64
|
+
from t402.types import (
|
|
65
|
+
PaymentPayload,
|
|
66
|
+
PaymentRequirements,
|
|
67
|
+
PaymentRequirementsV2,
|
|
68
|
+
PaymentRequiredV2,
|
|
69
|
+
ResourceInfo,
|
|
70
|
+
Price,
|
|
71
|
+
t402PaymentRequiredResponse,
|
|
72
|
+
PaywallConfig,
|
|
73
|
+
HTTPInputSchema,
|
|
74
|
+
T402_VERSION_V1,
|
|
75
|
+
T402_VERSION_V2,
|
|
76
|
+
VerifyResponse,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
logger = logging.getLogger(__name__)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PaymentDetails:
|
|
83
|
+
"""Payment details stored in request state after verification."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
requirements: Union[PaymentRequirements, PaymentRequirementsV2],
|
|
88
|
+
verify_response: VerifyResponse,
|
|
89
|
+
protocol_version: int,
|
|
90
|
+
):
|
|
91
|
+
self.requirements = requirements
|
|
92
|
+
self.verify_response = verify_response
|
|
93
|
+
self.protocol_version = protocol_version
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def is_verified(self) -> bool:
|
|
97
|
+
"""Check if payment was verified."""
|
|
98
|
+
return self.verify_response.is_valid
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def payer_address(self) -> Optional[str]:
|
|
102
|
+
"""Get payer address from verify response."""
|
|
103
|
+
return getattr(self.verify_response, "payer", None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PaymentConfig:
|
|
107
|
+
"""Configuration for a payment-protected route."""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
price: Price,
|
|
112
|
+
pay_to_address: str,
|
|
113
|
+
path: Union[str, List[str]] = "*",
|
|
114
|
+
description: str = "",
|
|
115
|
+
mime_type: str = "",
|
|
116
|
+
max_timeout_seconds: int = 60,
|
|
117
|
+
input_schema: Optional[HTTPInputSchema] = None,
|
|
118
|
+
output_schema: Optional[Any] = None,
|
|
119
|
+
discoverable: bool = True,
|
|
120
|
+
facilitator_config: Optional[FacilitatorConfig] = None,
|
|
121
|
+
network: str = "eip155:8453",
|
|
122
|
+
resource: Optional[str] = None,
|
|
123
|
+
paywall_config: Optional[PaywallConfig] = None,
|
|
124
|
+
custom_paywall_html: Optional[str] = None,
|
|
125
|
+
protocol_version: int = T402_VERSION_V2,
|
|
126
|
+
):
|
|
127
|
+
self.price = price
|
|
128
|
+
self.pay_to_address = pay_to_address
|
|
129
|
+
self.path = path
|
|
130
|
+
self.description = description
|
|
131
|
+
self.mime_type = mime_type
|
|
132
|
+
self.max_timeout_seconds = max_timeout_seconds
|
|
133
|
+
self.input_schema = input_schema
|
|
134
|
+
self.output_schema = output_schema
|
|
135
|
+
self.discoverable = discoverable
|
|
136
|
+
self.facilitator_config = facilitator_config
|
|
137
|
+
self.network = network
|
|
138
|
+
self.resource = resource
|
|
139
|
+
self.paywall_config = paywall_config
|
|
140
|
+
self.custom_paywall_html = custom_paywall_html
|
|
141
|
+
self.protocol_version = protocol_version
|
|
142
|
+
|
|
143
|
+
# Validate and process price
|
|
144
|
+
self._validate()
|
|
145
|
+
|
|
146
|
+
def _validate(self):
|
|
147
|
+
"""Validate configuration."""
|
|
148
|
+
supported_networks = get_all_supported_networks()
|
|
149
|
+
if self.network not in supported_networks:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"Unsupported network: {self.network}. Must be one of: {supported_networks}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
self.max_amount_required, self.asset_address, self.eip712_domain = (
|
|
156
|
+
process_price_to_atomic_amount(self.price, self.network)
|
|
157
|
+
)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
raise ValueError(f"Invalid price: {self.price}. Error: {e}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class PaymentMiddleware:
|
|
163
|
+
"""Starlette middleware for T402 payment requirements.
|
|
164
|
+
|
|
165
|
+
This class provides a flexible way to add payment requirements to Starlette routes.
|
|
166
|
+
It supports multiple configurations with different path patterns and settings.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
```python
|
|
170
|
+
app = Starlette(routes=[...])
|
|
171
|
+
payment = PaymentMiddleware(app)
|
|
172
|
+
|
|
173
|
+
# Add payment requirement for a specific path
|
|
174
|
+
payment.add(
|
|
175
|
+
path="/api/premium/*",
|
|
176
|
+
price="$0.10",
|
|
177
|
+
pay_to_address="0x1234...",
|
|
178
|
+
network="eip155:8453",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Add payment for another path with different config
|
|
182
|
+
payment.add(
|
|
183
|
+
path="/api/data",
|
|
184
|
+
price="$0.50",
|
|
185
|
+
pay_to_address="0x5678...",
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, app: Starlette):
|
|
191
|
+
"""Initialize the payment middleware.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
app: Starlette application instance
|
|
195
|
+
"""
|
|
196
|
+
self.app = app
|
|
197
|
+
self.configs: List[PaymentConfig] = []
|
|
198
|
+
self._middleware_added = False
|
|
199
|
+
|
|
200
|
+
def add(
|
|
201
|
+
self,
|
|
202
|
+
price: Price,
|
|
203
|
+
pay_to_address: str,
|
|
204
|
+
path: Union[str, List[str]] = "*",
|
|
205
|
+
description: str = "",
|
|
206
|
+
mime_type: str = "",
|
|
207
|
+
max_timeout_seconds: int = 60,
|
|
208
|
+
input_schema: Optional[HTTPInputSchema] = None,
|
|
209
|
+
output_schema: Optional[Any] = None,
|
|
210
|
+
discoverable: bool = True,
|
|
211
|
+
facilitator_config: Optional[FacilitatorConfig] = None,
|
|
212
|
+
network: str = "eip155:8453",
|
|
213
|
+
resource: Optional[str] = None,
|
|
214
|
+
paywall_config: Optional[PaywallConfig] = None,
|
|
215
|
+
custom_paywall_html: Optional[str] = None,
|
|
216
|
+
protocol_version: int = T402_VERSION_V2,
|
|
217
|
+
) -> "PaymentMiddleware":
|
|
218
|
+
"""Add a payment requirement configuration.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
price: Payment price (USD string or TokenAmount dict)
|
|
222
|
+
pay_to_address: Address to receive payment
|
|
223
|
+
path: Path pattern(s) to protect
|
|
224
|
+
description: Resource description
|
|
225
|
+
mime_type: Resource MIME type
|
|
226
|
+
max_timeout_seconds: Maximum payment timeout
|
|
227
|
+
input_schema: HTTP input schema
|
|
228
|
+
output_schema: Response schema
|
|
229
|
+
discoverable: Whether route is discoverable
|
|
230
|
+
facilitator_config: Facilitator configuration
|
|
231
|
+
network: Network identifier (CAIP-2 format)
|
|
232
|
+
resource: Explicit resource URL
|
|
233
|
+
paywall_config: Paywall UI configuration
|
|
234
|
+
custom_paywall_html: Custom paywall HTML
|
|
235
|
+
protocol_version: T402 protocol version (1 or 2)
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Self for chaining
|
|
239
|
+
"""
|
|
240
|
+
config = PaymentConfig(
|
|
241
|
+
price=price,
|
|
242
|
+
pay_to_address=pay_to_address,
|
|
243
|
+
path=path,
|
|
244
|
+
description=description,
|
|
245
|
+
mime_type=mime_type,
|
|
246
|
+
max_timeout_seconds=max_timeout_seconds,
|
|
247
|
+
input_schema=input_schema,
|
|
248
|
+
output_schema=output_schema,
|
|
249
|
+
discoverable=discoverable,
|
|
250
|
+
facilitator_config=facilitator_config,
|
|
251
|
+
network=network,
|
|
252
|
+
resource=resource,
|
|
253
|
+
paywall_config=paywall_config,
|
|
254
|
+
custom_paywall_html=custom_paywall_html,
|
|
255
|
+
protocol_version=protocol_version,
|
|
256
|
+
)
|
|
257
|
+
self.configs.append(config)
|
|
258
|
+
|
|
259
|
+
# Add middleware if not already added
|
|
260
|
+
if not self._middleware_added:
|
|
261
|
+
self.app.add_middleware(
|
|
262
|
+
BaseHTTPMiddleware,
|
|
263
|
+
dispatch=self._dispatch,
|
|
264
|
+
)
|
|
265
|
+
self._middleware_added = True
|
|
266
|
+
|
|
267
|
+
return self
|
|
268
|
+
|
|
269
|
+
async def _dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
270
|
+
"""Process request through payment middleware.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
request: Incoming request
|
|
274
|
+
call_next: Next middleware/handler
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Response object
|
|
278
|
+
"""
|
|
279
|
+
# Find matching config
|
|
280
|
+
config = self._find_matching_config(request.url.path)
|
|
281
|
+
if not config:
|
|
282
|
+
return await call_next(request)
|
|
283
|
+
|
|
284
|
+
# Create facilitator client
|
|
285
|
+
facilitator = FacilitatorClient(config.facilitator_config)
|
|
286
|
+
|
|
287
|
+
# Get resource URL
|
|
288
|
+
resource_url = config.resource or str(request.url)
|
|
289
|
+
|
|
290
|
+
# Detect protocol version from request headers
|
|
291
|
+
request_headers = dict(request.headers)
|
|
292
|
+
detect_protocol_version_from_headers(request_headers)
|
|
293
|
+
|
|
294
|
+
# Build payment requirements
|
|
295
|
+
requirements = self._build_requirements(config, request, resource_url)
|
|
296
|
+
|
|
297
|
+
# Create 402 response helper
|
|
298
|
+
def create_402_response(error: str) -> Response:
|
|
299
|
+
return self._create_402_response(
|
|
300
|
+
error=error,
|
|
301
|
+
requirements=[requirements],
|
|
302
|
+
request_headers=request_headers,
|
|
303
|
+
protocol_version=config.protocol_version,
|
|
304
|
+
paywall_config=config.paywall_config,
|
|
305
|
+
custom_paywall_html=config.custom_paywall_html,
|
|
306
|
+
resource_url=resource_url,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Extract payment from headers
|
|
310
|
+
version, payment_header = extract_payment_from_headers(request_headers)
|
|
311
|
+
|
|
312
|
+
if not payment_header:
|
|
313
|
+
return create_402_response("No payment header provided")
|
|
314
|
+
|
|
315
|
+
# Decode payment
|
|
316
|
+
try:
|
|
317
|
+
payment_dict = decode_payment_signature_header(payment_header)
|
|
318
|
+
payment = PaymentPayload(**payment_dict)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.warning(
|
|
321
|
+
f"Invalid payment header from {request.client.host if request.client else 'unknown'}: {e}"
|
|
322
|
+
)
|
|
323
|
+
return create_402_response("Invalid payment header format")
|
|
324
|
+
|
|
325
|
+
# Find matching requirements
|
|
326
|
+
selected_requirements = find_matching_payment_requirements(
|
|
327
|
+
[requirements], payment
|
|
328
|
+
)
|
|
329
|
+
if not selected_requirements:
|
|
330
|
+
return create_402_response("No matching payment requirements found")
|
|
331
|
+
|
|
332
|
+
# Verify payment
|
|
333
|
+
try:
|
|
334
|
+
verify_response = await facilitator.verify(payment, selected_requirements)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
logger.error(f"Payment verification failed: {e}")
|
|
337
|
+
return create_402_response(f"Payment verification failed: {e}")
|
|
338
|
+
|
|
339
|
+
if not verify_response.is_valid:
|
|
340
|
+
error_reason = verify_response.invalid_reason or "Unknown error"
|
|
341
|
+
return create_402_response(f"Invalid payment: {error_reason}")
|
|
342
|
+
|
|
343
|
+
# Store payment details in request state
|
|
344
|
+
request.state.payment_details = PaymentDetails(
|
|
345
|
+
requirements=selected_requirements,
|
|
346
|
+
verify_response=verify_response,
|
|
347
|
+
protocol_version=version,
|
|
348
|
+
)
|
|
349
|
+
request.state.verify_response = verify_response
|
|
350
|
+
|
|
351
|
+
# Process request
|
|
352
|
+
response = await call_next(request)
|
|
353
|
+
|
|
354
|
+
# Skip settlement for non-2xx responses
|
|
355
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
356
|
+
return response
|
|
357
|
+
|
|
358
|
+
# Settle payment
|
|
359
|
+
try:
|
|
360
|
+
settle_response = await facilitator.settle(payment, selected_requirements)
|
|
361
|
+
if settle_response.success:
|
|
362
|
+
# Add settlement header based on version
|
|
363
|
+
header_name = (
|
|
364
|
+
HEADER_PAYMENT_RESPONSE
|
|
365
|
+
if version == T402_VERSION_V2
|
|
366
|
+
else HEADER_X_PAYMENT_RESPONSE
|
|
367
|
+
)
|
|
368
|
+
header_value = encode_payment_response_header(settle_response)
|
|
369
|
+
response.headers[header_name] = header_value
|
|
370
|
+
else:
|
|
371
|
+
return create_402_response(
|
|
372
|
+
f"Settlement failed: {settle_response.error_reason or 'Unknown error'}"
|
|
373
|
+
)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.error(f"Settlement failed: {e}")
|
|
376
|
+
return create_402_response(f"Settlement failed: {e}")
|
|
377
|
+
|
|
378
|
+
return response
|
|
379
|
+
|
|
380
|
+
def _find_matching_config(self, path: str) -> Optional[PaymentConfig]:
|
|
381
|
+
"""Find a matching payment config for the given path.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
path: Request path
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Matching PaymentConfig or None
|
|
388
|
+
"""
|
|
389
|
+
for config in self.configs:
|
|
390
|
+
if path_is_match(config.path, path):
|
|
391
|
+
return config
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
def _build_requirements(
|
|
395
|
+
self,
|
|
396
|
+
config: PaymentConfig,
|
|
397
|
+
request: Request,
|
|
398
|
+
resource_url: str,
|
|
399
|
+
) -> PaymentRequirements:
|
|
400
|
+
"""Build payment requirements from config.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
config: Payment configuration
|
|
404
|
+
request: Incoming request
|
|
405
|
+
resource_url: Resource URL
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
PaymentRequirements object
|
|
409
|
+
"""
|
|
410
|
+
return PaymentRequirements(
|
|
411
|
+
scheme="exact",
|
|
412
|
+
network=cast(SupportedNetworks, config.network),
|
|
413
|
+
asset=config.asset_address,
|
|
414
|
+
max_amount_required=config.max_amount_required,
|
|
415
|
+
resource=resource_url,
|
|
416
|
+
description=config.description,
|
|
417
|
+
mime_type=config.mime_type,
|
|
418
|
+
pay_to=config.pay_to_address,
|
|
419
|
+
max_timeout_seconds=config.max_timeout_seconds,
|
|
420
|
+
output_schema={
|
|
421
|
+
"input": {
|
|
422
|
+
"type": "http",
|
|
423
|
+
"method": request.method.upper(),
|
|
424
|
+
"discoverable": config.discoverable,
|
|
425
|
+
**(config.input_schema.model_dump() if config.input_schema else {}),
|
|
426
|
+
},
|
|
427
|
+
"output": config.output_schema,
|
|
428
|
+
},
|
|
429
|
+
extra=config.eip712_domain,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def _create_402_response(
|
|
433
|
+
self,
|
|
434
|
+
error: str,
|
|
435
|
+
requirements: List[PaymentRequirements],
|
|
436
|
+
request_headers: Dict[str, str],
|
|
437
|
+
protocol_version: int,
|
|
438
|
+
paywall_config: Optional[PaywallConfig],
|
|
439
|
+
custom_paywall_html: Optional[str],
|
|
440
|
+
resource_url: str,
|
|
441
|
+
) -> Response:
|
|
442
|
+
"""Create a 402 Payment Required response.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
error: Error message
|
|
446
|
+
requirements: Payment requirements
|
|
447
|
+
request_headers: Request headers
|
|
448
|
+
protocol_version: Protocol version
|
|
449
|
+
paywall_config: Paywall configuration
|
|
450
|
+
custom_paywall_html: Custom HTML
|
|
451
|
+
resource_url: Resource URL
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
402 Response
|
|
455
|
+
"""
|
|
456
|
+
status_code = 402
|
|
457
|
+
|
|
458
|
+
# Browser request - return HTML paywall
|
|
459
|
+
if is_browser_request(request_headers):
|
|
460
|
+
html_content = custom_paywall_html or get_paywall_html(
|
|
461
|
+
error, requirements, paywall_config
|
|
462
|
+
)
|
|
463
|
+
return HTMLResponse(
|
|
464
|
+
content=html_content,
|
|
465
|
+
status_code=status_code,
|
|
466
|
+
headers={"Content-Type": "text/html; charset=utf-8"},
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# API request - return JSON with appropriate headers
|
|
470
|
+
if protocol_version == T402_VERSION_V2:
|
|
471
|
+
# V2: Use PAYMENT-REQUIRED header
|
|
472
|
+
resource_info = ResourceInfo(
|
|
473
|
+
url=resource_url,
|
|
474
|
+
description=requirements[0].description if requirements else "",
|
|
475
|
+
mime_type=requirements[0].mime_type if requirements else "",
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Convert V1 requirements to V2 format
|
|
479
|
+
accepts_v2 = []
|
|
480
|
+
for req in requirements:
|
|
481
|
+
accepts_v2.append(
|
|
482
|
+
PaymentRequirementsV2(
|
|
483
|
+
scheme=req.scheme,
|
|
484
|
+
network=req.network,
|
|
485
|
+
asset=req.asset,
|
|
486
|
+
amount=req.max_amount_required,
|
|
487
|
+
pay_to=req.pay_to,
|
|
488
|
+
max_timeout_seconds=req.max_timeout_seconds,
|
|
489
|
+
extra=req.extra or {},
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
payment_required = PaymentRequiredV2(
|
|
494
|
+
t402_version=T402_VERSION_V2,
|
|
495
|
+
resource=resource_info,
|
|
496
|
+
accepts=accepts_v2,
|
|
497
|
+
error=error,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
header_value = encode_payment_required_header(payment_required)
|
|
501
|
+
|
|
502
|
+
return JSONResponse(
|
|
503
|
+
content=payment_required.model_dump(by_alias=True),
|
|
504
|
+
status_code=status_code,
|
|
505
|
+
headers={
|
|
506
|
+
"Content-Type": "application/json",
|
|
507
|
+
HEADER_PAYMENT_REQUIRED: header_value,
|
|
508
|
+
},
|
|
509
|
+
)
|
|
510
|
+
else:
|
|
511
|
+
# V1: Return body only
|
|
512
|
+
response_data = t402PaymentRequiredResponse(
|
|
513
|
+
t402_version=T402_VERSION_V1,
|
|
514
|
+
accepts=requirements,
|
|
515
|
+
error=error,
|
|
516
|
+
).model_dump(by_alias=True)
|
|
517
|
+
|
|
518
|
+
return JSONResponse(
|
|
519
|
+
content=response_data,
|
|
520
|
+
status_code=status_code,
|
|
521
|
+
headers={"Content-Type": "application/json"},
|
|
522
|
+
)
|
t402/ton.py
CHANGED
|
@@ -43,7 +43,7 @@ MIN_VALIDITY_BUFFER = 30 # 30 seconds minimum validity
|
|
|
43
43
|
|
|
44
44
|
# USDT Jetton master addresses
|
|
45
45
|
USDT_MAINNET_ADDRESS = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"
|
|
46
|
-
USDT_TESTNET_ADDRESS = "
|
|
46
|
+
USDT_TESTNET_ADDRESS = "kQD0GKBM8ZbryVk2aESmzfU6b9b_8era_IkvBSELujFZPsyy"
|
|
47
47
|
|
|
48
48
|
# Address regex patterns
|
|
49
49
|
TON_FRIENDLY_ADDRESS_REGEX = re.compile(r"^[A-Za-z0-9_-]{46,48}$")
|