t402 1.6.1__tar.gz → 1.7.1__tar.gz
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-1.6.1 → t402-1.7.1}/PKG-INFO +1 -1
- {t402-1.6.1 → t402-1.7.1}/pyproject.toml +1 -1
- {t402-1.6.1 → t402-1.7.1}/src/t402/__init__.py +169 -2
- {t402-1.6.1 → t402-1.7.1}/src/t402/common.py +37 -8
- t402-1.7.1/src/t402/encoding.py +325 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/facilitator.py +1 -1
- t402-1.7.1/src/t402/fastapi/__init__.py +79 -0
- t402-1.7.1/src/t402/fastapi/dependencies.py +398 -0
- t402-1.7.1/src/t402/fastapi/middleware.py +754 -0
- t402-1.7.1/src/t402/schemes/__init__.py +125 -0
- t402-1.7.1/src/t402/schemes/evm/__init__.py +25 -0
- t402-1.7.1/src/t402/schemes/evm/exact/__init__.py +29 -0
- t402-1.7.1/src/t402/schemes/evm/exact/client.py +265 -0
- t402-1.7.1/src/t402/schemes/evm/exact/server.py +181 -0
- t402-1.7.1/src/t402/schemes/interfaces.py +401 -0
- t402-1.7.1/src/t402/schemes/registry.py +477 -0
- t402-1.7.1/src/t402/schemes/ton/__init__.py +22 -0
- t402-1.7.1/src/t402/schemes/ton/exact/__init__.py +27 -0
- t402-1.7.1/src/t402/schemes/ton/exact/client.py +343 -0
- t402-1.7.1/src/t402/schemes/ton/exact/server.py +201 -0
- t402-1.7.1/src/t402/schemes/tron/__init__.py +22 -0
- t402-1.7.1/src/t402/schemes/tron/exact/__init__.py +27 -0
- t402-1.7.1/src/t402/schemes/tron/exact/client.py +260 -0
- t402-1.7.1/src/t402/schemes/tron/exact/server.py +192 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/types.py +178 -8
- t402-1.7.1/tests/test_fastapi.py +430 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_paywall.py +1 -1
- t402-1.7.1/tests/test_schemes.py +1115 -0
- t402-1.7.1/tests/test_v2_types.py +419 -0
- {t402-1.6.1 → t402-1.7.1}/uv.lock +1 -1
- t402-1.6.1/src/t402/encoding.py +0 -28
- t402-1.6.1/src/t402/fastapi/middleware.py +0 -219
- t402-1.6.1/tests/flask_tests/__init__.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/.gitignore +0 -0
- {t402-1.6.1 → t402-1.7.1}/.python-version +0 -0
- {t402-1.6.1 → t402-1.7.1}/README.md +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/__init__.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/client.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/constants.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/router.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/scan.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/bridge/types.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/chains.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/cli.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/clients/__init__.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/clients/base.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/clients/httpx.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/clients/requests.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/erc4337/__init__.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/erc4337/accounts.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/erc4337/bundlers.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/erc4337/paymasters.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/erc4337/types.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/evm_paywall_template.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/exact.py +0 -0
- {t402-1.6.1/src/t402/fastapi → t402-1.7.1/src/t402/flask}/__init__.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/flask/middleware.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/mcp/__init__.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/mcp/constants.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/mcp/server.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/mcp/tools.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/mcp/types.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/networks.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/path.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/paywall.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/py.typed +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/svm.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/svm_paywall_template.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/ton.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/ton_paywall_template.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/tron.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/wdk/__init__.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/wdk/chains.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/wdk/errors.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/wdk/signer.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/src/t402/wdk/types.py +0 -0
- {t402-1.6.1/src/t402/flask → t402-1.7.1/tests/clients}/__init__.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/clients/test_base.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/clients/test_httpx.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/clients/test_requests.py +0 -0
- {t402-1.6.1/tests/clients → t402-1.7.1/tests/fastapi_tests}/__init__.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/fastapi_tests/test_middleware.py +0 -0
- {t402-1.6.1/tests/fastapi_tests → t402-1.7.1/tests/flask_tests}/__init__.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/flask_tests/test_middleware.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_bridge.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_common.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_encoding.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_exact.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_mcp.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_svm.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_ton.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_tron.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_types.py +0 -0
- {t402-1.6.1 → t402-1.7.1}/tests/test_wdk.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Package version
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.6.2"
|
|
3
3
|
|
|
4
4
|
# Re-export commonly used items for convenience
|
|
5
5
|
from t402.common import (
|
|
@@ -16,8 +16,28 @@ from t402.networks import (
|
|
|
16
16
|
get_network_type,
|
|
17
17
|
)
|
|
18
18
|
from t402.types import (
|
|
19
|
+
# Protocol version constants
|
|
20
|
+
T402_VERSION,
|
|
21
|
+
T402_VERSION_V1,
|
|
22
|
+
T402_VERSION_V2,
|
|
23
|
+
Network,
|
|
24
|
+
# V1 Types (Legacy)
|
|
19
25
|
PaymentRequirements,
|
|
26
|
+
PaymentRequirementsV1,
|
|
20
27
|
PaymentPayload,
|
|
28
|
+
PaymentPayloadV1,
|
|
29
|
+
t402PaymentRequiredResponse,
|
|
30
|
+
t402PaymentRequiredResponseV1,
|
|
31
|
+
# V2 Types (Current)
|
|
32
|
+
ResourceInfo,
|
|
33
|
+
PaymentRequirementsV2,
|
|
34
|
+
PaymentRequiredV2,
|
|
35
|
+
PaymentPayloadV2,
|
|
36
|
+
PaymentResponseV2,
|
|
37
|
+
# Facilitator Types
|
|
38
|
+
SupportedKind,
|
|
39
|
+
SupportedResponse,
|
|
40
|
+
# Common Types
|
|
21
41
|
VerifyResponse,
|
|
22
42
|
SettleResponse,
|
|
23
43
|
TonAuthorization,
|
|
@@ -25,6 +45,31 @@ from t402.types import (
|
|
|
25
45
|
TronAuthorization,
|
|
26
46
|
TronPaymentPayload,
|
|
27
47
|
)
|
|
48
|
+
from t402.encoding import (
|
|
49
|
+
# Base64 utilities
|
|
50
|
+
safe_base64_encode,
|
|
51
|
+
safe_base64_decode,
|
|
52
|
+
is_valid_base64,
|
|
53
|
+
# Header name constants
|
|
54
|
+
HEADER_PAYMENT_SIGNATURE,
|
|
55
|
+
HEADER_PAYMENT_REQUIRED,
|
|
56
|
+
HEADER_PAYMENT_RESPONSE,
|
|
57
|
+
HEADER_X_PAYMENT,
|
|
58
|
+
HEADER_X_PAYMENT_RESPONSE,
|
|
59
|
+
# Encoding/Decoding functions
|
|
60
|
+
encode_payment_signature_header,
|
|
61
|
+
decode_payment_signature_header,
|
|
62
|
+
encode_payment_required_header,
|
|
63
|
+
decode_payment_required_header,
|
|
64
|
+
encode_payment_response_header,
|
|
65
|
+
decode_payment_response_header,
|
|
66
|
+
# Header detection utilities
|
|
67
|
+
get_payment_header_name,
|
|
68
|
+
get_payment_response_header_name,
|
|
69
|
+
detect_protocol_version_from_headers,
|
|
70
|
+
extract_payment_from_headers,
|
|
71
|
+
extract_payment_required_from_response,
|
|
72
|
+
)
|
|
28
73
|
from t402.facilitator import FacilitatorClient, FacilitatorConfig
|
|
29
74
|
from t402.exact import (
|
|
30
75
|
prepare_payment_header,
|
|
@@ -200,6 +245,50 @@ from t402.wdk import (
|
|
|
200
245
|
BalanceError as WDKBalanceError,
|
|
201
246
|
WDKErrorCode,
|
|
202
247
|
)
|
|
248
|
+
from t402.schemes import (
|
|
249
|
+
# Interfaces
|
|
250
|
+
SchemeNetworkClient,
|
|
251
|
+
SchemeNetworkServer,
|
|
252
|
+
SchemeNetworkFacilitator,
|
|
253
|
+
BaseSchemeNetworkClient,
|
|
254
|
+
BaseSchemeNetworkServer,
|
|
255
|
+
BaseSchemeNetworkFacilitator,
|
|
256
|
+
# Registry
|
|
257
|
+
SchemeRegistry,
|
|
258
|
+
ClientSchemeRegistry,
|
|
259
|
+
ServerSchemeRegistry,
|
|
260
|
+
FacilitatorSchemeRegistry,
|
|
261
|
+
get_client_registry,
|
|
262
|
+
get_server_registry,
|
|
263
|
+
get_facilitator_registry,
|
|
264
|
+
reset_global_registries,
|
|
265
|
+
)
|
|
266
|
+
from t402.schemes.evm import (
|
|
267
|
+
ExactEvmClientScheme,
|
|
268
|
+
ExactEvmServerScheme,
|
|
269
|
+
EvmSigner,
|
|
270
|
+
)
|
|
271
|
+
from t402.schemes.ton import (
|
|
272
|
+
ExactTonClientScheme,
|
|
273
|
+
ExactTonServerScheme,
|
|
274
|
+
TonSigner,
|
|
275
|
+
)
|
|
276
|
+
from t402.schemes.tron import (
|
|
277
|
+
ExactTronClientScheme,
|
|
278
|
+
ExactTronServerScheme,
|
|
279
|
+
TronSigner,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# FastAPI Integration
|
|
283
|
+
from t402.fastapi import (
|
|
284
|
+
PaymentMiddleware as FastAPIPaymentMiddleware,
|
|
285
|
+
PaymentConfig as FastAPIPaymentConfig,
|
|
286
|
+
PaymentDetails as FastAPIPaymentDetails,
|
|
287
|
+
PaymentRequired,
|
|
288
|
+
require_payment as fastapi_require_payment,
|
|
289
|
+
get_payment_details,
|
|
290
|
+
settle_payment,
|
|
291
|
+
)
|
|
203
292
|
|
|
204
293
|
def hello() -> str:
|
|
205
294
|
return "Hello from t402!"
|
|
@@ -211,6 +300,11 @@ __all__ = [
|
|
|
211
300
|
# Core
|
|
212
301
|
"hello",
|
|
213
302
|
"t402_VERSION",
|
|
303
|
+
# Protocol Version Constants
|
|
304
|
+
"T402_VERSION",
|
|
305
|
+
"T402_VERSION_V1",
|
|
306
|
+
"T402_VERSION_V2",
|
|
307
|
+
"Network",
|
|
214
308
|
# Common utilities
|
|
215
309
|
"parse_money",
|
|
216
310
|
"process_price_to_atomic_amount",
|
|
@@ -221,15 +315,52 @@ __all__ = [
|
|
|
221
315
|
"is_evm_network",
|
|
222
316
|
"is_svm_network",
|
|
223
317
|
"get_network_type",
|
|
224
|
-
# Types
|
|
318
|
+
# V1 Types (Legacy)
|
|
225
319
|
"PaymentRequirements",
|
|
320
|
+
"PaymentRequirementsV1",
|
|
226
321
|
"PaymentPayload",
|
|
322
|
+
"PaymentPayloadV1",
|
|
323
|
+
"t402PaymentRequiredResponse",
|
|
324
|
+
"t402PaymentRequiredResponseV1",
|
|
325
|
+
# V2 Types (Current)
|
|
326
|
+
"ResourceInfo",
|
|
327
|
+
"PaymentRequirementsV2",
|
|
328
|
+
"PaymentRequiredV2",
|
|
329
|
+
"PaymentPayloadV2",
|
|
330
|
+
"PaymentResponseV2",
|
|
331
|
+
# Facilitator Types
|
|
332
|
+
"SupportedKind",
|
|
333
|
+
"SupportedResponse",
|
|
334
|
+
# Common Types
|
|
227
335
|
"VerifyResponse",
|
|
228
336
|
"SettleResponse",
|
|
229
337
|
"TonAuthorization",
|
|
230
338
|
"TonPaymentPayload",
|
|
231
339
|
"TronAuthorization",
|
|
232
340
|
"TronPaymentPayload",
|
|
341
|
+
# Encoding utilities
|
|
342
|
+
"safe_base64_encode",
|
|
343
|
+
"safe_base64_decode",
|
|
344
|
+
"is_valid_base64",
|
|
345
|
+
# Header constants
|
|
346
|
+
"HEADER_PAYMENT_SIGNATURE",
|
|
347
|
+
"HEADER_PAYMENT_REQUIRED",
|
|
348
|
+
"HEADER_PAYMENT_RESPONSE",
|
|
349
|
+
"HEADER_X_PAYMENT",
|
|
350
|
+
"HEADER_X_PAYMENT_RESPONSE",
|
|
351
|
+
# Header encoding/decoding
|
|
352
|
+
"encode_payment_signature_header",
|
|
353
|
+
"decode_payment_signature_header",
|
|
354
|
+
"encode_payment_required_header",
|
|
355
|
+
"decode_payment_required_header",
|
|
356
|
+
"encode_payment_response_header",
|
|
357
|
+
"decode_payment_response_header",
|
|
358
|
+
# Header detection
|
|
359
|
+
"get_payment_header_name",
|
|
360
|
+
"get_payment_response_header_name",
|
|
361
|
+
"detect_protocol_version_from_headers",
|
|
362
|
+
"extract_payment_from_headers",
|
|
363
|
+
"extract_payment_required_from_response",
|
|
233
364
|
# Facilitator
|
|
234
365
|
"FacilitatorClient",
|
|
235
366
|
"FacilitatorConfig",
|
|
@@ -387,4 +518,40 @@ __all__ = [
|
|
|
387
518
|
"SigningError",
|
|
388
519
|
"WDKBalanceError",
|
|
389
520
|
"WDKErrorCode",
|
|
521
|
+
# Scheme Interfaces
|
|
522
|
+
"SchemeNetworkClient",
|
|
523
|
+
"SchemeNetworkServer",
|
|
524
|
+
"SchemeNetworkFacilitator",
|
|
525
|
+
"BaseSchemeNetworkClient",
|
|
526
|
+
"BaseSchemeNetworkServer",
|
|
527
|
+
"BaseSchemeNetworkFacilitator",
|
|
528
|
+
# Scheme Registry
|
|
529
|
+
"SchemeRegistry",
|
|
530
|
+
"ClientSchemeRegistry",
|
|
531
|
+
"ServerSchemeRegistry",
|
|
532
|
+
"FacilitatorSchemeRegistry",
|
|
533
|
+
"get_client_registry",
|
|
534
|
+
"get_server_registry",
|
|
535
|
+
"get_facilitator_registry",
|
|
536
|
+
"reset_global_registries",
|
|
537
|
+
# EVM Schemes
|
|
538
|
+
"ExactEvmClientScheme",
|
|
539
|
+
"ExactEvmServerScheme",
|
|
540
|
+
"EvmSigner",
|
|
541
|
+
# TON Schemes
|
|
542
|
+
"ExactTonClientScheme",
|
|
543
|
+
"ExactTonServerScheme",
|
|
544
|
+
"TonSigner",
|
|
545
|
+
# TRON Schemes
|
|
546
|
+
"ExactTronClientScheme",
|
|
547
|
+
"ExactTronServerScheme",
|
|
548
|
+
"TronSigner",
|
|
549
|
+
# FastAPI Integration
|
|
550
|
+
"FastAPIPaymentMiddleware",
|
|
551
|
+
"FastAPIPaymentConfig",
|
|
552
|
+
"FastAPIPaymentDetails",
|
|
553
|
+
"PaymentRequired",
|
|
554
|
+
"fastapi_require_payment",
|
|
555
|
+
"get_payment_details",
|
|
556
|
+
"settle_payment",
|
|
390
557
|
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
|
-
from typing import List, Optional
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
3
|
|
|
4
4
|
from t402.chains import (
|
|
5
5
|
get_chain_id,
|
|
@@ -9,7 +9,15 @@ from t402.chains import (
|
|
|
9
9
|
get_default_token_address,
|
|
10
10
|
)
|
|
11
11
|
from t402.networks import is_ton_network, is_tron_network
|
|
12
|
-
from t402.types import
|
|
12
|
+
from t402.types import (
|
|
13
|
+
Price,
|
|
14
|
+
TokenAmount,
|
|
15
|
+
PaymentRequirementsV1,
|
|
16
|
+
PaymentRequirementsV2,
|
|
17
|
+
PaymentPayloadV1,
|
|
18
|
+
PaymentPayloadV2,
|
|
19
|
+
T402_VERSION,
|
|
20
|
+
)
|
|
13
21
|
|
|
14
22
|
|
|
15
23
|
def parse_money(amount: str | int, address: str, network: str) -> int:
|
|
@@ -147,23 +155,44 @@ def get_usdc_address(chain_id: int | str) -> str:
|
|
|
147
155
|
|
|
148
156
|
|
|
149
157
|
def find_matching_payment_requirements(
|
|
150
|
-
payment_requirements: List[
|
|
151
|
-
payment:
|
|
152
|
-
) -> Optional[
|
|
158
|
+
payment_requirements: List[Union[PaymentRequirementsV1, PaymentRequirementsV2]],
|
|
159
|
+
payment: Union[PaymentPayloadV1, PaymentPayloadV2, dict],
|
|
160
|
+
) -> Optional[Union[PaymentRequirementsV1, PaymentRequirementsV2]]:
|
|
153
161
|
"""
|
|
154
162
|
Finds the matching payment requirements for the given payment.
|
|
155
163
|
|
|
164
|
+
Supports both V1 and V2 payment formats.
|
|
165
|
+
|
|
156
166
|
Args:
|
|
157
167
|
payment_requirements: The payment requirements to search through
|
|
158
|
-
payment: The payment to match against
|
|
168
|
+
payment: The payment to match against (V1, V2, or dict)
|
|
159
169
|
|
|
160
170
|
Returns:
|
|
161
171
|
The matching payment requirements or None if no match is found
|
|
162
172
|
"""
|
|
173
|
+
# Handle dict input
|
|
174
|
+
if isinstance(payment, dict):
|
|
175
|
+
payment_scheme = payment.get("scheme")
|
|
176
|
+
payment_network = payment.get("network")
|
|
177
|
+
# V2 format uses "accepted" field
|
|
178
|
+
if "accepted" in payment:
|
|
179
|
+
accepted = payment["accepted"]
|
|
180
|
+
payment_scheme = accepted.get("scheme")
|
|
181
|
+
payment_network = accepted.get("network")
|
|
182
|
+
elif hasattr(payment, "accepted"):
|
|
183
|
+
# V2 PaymentPayload
|
|
184
|
+
payment_scheme = payment.accepted.scheme
|
|
185
|
+
payment_network = payment.accepted.network
|
|
186
|
+
else:
|
|
187
|
+
# V1 PaymentPayload
|
|
188
|
+
payment_scheme = payment.scheme
|
|
189
|
+
payment_network = payment.network
|
|
190
|
+
|
|
163
191
|
for req in payment_requirements:
|
|
164
|
-
if req.scheme ==
|
|
192
|
+
if req.scheme == payment_scheme and req.network == payment_network:
|
|
165
193
|
return req
|
|
166
194
|
return None
|
|
167
195
|
|
|
168
196
|
|
|
169
|
-
|
|
197
|
+
# Re-export version constant for backward compatibility
|
|
198
|
+
t402_VERSION = T402_VERSION
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""T402 Protocol Encoding/Decoding Utilities.
|
|
2
|
+
|
|
3
|
+
This module provides encoding and decoding utilities for the T402 payment protocol,
|
|
4
|
+
supporting both V1 and V2 protocol versions.
|
|
5
|
+
|
|
6
|
+
V1 Headers:
|
|
7
|
+
- X-PAYMENT: Payment signature (client → server)
|
|
8
|
+
- X-PAYMENT-RESPONSE: Settlement response (server → client)
|
|
9
|
+
- Response body: PaymentRequired JSON
|
|
10
|
+
|
|
11
|
+
V2 Headers:
|
|
12
|
+
- PAYMENT-SIGNATURE: Payment signature (client → server)
|
|
13
|
+
- PAYMENT-REQUIRED: Payment requirements (server → client, 402 response)
|
|
14
|
+
- PAYMENT-RESPONSE: Settlement response (server → client)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
from typing import Any, Union
|
|
21
|
+
|
|
22
|
+
from t402.types import (
|
|
23
|
+
PaymentPayloadV1,
|
|
24
|
+
PaymentPayloadV2,
|
|
25
|
+
PaymentRequiredV2,
|
|
26
|
+
PaymentResponseV2,
|
|
27
|
+
SettleResponse,
|
|
28
|
+
T402_VERSION_V1,
|
|
29
|
+
T402_VERSION_V2,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Header name constants
|
|
33
|
+
HEADER_PAYMENT_SIGNATURE = "PAYMENT-SIGNATURE" # V2: Client payment
|
|
34
|
+
HEADER_PAYMENT_REQUIRED = "PAYMENT-REQUIRED" # V2: Server 402 response
|
|
35
|
+
HEADER_PAYMENT_RESPONSE = "PAYMENT-RESPONSE" # V2: Server settlement
|
|
36
|
+
HEADER_X_PAYMENT = "X-PAYMENT" # V1: Client payment
|
|
37
|
+
HEADER_X_PAYMENT_RESPONSE = "X-PAYMENT-RESPONSE" # V1: Server settlement
|
|
38
|
+
|
|
39
|
+
# Base64 validation regex (standard base64 with optional padding)
|
|
40
|
+
BASE64_REGEX = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def safe_base64_encode(data: Union[str, bytes]) -> str:
|
|
44
|
+
"""Safely encode string or bytes to base64 string.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
data: String or bytes to encode
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Base64 encoded string
|
|
51
|
+
"""
|
|
52
|
+
if isinstance(data, str):
|
|
53
|
+
data = data.encode("utf-8")
|
|
54
|
+
return base64.b64encode(data).decode("utf-8")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def safe_base64_decode(data: str) -> str:
|
|
58
|
+
"""Safely decode base64 string to bytes and then to utf-8 string.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
data: Base64 encoded string
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Decoded utf-8 string
|
|
65
|
+
"""
|
|
66
|
+
return base64.b64decode(data).decode("utf-8")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_valid_base64(data: str) -> bool:
|
|
70
|
+
"""Check if a string is valid base64.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
data: String to validate
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if valid base64, False otherwise
|
|
77
|
+
"""
|
|
78
|
+
if not data:
|
|
79
|
+
return False
|
|
80
|
+
# Check length is multiple of 4 (with padding)
|
|
81
|
+
if len(data) % 4 != 0:
|
|
82
|
+
return False
|
|
83
|
+
return bool(BASE64_REGEX.match(data))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# V2 Header Encoding/Decoding
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def encode_payment_signature_header(payment_payload: Union[PaymentPayloadV1, PaymentPayloadV2, dict]) -> str:
|
|
92
|
+
"""Encode a payment payload as a base64 header value.
|
|
93
|
+
|
|
94
|
+
Works with both V1 and V2 payment payloads.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
payment_payload: The payment payload to encode (model or dict)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Base64 encoded string representation of the payment payload
|
|
101
|
+
"""
|
|
102
|
+
if hasattr(payment_payload, "model_dump"):
|
|
103
|
+
data = payment_payload.model_dump(by_alias=True, exclude_none=True)
|
|
104
|
+
else:
|
|
105
|
+
data = payment_payload
|
|
106
|
+
return safe_base64_encode(json.dumps(data))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def decode_payment_signature_header(header_value: str) -> dict[str, Any]:
|
|
110
|
+
"""Decode a base64 payment signature header into a payment payload dict.
|
|
111
|
+
|
|
112
|
+
The caller should determine the version from the t402Version field
|
|
113
|
+
and parse into the appropriate type.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
header_value: The base64 encoded payment signature header
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The decoded payment payload as a dict
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: If the header is not valid base64 or JSON
|
|
123
|
+
"""
|
|
124
|
+
if not is_valid_base64(header_value):
|
|
125
|
+
raise ValueError("Invalid payment signature header: not valid base64")
|
|
126
|
+
try:
|
|
127
|
+
return json.loads(safe_base64_decode(header_value))
|
|
128
|
+
except json.JSONDecodeError as e:
|
|
129
|
+
raise ValueError(f"Invalid payment signature header: invalid JSON - {e}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def encode_payment_required_header(payment_required: Union[PaymentRequiredV2, dict]) -> str:
|
|
133
|
+
"""Encode a payment required object as a base64 header value.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
payment_required: The payment required object to encode (model or dict)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Base64 encoded string representation of the payment required object
|
|
140
|
+
"""
|
|
141
|
+
if hasattr(payment_required, "model_dump"):
|
|
142
|
+
data = payment_required.model_dump(by_alias=True, exclude_none=True)
|
|
143
|
+
else:
|
|
144
|
+
data = payment_required
|
|
145
|
+
return safe_base64_encode(json.dumps(data))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def decode_payment_required_header(header_value: str) -> dict[str, Any]:
|
|
149
|
+
"""Decode a base64 payment required header.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
header_value: The base64 encoded payment required header
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The decoded payment required object as a dict
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ValueError: If the header is not valid base64 or JSON
|
|
159
|
+
"""
|
|
160
|
+
if not is_valid_base64(header_value):
|
|
161
|
+
raise ValueError("Invalid payment required header: not valid base64")
|
|
162
|
+
try:
|
|
163
|
+
return json.loads(safe_base64_decode(header_value))
|
|
164
|
+
except json.JSONDecodeError as e:
|
|
165
|
+
raise ValueError(f"Invalid payment required header: invalid JSON - {e}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def encode_payment_response_header(
|
|
169
|
+
payment_response: Union[PaymentResponseV2, SettleResponse, dict],
|
|
170
|
+
requirements: dict[str, Any] | None = None,
|
|
171
|
+
) -> str:
|
|
172
|
+
"""Encode a payment response as a base64 header value.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
payment_response: The payment response to encode (model or dict)
|
|
176
|
+
requirements: Optional requirements to include (for V2 format)
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Base64 encoded string representation of the payment response
|
|
180
|
+
"""
|
|
181
|
+
if hasattr(payment_response, "model_dump"):
|
|
182
|
+
data = payment_response.model_dump(by_alias=True, exclude_none=True)
|
|
183
|
+
else:
|
|
184
|
+
data = dict(payment_response)
|
|
185
|
+
|
|
186
|
+
# If requirements provided separately, add them
|
|
187
|
+
if requirements and "requirements" not in data:
|
|
188
|
+
data["requirements"] = requirements
|
|
189
|
+
|
|
190
|
+
return safe_base64_encode(json.dumps(data))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def decode_payment_response_header(header_value: str) -> dict[str, Any]:
|
|
194
|
+
"""Decode a base64 payment response header.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
header_value: The base64 encoded payment response header
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The decoded payment response as a dict
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
ValueError: If the header is not valid base64 or JSON
|
|
204
|
+
"""
|
|
205
|
+
if not is_valid_base64(header_value):
|
|
206
|
+
raise ValueError("Invalid payment response header: not valid base64")
|
|
207
|
+
try:
|
|
208
|
+
return json.loads(safe_base64_decode(header_value))
|
|
209
|
+
except json.JSONDecodeError as e:
|
|
210
|
+
raise ValueError(f"Invalid payment response header: invalid JSON - {e}")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# =============================================================================
|
|
214
|
+
# Header Detection Utilities
|
|
215
|
+
# =============================================================================
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_payment_header_name(version: int) -> str:
|
|
219
|
+
"""Get the payment header name for a protocol version.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
version: Protocol version (1 or 2)
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Header name string
|
|
226
|
+
"""
|
|
227
|
+
if version == T402_VERSION_V1:
|
|
228
|
+
return HEADER_X_PAYMENT
|
|
229
|
+
return HEADER_PAYMENT_SIGNATURE
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_payment_response_header_name(version: int) -> str:
|
|
233
|
+
"""Get the payment response header name for a protocol version.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
version: Protocol version (1 or 2)
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Header name string
|
|
240
|
+
"""
|
|
241
|
+
if version == T402_VERSION_V1:
|
|
242
|
+
return HEADER_X_PAYMENT_RESPONSE
|
|
243
|
+
return HEADER_PAYMENT_RESPONSE
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def detect_protocol_version_from_headers(headers: dict[str, str]) -> int:
|
|
247
|
+
"""Detect the protocol version from HTTP headers.
|
|
248
|
+
|
|
249
|
+
Checks for V2 headers first, falls back to V1.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
headers: HTTP headers (case-insensitive keys)
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Protocol version (1 or 2)
|
|
256
|
+
"""
|
|
257
|
+
# Normalize header keys to lowercase for comparison
|
|
258
|
+
lower_headers = {k.lower(): v for k, v in headers.items()}
|
|
259
|
+
|
|
260
|
+
# Check for V2 headers first
|
|
261
|
+
if "payment-signature" in lower_headers or "payment-required" in lower_headers:
|
|
262
|
+
return T402_VERSION_V2
|
|
263
|
+
|
|
264
|
+
# Fall back to V1
|
|
265
|
+
if "x-payment" in lower_headers:
|
|
266
|
+
return T402_VERSION_V1
|
|
267
|
+
|
|
268
|
+
# Default to V2 (current version)
|
|
269
|
+
return T402_VERSION_V2
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def extract_payment_from_headers(headers: dict[str, str]) -> tuple[int, str | None]:
|
|
273
|
+
"""Extract payment header value and detect version from headers.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
headers: HTTP headers (case-insensitive keys)
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Tuple of (version, header_value or None)
|
|
280
|
+
"""
|
|
281
|
+
# Normalize header keys to lowercase for comparison
|
|
282
|
+
lower_headers = {k.lower(): v for k, v in headers.items()}
|
|
283
|
+
|
|
284
|
+
# Check V2 first
|
|
285
|
+
if "payment-signature" in lower_headers:
|
|
286
|
+
return T402_VERSION_V2, lower_headers["payment-signature"]
|
|
287
|
+
|
|
288
|
+
# Check V1
|
|
289
|
+
if "x-payment" in lower_headers:
|
|
290
|
+
return T402_VERSION_V1, lower_headers["x-payment"]
|
|
291
|
+
|
|
292
|
+
return T402_VERSION_V2, None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def extract_payment_required_from_response(
|
|
296
|
+
headers: dict[str, str],
|
|
297
|
+
body: dict[str, Any] | None = None,
|
|
298
|
+
) -> tuple[int, dict[str, Any] | None]:
|
|
299
|
+
"""Extract payment required from HTTP response (headers or body).
|
|
300
|
+
|
|
301
|
+
V2: Payment required is in PAYMENT-REQUIRED header
|
|
302
|
+
V1: Payment required is in response body
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
headers: HTTP response headers
|
|
306
|
+
body: Optional response body (for V1)
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Tuple of (version, payment_required dict or None)
|
|
310
|
+
"""
|
|
311
|
+
# Normalize header keys
|
|
312
|
+
lower_headers = {k.lower(): v for k, v in headers.items()}
|
|
313
|
+
|
|
314
|
+
# Check V2 header first
|
|
315
|
+
if "payment-required" in lower_headers:
|
|
316
|
+
try:
|
|
317
|
+
return T402_VERSION_V2, decode_payment_required_header(lower_headers["payment-required"])
|
|
318
|
+
except ValueError:
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
# Check V1 body
|
|
322
|
+
if body and isinstance(body, dict) and "t402Version" in body:
|
|
323
|
+
return body.get("t402Version", T402_VERSION_V1), body
|
|
324
|
+
|
|
325
|
+
return T402_VERSION_V2, None
|
|
@@ -28,7 +28,7 @@ class FacilitatorConfig(TypedDict, total=False):
|
|
|
28
28
|
class FacilitatorClient:
|
|
29
29
|
def __init__(self, config: Optional[FacilitatorConfig] = None):
|
|
30
30
|
if config is None:
|
|
31
|
-
config = {"url": "https://t402.
|
|
31
|
+
config = {"url": "https://facilitator.t402.io"}
|
|
32
32
|
|
|
33
33
|
# Validate URL format
|
|
34
34
|
url = config.get("url", "")
|