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.
Files changed (51) hide show
  1. t402/__init__.py +1 -1
  2. t402/a2a/__init__.py +73 -0
  3. t402/a2a/helpers.py +158 -0
  4. t402/a2a/types.py +145 -0
  5. t402/bridge/constants.py +1 -1
  6. t402/django/__init__.py +42 -0
  7. t402/django/middleware.py +596 -0
  8. t402/errors.py +213 -0
  9. t402/facilitator.py +125 -0
  10. t402/mcp/constants.py +3 -6
  11. t402/mcp/server.py +428 -44
  12. t402/mcp/web3_utils.py +493 -0
  13. t402/multisig/__init__.py +120 -0
  14. t402/multisig/constants.py +54 -0
  15. t402/multisig/safe.py +441 -0
  16. t402/multisig/signature.py +228 -0
  17. t402/multisig/transaction.py +238 -0
  18. t402/multisig/types.py +108 -0
  19. t402/multisig/utils.py +77 -0
  20. t402/schemes/__init__.py +19 -0
  21. t402/schemes/cosmos/__init__.py +114 -0
  22. t402/schemes/cosmos/constants.py +211 -0
  23. t402/schemes/cosmos/exact_direct/__init__.py +21 -0
  24. t402/schemes/cosmos/exact_direct/client.py +198 -0
  25. t402/schemes/cosmos/exact_direct/facilitator.py +493 -0
  26. t402/schemes/cosmos/exact_direct/server.py +315 -0
  27. t402/schemes/cosmos/types.py +501 -0
  28. t402/schemes/evm/__init__.py +1 -1
  29. t402/schemes/evm/exact_legacy/server.py +1 -1
  30. t402/schemes/near/__init__.py +25 -0
  31. t402/schemes/near/upto/__init__.py +54 -0
  32. t402/schemes/near/upto/types.py +272 -0
  33. t402/schemes/svm/__init__.py +15 -0
  34. t402/schemes/svm/upto/__init__.py +23 -0
  35. t402/schemes/svm/upto/types.py +193 -0
  36. t402/schemes/ton/__init__.py +15 -0
  37. t402/schemes/ton/upto/__init__.py +31 -0
  38. t402/schemes/ton/upto/types.py +215 -0
  39. t402/schemes/tron/__init__.py +21 -4
  40. t402/schemes/tron/upto/__init__.py +30 -0
  41. t402/schemes/tron/upto/types.py +213 -0
  42. t402/starlette/__init__.py +38 -0
  43. t402/starlette/middleware.py +522 -0
  44. t402/ton.py +1 -1
  45. t402/ton_paywall_template.py +1 -1
  46. t402/types.py +100 -2
  47. t402/wdk/chains.py +1 -1
  48. {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/METADATA +3 -3
  49. {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/RECORD +51 -20
  50. {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/WHEEL +0 -0
  51. {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 = "kQBqSpvo4S87mX9tTc4FX3Sfqf4uSp3Tx-Fz4RBUfTRWBx"
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}$")