t402 1.0.0__py3-none-any.whl → 1.1.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 CHANGED
@@ -1,2 +1,98 @@
1
+ # Re-export commonly used items for convenience
2
+ from t402.common import (
3
+ parse_money,
4
+ process_price_to_atomic_amount,
5
+ find_matching_payment_requirements,
6
+ t402_VERSION,
7
+ )
8
+ from t402.networks import (
9
+ is_ton_network,
10
+ is_evm_network,
11
+ get_network_type,
12
+ )
13
+ from t402.types import (
14
+ PaymentRequirements,
15
+ PaymentPayload,
16
+ VerifyResponse,
17
+ SettleResponse,
18
+ TonAuthorization,
19
+ TonPaymentPayload,
20
+ )
21
+ from t402.facilitator import FacilitatorClient, FacilitatorConfig
22
+ from t402.exact import (
23
+ prepare_payment_header,
24
+ sign_payment_header,
25
+ encode_payment,
26
+ decode_payment,
27
+ )
28
+ from t402.ton import (
29
+ TON_MAINNET,
30
+ TON_TESTNET,
31
+ USDT_MAINNET_ADDRESS,
32
+ USDT_TESTNET_ADDRESS,
33
+ validate_ton_address,
34
+ get_usdt_address,
35
+ get_network_config as get_ton_network_config,
36
+ get_default_asset as get_ton_default_asset,
37
+ prepare_ton_payment_header,
38
+ parse_amount as parse_ton_amount,
39
+ format_amount as format_ton_amount,
40
+ validate_boc,
41
+ is_testnet as is_ton_testnet,
42
+ )
43
+ from t402.paywall import (
44
+ get_paywall_html,
45
+ get_paywall_template,
46
+ is_browser_request,
47
+ )
48
+
1
49
  def hello() -> str:
2
50
  return "Hello from t402!"
51
+
52
+
53
+ __all__ = [
54
+ # Core
55
+ "hello",
56
+ "t402_VERSION",
57
+ # Common utilities
58
+ "parse_money",
59
+ "process_price_to_atomic_amount",
60
+ "find_matching_payment_requirements",
61
+ # Network utilities
62
+ "is_ton_network",
63
+ "is_evm_network",
64
+ "get_network_type",
65
+ # Types
66
+ "PaymentRequirements",
67
+ "PaymentPayload",
68
+ "VerifyResponse",
69
+ "SettleResponse",
70
+ "TonAuthorization",
71
+ "TonPaymentPayload",
72
+ # Facilitator
73
+ "FacilitatorClient",
74
+ "FacilitatorConfig",
75
+ # EVM payment
76
+ "prepare_payment_header",
77
+ "sign_payment_header",
78
+ "encode_payment",
79
+ "decode_payment",
80
+ # TON utilities
81
+ "TON_MAINNET",
82
+ "TON_TESTNET",
83
+ "USDT_MAINNET_ADDRESS",
84
+ "USDT_TESTNET_ADDRESS",
85
+ "validate_ton_address",
86
+ "get_usdt_address",
87
+ "get_ton_network_config",
88
+ "get_ton_default_asset",
89
+ "prepare_ton_payment_header",
90
+ "parse_ton_amount",
91
+ "format_ton_amount",
92
+ "validate_boc",
93
+ "is_ton_testnet",
94
+ # Paywall
95
+ "get_paywall_html",
96
+ "get_paywall_template",
97
+ "is_browser_request",
98
+ ]
t402/common.py CHANGED
@@ -8,6 +8,7 @@ from t402.chains import (
8
8
  get_token_version,
9
9
  get_default_token_address,
10
10
  )
11
+ from t402.networks import is_ton_network
11
12
  from t402.types import Price, TokenAmount, PaymentRequirements, PaymentPayload
12
13
 
13
14
 
@@ -22,8 +23,14 @@ def parse_money(amount: str | int, address: str, network: str) -> int:
22
23
  amount = amount[1:]
23
24
  decimal_amount = Decimal(amount)
24
25
 
25
- chain_id = get_chain_id(network)
26
- decimals = get_token_decimals(chain_id, address)
26
+ # Handle TON networks differently
27
+ if is_ton_network(network):
28
+ from t402.ton import DEFAULT_DECIMALS
29
+ decimals = DEFAULT_DECIMALS # USDT on TON uses 6 decimals
30
+ else:
31
+ chain_id = get_chain_id(network)
32
+ decimals = get_token_decimals(chain_id, address)
33
+
27
34
  decimal_amount = decimal_amount * Decimal(10**decimals)
28
35
  return int(decimal_amount)
29
36
  return amount
@@ -39,19 +46,42 @@ def process_price_to_atomic_amount(
39
46
  network: Network identifier
40
47
 
41
48
  Returns:
42
- Tuple of (max_amount_required, asset_address, eip712_domain)
49
+ Tuple of (max_amount_required, asset_address, extra_info)
50
+ For EVM: extra_info contains EIP-712 domain (name, version)
51
+ For TON: extra_info contains Jetton metadata (name, symbol)
43
52
 
44
53
  Raises:
45
54
  ValueError: If price format is invalid
46
55
  """
47
56
  if isinstance(price, (str, int)):
48
- # Money type - convert USD to USDC atomic units
57
+ # Money type - convert USD to atomic units
49
58
  try:
50
59
  if isinstance(price, str) and price.startswith("$"):
51
60
  price = price[1:]
52
61
  amount = Decimal(str(price))
53
62
 
54
- # Get USDC address for the network
63
+ # Handle TON networks
64
+ if is_ton_network(network):
65
+ from t402.ton import (
66
+ get_usdt_address,
67
+ get_default_asset,
68
+ DEFAULT_DECIMALS,
69
+ )
70
+
71
+ asset_address = get_usdt_address(network)
72
+ decimals = DEFAULT_DECIMALS
73
+ atomic_amount = int(amount * Decimal(10**decimals))
74
+
75
+ # For TON, return Jetton metadata instead of EIP-712 domain
76
+ asset_info = get_default_asset(network)
77
+ extra_info = {
78
+ "name": asset_info["name"] if asset_info else "Tether USD",
79
+ "symbol": asset_info["symbol"] if asset_info else "USDT",
80
+ }
81
+
82
+ return str(atomic_amount), asset_address, extra_info
83
+
84
+ # Handle EVM networks
55
85
  chain_id = get_chain_id(network)
56
86
  asset_address = get_usdc_address(chain_id)
57
87
  decimals = get_token_decimals(chain_id, asset_address)
@@ -1,7 +1,7 @@
1
1
  import base64
2
2
  import json
3
3
  import logging
4
- from typing import Any, Callable, Optional, get_args, cast
4
+ from typing import Any, Callable, Optional, cast
5
5
 
6
6
  from fastapi import Request
7
7
  from fastapi.responses import JSONResponse, HTMLResponse
@@ -14,6 +14,7 @@ from t402.common import (
14
14
  )
15
15
  from t402.encoding import safe_base64_decode
16
16
  from t402.facilitator import FacilitatorClient, FacilitatorConfig
17
+ from t402.networks import get_all_supported_networks, SupportedNetworks
17
18
  from t402.path import path_is_match
18
19
  from t402.paywall import is_browser_request, get_paywall_html
19
20
  from t402.types import (
@@ -22,7 +23,6 @@ from t402.types import (
22
23
  Price,
23
24
  t402PaymentRequiredResponse,
24
25
  PaywallConfig,
25
- SupportedNetworks,
26
26
  HTTPInputSchema,
27
27
  )
28
28
 
@@ -73,7 +73,7 @@ def require_payment(
73
73
  """
74
74
 
75
75
  # Validate network is supported
76
- supported_networks = get_args(SupportedNetworks)
76
+ supported_networks = get_all_supported_networks()
77
77
  if network not in supported_networks:
78
78
  raise ValueError(
79
79
  f"Unsupported network: {network}. Must be one of: {supported_networks}"
t402/flask/middleware.py CHANGED
@@ -1,15 +1,15 @@
1
1
  import base64
2
2
  import json
3
- from typing import Any, Dict, Optional, Union, get_args, cast
3
+ from typing import Any, Dict, Optional, Union, cast
4
4
  from flask import Flask, request, g
5
5
  from t402.path import path_is_match
6
+ from t402.networks import get_all_supported_networks, SupportedNetworks
6
7
  from t402.types import (
7
8
  Price,
8
9
  PaymentPayload,
9
10
  PaymentRequirements,
10
11
  t402PaymentRequiredResponse,
11
12
  PaywallConfig,
12
- SupportedNetworks,
13
13
  HTTPInputSchema,
14
14
  )
15
15
  from t402.common import (
@@ -148,7 +148,7 @@ class PaymentMiddleware:
148
148
  """Create a WSGI middleware function for the given configuration."""
149
149
 
150
150
  # Validate network is supported
151
- supported_networks = get_args(SupportedNetworks)
151
+ supported_networks = get_all_supported_networks()
152
152
  if config["network"] not in supported_networks:
153
153
  raise ValueError(
154
154
  f"Unsupported network: {config['network']}. Must be one of: {supported_networks}"
t402/networks.py CHANGED
@@ -1,7 +1,21 @@
1
- from typing import Literal
1
+ from typing import Literal, Union, get_args
2
2
 
3
3
 
4
- SupportedNetworks = Literal["base", "base-sepolia", "avalanche-fuji", "avalanche"]
4
+ # EVM Networks
5
+ EVMNetworks = Literal["base", "base-sepolia", "avalanche-fuji", "avalanche"]
6
+
7
+ # TON Networks (CAIP-2 format)
8
+ TONNetworks = Literal["ton:mainnet", "ton:testnet"]
9
+
10
+ # All supported networks
11
+ SupportedNetworks = Union[EVMNetworks, TONNetworks]
12
+
13
+
14
+ def get_all_supported_networks() -> tuple[str, ...]:
15
+ """Get all supported network identifiers as a flat tuple of strings."""
16
+ evm = get_args(EVMNetworks)
17
+ ton = get_args(TONNetworks)
18
+ return evm + ton
5
19
 
6
20
  EVM_NETWORK_TO_CHAIN_ID = {
7
21
  "base-sepolia": 84532,
@@ -9,3 +23,36 @@ EVM_NETWORK_TO_CHAIN_ID = {
9
23
  "avalanche-fuji": 43113,
10
24
  "avalanche": 43114,
11
25
  }
26
+
27
+ # TON Network configurations
28
+ TON_NETWORKS = {
29
+ "ton:mainnet": {
30
+ "name": "TON Mainnet",
31
+ "endpoint": "https://toncenter.com/api/v2/jsonRPC",
32
+ "is_testnet": False,
33
+ },
34
+ "ton:testnet": {
35
+ "name": "TON Testnet",
36
+ "endpoint": "https://testnet.toncenter.com/api/v2/jsonRPC",
37
+ "is_testnet": True,
38
+ },
39
+ }
40
+
41
+
42
+ def is_ton_network(network: str) -> bool:
43
+ """Check if a network is a TON network."""
44
+ return network.startswith("ton:")
45
+
46
+
47
+ def is_evm_network(network: str) -> bool:
48
+ """Check if a network is an EVM network."""
49
+ return network in EVM_NETWORK_TO_CHAIN_ID
50
+
51
+
52
+ def get_network_type(network: str) -> str:
53
+ """Get the network type (ton, evm, or unknown)."""
54
+ if is_ton_network(network):
55
+ return "ton"
56
+ if is_evm_network(network):
57
+ return "evm"
58
+ return "unknown"
t402/paywall.py CHANGED
@@ -5,12 +5,15 @@ from t402.types import PaymentRequirements, PaywallConfig
5
5
  from t402.common import t402_VERSION
6
6
  from t402.evm_paywall_template import EVM_PAYWALL_TEMPLATE
7
7
  from t402.svm_paywall_template import SVM_PAYWALL_TEMPLATE
8
+ from t402.ton_paywall_template import TON_PAYWALL_TEMPLATE
8
9
 
9
10
 
10
11
  def get_paywall_template(network: str) -> str:
11
12
  """Get the appropriate paywall template for the given network."""
12
13
  if network.startswith("solana:"):
13
14
  return SVM_PAYWALL_TEMPLATE
15
+ if network.startswith("ton:"):
16
+ return TON_PAYWALL_TEMPLATE
14
17
  return EVM_PAYWALL_TEMPLATE
15
18
 
16
19
 
t402/ton.py ADDED
@@ -0,0 +1,474 @@
1
+ """
2
+ TON blockchain support for t402 protocol.
3
+
4
+ This module provides types and utilities for TON (The Open Network) payments
5
+ using USDT Jetton transfers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import time
12
+ import base64
13
+ from typing import Any, Dict, Optional, List
14
+ from typing_extensions import TypedDict
15
+
16
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
17
+ from pydantic.alias_generators import to_camel
18
+
19
+
20
+ # Constants
21
+ SCHEME_EXACT = "exact"
22
+ DEFAULT_DECIMALS = 6
23
+
24
+ # CAIP-2 network identifiers
25
+ TON_MAINNET = "ton:mainnet"
26
+ TON_TESTNET = "ton:testnet"
27
+
28
+ # Jetton transfer operation codes (TEP-74)
29
+ JETTON_TRANSFER_OP = 0x0F8A7EA5
30
+ JETTON_INTERNAL_TRANSFER_OP = 0x178D4519
31
+ JETTON_TRANSFER_NOTIFICATION_OP = 0x7362D09C
32
+ JETTON_BURN_OP = 0x595F07BC
33
+
34
+ # Gas defaults (in nanoTON)
35
+ DEFAULT_JETTON_TRANSFER_TON = 100_000_000 # 0.1 TON
36
+ DEFAULT_FORWARD_TON = 1 # Minimal forward
37
+ MIN_JETTON_TRANSFER_TON = 50_000_000 # 0.05 TON minimum
38
+ MAX_JETTON_TRANSFER_TON = 500_000_000 # 0.5 TON maximum
39
+
40
+ # Validity and timing
41
+ DEFAULT_VALIDITY_DURATION = 3600 # 1 hour in seconds
42
+ MIN_VALIDITY_BUFFER = 30 # 30 seconds minimum validity
43
+
44
+ # USDT Jetton master addresses
45
+ USDT_MAINNET_ADDRESS = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"
46
+ USDT_TESTNET_ADDRESS = "kQBqSpvo4S87mX9tTc4FX3Sfqf4uSp3Tx-Fz4RBUfTRWBx"
47
+
48
+ # Address regex patterns
49
+ TON_FRIENDLY_ADDRESS_REGEX = re.compile(r"^[A-Za-z0-9_-]{46,48}$")
50
+ TON_RAW_ADDRESS_REGEX = re.compile(r"^-?[0-9]:[a-fA-F0-9]{64}$")
51
+
52
+
53
+ class JettonConfig(TypedDict):
54
+ """Configuration for a Jetton token."""
55
+ master_address: str
56
+ symbol: str
57
+ name: str
58
+ decimals: int
59
+
60
+
61
+ class NetworkConfig(TypedDict):
62
+ """Configuration for a TON network."""
63
+ name: str
64
+ endpoint: str
65
+ is_testnet: bool
66
+ default_asset: JettonConfig
67
+ supported_assets: Dict[str, JettonConfig]
68
+
69
+
70
+ # Network configurations
71
+ NETWORK_CONFIGS: Dict[str, NetworkConfig] = {
72
+ TON_MAINNET: {
73
+ "name": "TON Mainnet",
74
+ "endpoint": "https://toncenter.com/api/v2/jsonRPC",
75
+ "is_testnet": False,
76
+ "default_asset": {
77
+ "master_address": USDT_MAINNET_ADDRESS,
78
+ "symbol": "USDT",
79
+ "name": "Tether USD",
80
+ "decimals": DEFAULT_DECIMALS,
81
+ },
82
+ "supported_assets": {
83
+ "USDT": {
84
+ "master_address": USDT_MAINNET_ADDRESS,
85
+ "symbol": "USDT",
86
+ "name": "Tether USD",
87
+ "decimals": DEFAULT_DECIMALS,
88
+ },
89
+ },
90
+ },
91
+ TON_TESTNET: {
92
+ "name": "TON Testnet",
93
+ "endpoint": "https://testnet.toncenter.com/api/v2/jsonRPC",
94
+ "is_testnet": True,
95
+ "default_asset": {
96
+ "master_address": USDT_TESTNET_ADDRESS,
97
+ "symbol": "USDT",
98
+ "name": "Tether USD (Testnet)",
99
+ "decimals": DEFAULT_DECIMALS,
100
+ },
101
+ "supported_assets": {
102
+ "USDT": {
103
+ "master_address": USDT_TESTNET_ADDRESS,
104
+ "symbol": "USDT",
105
+ "name": "Tether USD (Testnet)",
106
+ "decimals": DEFAULT_DECIMALS,
107
+ },
108
+ },
109
+ },
110
+ }
111
+
112
+
113
+ class TonAuthorization(BaseModel):
114
+ """TON transfer authorization metadata."""
115
+
116
+ from_: str = Field(alias="from")
117
+ to: str
118
+ jetton_master: str = Field(alias="jettonMaster")
119
+ jetton_amount: str = Field(alias="jettonAmount")
120
+ ton_amount: str = Field(alias="tonAmount")
121
+ valid_until: int = Field(alias="validUntil")
122
+ seqno: int
123
+ query_id: str = Field(alias="queryId")
124
+
125
+ model_config = ConfigDict(
126
+ alias_generator=to_camel,
127
+ populate_by_name=True,
128
+ from_attributes=True,
129
+ )
130
+
131
+ @field_validator("jetton_amount", "ton_amount")
132
+ def validate_amount(cls, v):
133
+ try:
134
+ int(v)
135
+ except ValueError:
136
+ raise ValueError("amount must be an integer encoded as a string")
137
+ return v
138
+
139
+
140
+ class TonPaymentPayload(BaseModel):
141
+ """TON payment payload containing signed BOC and authorization."""
142
+
143
+ signed_boc: str = Field(alias="signedBoc")
144
+ authorization: TonAuthorization
145
+
146
+ model_config = ConfigDict(
147
+ alias_generator=to_camel,
148
+ populate_by_name=True,
149
+ from_attributes=True,
150
+ )
151
+
152
+
153
+ class TonVerifyMessageResult(BaseModel):
154
+ """Result of TON message verification."""
155
+
156
+ valid: bool
157
+ reason: Optional[str] = None
158
+ transfer: Optional[Dict[str, Any]] = None
159
+
160
+
161
+ class TonTransactionConfirmation(BaseModel):
162
+ """TON transaction confirmation result."""
163
+
164
+ success: bool
165
+ lt: Optional[str] = None
166
+ hash: Optional[str] = None
167
+ error: Optional[str] = None
168
+
169
+
170
+ def validate_ton_address(address: str) -> bool:
171
+ """
172
+ Validate a TON address.
173
+
174
+ Supports both friendly format (base64url, 48 chars) and
175
+ raw format (workchain:hash).
176
+
177
+ Args:
178
+ address: The address to validate
179
+
180
+ Returns:
181
+ True if valid, False otherwise
182
+ """
183
+ if not address:
184
+ return False
185
+
186
+ # Check friendly format (base64url, 46-48 chars)
187
+ if TON_FRIENDLY_ADDRESS_REGEX.match(address):
188
+ return True
189
+
190
+ # Check raw format (workchain:hash)
191
+ if TON_RAW_ADDRESS_REGEX.match(address):
192
+ return True
193
+
194
+ return False
195
+
196
+
197
+ def addresses_equal(addr1: str, addr2: str) -> bool:
198
+ """
199
+ Compare two TON addresses for equality.
200
+
201
+ Args:
202
+ addr1: First address
203
+ addr2: Second address
204
+
205
+ Returns:
206
+ True if addresses are equal (case-insensitive)
207
+ """
208
+ return addr1.lower() == addr2.lower()
209
+
210
+
211
+ def is_valid_network(network: str) -> bool:
212
+ """
213
+ Check if a network is a supported TON network.
214
+
215
+ Args:
216
+ network: Network identifier
217
+
218
+ Returns:
219
+ True if supported
220
+ """
221
+ return network in NETWORK_CONFIGS
222
+
223
+
224
+ def get_network_config(network: str) -> Optional[NetworkConfig]:
225
+ """
226
+ Get configuration for a TON network.
227
+
228
+ Args:
229
+ network: Network identifier (e.g., "ton:mainnet")
230
+
231
+ Returns:
232
+ NetworkConfig or None if not found
233
+ """
234
+ return NETWORK_CONFIGS.get(network)
235
+
236
+
237
+ def get_default_asset(network: str) -> Optional[JettonConfig]:
238
+ """
239
+ Get the default asset (USDT) for a network.
240
+
241
+ Args:
242
+ network: Network identifier
243
+
244
+ Returns:
245
+ JettonConfig or None if network not found
246
+ """
247
+ config = get_network_config(network)
248
+ if config:
249
+ return config["default_asset"]
250
+ return None
251
+
252
+
253
+ def get_asset_info(network: str, asset_symbol_or_address: str) -> Optional[JettonConfig]:
254
+ """
255
+ Get asset information by symbol or address.
256
+
257
+ Args:
258
+ network: Network identifier
259
+ asset_symbol_or_address: Asset symbol (e.g., "USDT") or master address
260
+
261
+ Returns:
262
+ JettonConfig or None if not found
263
+ """
264
+ config = get_network_config(network)
265
+ if not config:
266
+ return None
267
+
268
+ # Check if it's a valid address
269
+ if validate_ton_address(asset_symbol_or_address):
270
+ # Check default asset
271
+ if addresses_equal(
272
+ asset_symbol_or_address, config["default_asset"]["master_address"]
273
+ ):
274
+ return config["default_asset"]
275
+
276
+ # Check supported assets by address
277
+ for asset in config["supported_assets"].values():
278
+ if addresses_equal(asset_symbol_or_address, asset["master_address"]):
279
+ return asset
280
+
281
+ # Unknown token
282
+ return {
283
+ "master_address": asset_symbol_or_address,
284
+ "symbol": "UNKNOWN",
285
+ "name": "Unknown Jetton",
286
+ "decimals": 9,
287
+ }
288
+
289
+ # Look up by symbol
290
+ symbol = asset_symbol_or_address.upper()
291
+ if symbol in config["supported_assets"]:
292
+ return config["supported_assets"][symbol]
293
+
294
+ # Default to network's default asset
295
+ return config["default_asset"]
296
+
297
+
298
+ def parse_amount(amount: str, decimals: int) -> int:
299
+ """
300
+ Parse a decimal string amount to token smallest units.
301
+
302
+ Args:
303
+ amount: Decimal string (e.g., "1.50")
304
+ decimals: Token decimals
305
+
306
+ Returns:
307
+ Amount in smallest units
308
+
309
+ Raises:
310
+ ValueError: If amount format is invalid
311
+ """
312
+ amount = amount.strip()
313
+ parts = amount.split(".")
314
+
315
+ if len(parts) > 2:
316
+ raise ValueError(f"Invalid amount format: {amount}")
317
+
318
+ int_part = int(parts[0])
319
+
320
+ dec_part = 0
321
+ if len(parts) == 2 and parts[1]:
322
+ dec_str = parts[1]
323
+ if len(dec_str) > decimals:
324
+ dec_str = dec_str[:decimals]
325
+ else:
326
+ dec_str = dec_str + "0" * (decimals - len(dec_str))
327
+ dec_part = int(dec_str)
328
+
329
+ multiplier = 10**decimals
330
+ return int_part * multiplier + dec_part
331
+
332
+
333
+ def format_amount(amount: int, decimals: int) -> str:
334
+ """
335
+ Format an amount in smallest units to a decimal string.
336
+
337
+ Args:
338
+ amount: Amount in smallest units
339
+ decimals: Token decimals
340
+
341
+ Returns:
342
+ Decimal string representation
343
+ """
344
+ if amount == 0:
345
+ return "0"
346
+
347
+ divisor = 10**decimals
348
+ quotient = amount // divisor
349
+ remainder = amount % divisor
350
+
351
+ if remainder == 0:
352
+ return str(quotient)
353
+
354
+ dec_str = str(remainder).zfill(decimals).rstrip("0")
355
+ return f"{quotient}.{dec_str}"
356
+
357
+
358
+ def validate_boc(boc_base64: str) -> bool:
359
+ """
360
+ Validate that a string is a valid base64-encoded BOC.
361
+
362
+ Args:
363
+ boc_base64: Base64 encoded BOC string
364
+
365
+ Returns:
366
+ True if valid, False otherwise
367
+ """
368
+ if not boc_base64:
369
+ return False
370
+
371
+ try:
372
+ base64.b64decode(boc_base64)
373
+ return True
374
+ except Exception:
375
+ return False
376
+
377
+
378
+ def is_testnet(network: str) -> bool:
379
+ """
380
+ Check if a network is a testnet.
381
+
382
+ Args:
383
+ network: Network identifier
384
+
385
+ Returns:
386
+ True if testnet
387
+ """
388
+ return network == TON_TESTNET
389
+
390
+
391
+ def prepare_ton_payment_header(
392
+ sender_address: str,
393
+ t402_version: int,
394
+ network: str,
395
+ pay_to: str,
396
+ asset: str,
397
+ amount: str,
398
+ max_timeout_seconds: int = DEFAULT_VALIDITY_DURATION,
399
+ ) -> Dict[str, Any]:
400
+ """
401
+ Prepare an unsigned TON payment header.
402
+
403
+ Args:
404
+ sender_address: Sender's TON address
405
+ t402_version: Protocol version
406
+ network: Network identifier
407
+ pay_to: Recipient address
408
+ asset: Jetton master address
409
+ amount: Amount in smallest units
410
+ max_timeout_seconds: Maximum timeout in seconds
411
+
412
+ Returns:
413
+ Unsigned payment header dictionary
414
+ """
415
+ now = int(time.time())
416
+ valid_until = now + max_timeout_seconds
417
+ seqno = 0 # Will be filled by client
418
+ query_id = str(now * 1000000) # Unique query ID
419
+
420
+ return {
421
+ "t402Version": t402_version,
422
+ "scheme": SCHEME_EXACT,
423
+ "network": network,
424
+ "payload": {
425
+ "signedBoc": None, # Will be filled after signing
426
+ "authorization": {
427
+ "from": sender_address,
428
+ "to": pay_to,
429
+ "jettonMaster": asset,
430
+ "jettonAmount": amount,
431
+ "tonAmount": str(DEFAULT_JETTON_TRANSFER_TON),
432
+ "validUntil": valid_until,
433
+ "seqno": seqno,
434
+ "queryId": query_id,
435
+ },
436
+ },
437
+ }
438
+
439
+
440
+ def get_usdt_address(network: str) -> str:
441
+ """
442
+ Get the USDT Jetton master address for a network.
443
+
444
+ Args:
445
+ network: Network identifier
446
+
447
+ Returns:
448
+ USDT master address
449
+
450
+ Raises:
451
+ ValueError: If network is not supported
452
+ """
453
+ if network == TON_MAINNET:
454
+ return USDT_MAINNET_ADDRESS
455
+ elif network == TON_TESTNET:
456
+ return USDT_TESTNET_ADDRESS
457
+ else:
458
+ raise ValueError(f"Unsupported TON network: {network}")
459
+
460
+
461
+ def get_known_jettons(network: str) -> List[JettonConfig]:
462
+ """
463
+ Get list of known Jettons for a network.
464
+
465
+ Args:
466
+ network: Network identifier
467
+
468
+ Returns:
469
+ List of JettonConfig
470
+ """
471
+ config = get_network_config(network)
472
+ if not config:
473
+ return []
474
+ return list(config["supported_assets"].values())
@@ -0,0 +1,193 @@
1
+ # THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ # TON Paywall Template - A simple placeholder template for TON payments
3
+ # In production, this would be a full React-based paywall like the EVM/SVM templates
4
+
5
+ TON_PAYWALL_TEMPLATE = """<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
+ <title>Payment Required - TON</title>
11
+ <style>
12
+ * { box-sizing: border-box; margin: 0; padding: 0; }
13
+ body {
14
+ min-height: 100vh;
15
+ background-color: #f9fafb;
16
+ font-family: Inter, system-ui, -apple-system, sans-serif;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ }
21
+ .container {
22
+ max-width: 32rem;
23
+ margin: 2rem;
24
+ padding: 2rem;
25
+ background-color: white;
26
+ border-radius: 0.75rem;
27
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
28
+ text-align: center;
29
+ }
30
+ .logo {
31
+ width: 64px;
32
+ height: 64px;
33
+ margin: 0 auto 1rem;
34
+ }
35
+ .title {
36
+ font-size: 1.5rem;
37
+ font-weight: 700;
38
+ color: #111827;
39
+ margin-bottom: 0.5rem;
40
+ }
41
+ .subtitle {
42
+ color: #6b7280;
43
+ margin-bottom: 1.5rem;
44
+ }
45
+ .payment-details {
46
+ background-color: #f3f4f6;
47
+ padding: 1rem;
48
+ border-radius: 0.5rem;
49
+ margin-bottom: 1.5rem;
50
+ }
51
+ .payment-row {
52
+ display: flex;
53
+ justify-content: space-between;
54
+ margin-bottom: 0.5rem;
55
+ }
56
+ .payment-row:last-child { margin-bottom: 0; }
57
+ .payment-label { color: #6b7280; }
58
+ .payment-value { font-weight: 600; color: #111827; }
59
+ .button {
60
+ width: 100%;
61
+ padding: 0.75rem 1rem;
62
+ border-radius: 0.5rem;
63
+ font-weight: 600;
64
+ border: none;
65
+ cursor: pointer;
66
+ background-color: #0098EA;
67
+ color: white;
68
+ font-size: 1rem;
69
+ transition: background-color 0.15s;
70
+ }
71
+ .button:hover { background-color: #0088D4; }
72
+ .button:disabled {
73
+ background-color: #9ca3af;
74
+ cursor: not-allowed;
75
+ }
76
+ .ton-logo {
77
+ fill: #0098EA;
78
+ }
79
+ .status {
80
+ margin-top: 1rem;
81
+ font-size: 0.875rem;
82
+ color: #6b7280;
83
+ }
84
+ .error {
85
+ color: #dc2626;
86
+ background-color: #fef2f2;
87
+ padding: 0.75rem;
88
+ border-radius: 0.5rem;
89
+ margin-bottom: 1rem;
90
+ }
91
+ </style>
92
+ </head>
93
+ <body>
94
+ <div class="container">
95
+ <svg class="logo" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
96
+ <path d="M28 56C43.464 56 56 43.464 56 28C56 12.536 43.464 0 28 0C12.536 0 0 12.536 0 28C0 43.464 12.536 56 28 56Z" fill="#0098EA"/>
97
+ <path d="M37.5603 15.6277H18.4386C14.9228 15.6277 12.6944 19.4202 14.4632 22.4861L26.2644 42.9409C27.0345 44.2765 28.9644 44.2765 29.7345 42.9409L41.5765 22.4861C43.3045 19.4202 41.0761 15.6277 37.5603 15.6277ZM26.2211 36.5583L23.6856 31.0379L17.4565 19.7027C17.0579 18.9891 17.5607 18.0868 18.4386 18.0868H26.2211V36.5583ZM38.5765 19.6635L32.3474 31.0379L29.7711 36.5583V18.0868H37.5603C38.4382 18.0868 38.941 18.9891 38.5765 19.6635Z" fill="white"/>
98
+ </svg>
99
+ <h1 class="title">Payment Required</h1>
100
+ <p class="subtitle">This resource requires a TON payment to access.</p>
101
+
102
+ <div id="error-container"></div>
103
+
104
+ <div class="payment-details">
105
+ <div class="payment-row">
106
+ <span class="payment-label">Amount</span>
107
+ <span class="payment-value" id="amount">Loading...</span>
108
+ </div>
109
+ <div class="payment-row">
110
+ <span class="payment-label">Token</span>
111
+ <span class="payment-value">USDT</span>
112
+ </div>
113
+ <div class="payment-row">
114
+ <span class="payment-label">Network</span>
115
+ <span class="payment-value" id="network">TON</span>
116
+ </div>
117
+ </div>
118
+
119
+ <button class="button" id="connect-btn" onclick="connectWallet()">
120
+ Connect TON Wallet
121
+ </button>
122
+
123
+ <p class="status" id="status"></p>
124
+ </div>
125
+
126
+ <script>
127
+ // Initialize from window.t402 config
128
+ document.addEventListener('DOMContentLoaded', function() {
129
+ if (window.t402) {
130
+ const config = window.t402;
131
+
132
+ // Display amount
133
+ if (config.amount) {
134
+ document.getElementById('amount').textContent = '$' + config.amount.toFixed(2) + ' USDT';
135
+ }
136
+
137
+ // Display network
138
+ if (config.paymentRequirements && config.paymentRequirements[0]) {
139
+ const network = config.paymentRequirements[0].network;
140
+ document.getElementById('network').textContent =
141
+ network === 'ton:testnet' ? 'TON Testnet' : 'TON Mainnet';
142
+ }
143
+
144
+ // Display error if any
145
+ if (config.error) {
146
+ const errorContainer = document.getElementById('error-container');
147
+ errorContainer.innerHTML = '<div class="error">' + config.error + '</div>';
148
+ }
149
+ }
150
+ });
151
+
152
+ async function connectWallet() {
153
+ const btn = document.getElementById('connect-btn');
154
+ const status = document.getElementById('status');
155
+
156
+ btn.disabled = true;
157
+ btn.textContent = 'Connecting...';
158
+ status.textContent = '';
159
+
160
+ try {
161
+ // Check for TON wallet providers
162
+ if (typeof window.tonkeeper !== 'undefined') {
163
+ status.textContent = 'Tonkeeper detected. Please approve the connection.';
164
+ // Tonkeeper integration would go here
165
+ } else if (typeof window.ton !== 'undefined') {
166
+ status.textContent = 'TON wallet detected. Please approve the connection.';
167
+ // Generic TON wallet integration would go here
168
+ } else {
169
+ status.textContent = 'No TON wallet detected. Please install Tonkeeper or another TON wallet.';
170
+ btn.textContent = 'Install Wallet';
171
+ btn.onclick = function() {
172
+ window.open('https://tonkeeper.com/', '_blank');
173
+ };
174
+ btn.disabled = false;
175
+ return;
176
+ }
177
+
178
+ // In a full implementation, this would:
179
+ // 1. Connect to the wallet
180
+ // 2. Build the Jetton transfer message
181
+ // 3. Sign the message
182
+ // 4. Create the payment payload
183
+ // 5. Retry the original request with the payment header
184
+
185
+ } catch (error) {
186
+ status.textContent = 'Error: ' + error.message;
187
+ btn.disabled = false;
188
+ btn.textContent = 'Connect TON Wallet';
189
+ }
190
+ }
191
+ </script>
192
+ </body>
193
+ </html>"""
t402/types.py CHANGED
@@ -164,6 +164,46 @@ class EIP3009Authorization(BaseModel):
164
164
  return v
165
165
 
166
166
 
167
+ class TonAuthorization(BaseModel):
168
+ """TON Jetton transfer authorization metadata."""
169
+
170
+ from_: str = Field(alias="from")
171
+ to: str
172
+ jetton_master: str = Field(alias="jettonMaster")
173
+ jetton_amount: str = Field(alias="jettonAmount")
174
+ ton_amount: str = Field(alias="tonAmount")
175
+ valid_until: int = Field(alias="validUntil")
176
+ seqno: int
177
+ query_id: str = Field(alias="queryId")
178
+
179
+ model_config = ConfigDict(
180
+ alias_generator=to_camel,
181
+ populate_by_name=True,
182
+ from_attributes=True,
183
+ )
184
+
185
+ @field_validator("jetton_amount", "ton_amount")
186
+ def validate_amount(cls, v):
187
+ try:
188
+ int(v)
189
+ except ValueError:
190
+ raise ValueError("amount must be an integer encoded as a string")
191
+ return v
192
+
193
+
194
+ class TonPaymentPayload(BaseModel):
195
+ """TON payment payload containing signed BOC and authorization."""
196
+
197
+ signed_boc: str = Field(alias="signedBoc")
198
+ authorization: TonAuthorization
199
+
200
+ model_config = ConfigDict(
201
+ alias_generator=to_camel,
202
+ populate_by_name=True,
203
+ from_attributes=True,
204
+ )
205
+
206
+
167
207
  class VerifyResponse(BaseModel):
168
208
  is_valid: bool = Field(alias="isValid")
169
209
  invalid_reason: Optional[str] = Field(None, alias="invalidReason")
@@ -191,7 +231,7 @@ class SettleResponse(BaseModel):
191
231
 
192
232
 
193
233
  # Union of payloads for each scheme
194
- SchemePayloads = ExactPaymentPayload
234
+ SchemePayloads = Union[ExactPaymentPayload, TonPaymentPayload]
195
235
 
196
236
 
197
237
  class PaymentPayload(BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t402
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: t402: An internet native payments protocol
5
5
  Author-email: T402 Team <dev@t402.io>
6
6
  License: Apache-2.0
@@ -25,6 +25,9 @@ Python package for the t402 payments protocol.
25
25
 
26
26
  ```bash
27
27
  pip install t402
28
+
29
+ # or with uv
30
+ uv add t402
28
31
  ```
29
32
 
30
33
  ## Overview
@@ -1,24 +1,26 @@
1
- t402/__init__.py,sha256=03t8aR8QJjUbmLDgBWyjMRnxwiOtQsDE0i0uV2v3zw0,50
1
+ t402/__init__.py,sha256=UCMjld6jAiYAeUqwxDa-SQFaE7w_AICzBTjpEzZuFBY,2250
2
2
  t402/chains.py,sha256=Hyn3bfubNpiWE1fqkyJulkonzMu3lncSt-_RCgagdro,2832
3
- t402/common.py,sha256=rSKXLzDBck_fYhH67VHd9DYoNYH9Ftw-VFRmNSXwQy8,3569
3
+ t402/common.py,sha256=7wy6GL3XRv07KDqVxqpfMTVtMsSaz27anlQJzhqHCNs,4775
4
4
  t402/encoding.py,sha256=n-D5CevbJI7y0O_9OASOinB1vigYOOSU9RsWWSixMTk,633
5
5
  t402/evm_paywall_template.py,sha256=VJp3bntjRmz5ocWFCtzeB9842ds1Kd8as8rP48GhaDI,1928580
6
6
  t402/exact.py,sha256=ZC2CqwBYKv5WzhTpNWtcUPeAItDvatyrMNV47nYumE8,4448
7
7
  t402/facilitator.py,sha256=yfrWC5cdvaO1ZRQ5Y-2PMfTddsdahO-3MoKlvo3Xkbg,4792
8
- t402/networks.py,sha256=W8b4Q8fPdSDNhrnmIZDL5fVYYMZM69eM0_YrDIEnoNI,241
8
+ t402/networks.py,sha256=Qq0dbPcTOrM9kkxm-ARbQGF7j21nWNytkxdyjb8iZF8,1491
9
9
  t402/path.py,sha256=G3oxm12FS1OsxXK_BPopS4bVCFrei5MOMnQAvDbgZsY,1311
10
- t402/paywall.py,sha256=CtEQz6hlwQLGFJt0iztpJDuIQ6jqveId-1ZWuMqIpZ8,3989
10
+ t402/paywall.py,sha256=lSpGlDBC4slPU5rw7aDq5iBrBJXxLHRe3lgddFMKZ-Q,4119
11
11
  t402/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  t402/svm_paywall_template.py,sha256=pmueO0Qgcwl-uyci8lVI1eiery2vggHGl_b1tnxcB_E,827953
13
- t402/types.py,sha256=0sDiro29HcBjDBX9B1LYlM1sJV9PfHoUvmUuderwQm4,7027
13
+ t402/ton.py,sha256=ggh_6nvZ55c-iYe6bNVejPJIWoFRSf85-DQmsvu-auI,11919
14
+ t402/ton_paywall_template.py,sha256=kRddMyZk51dDd1b4-r_MlSGCc-DLRHskIxA-LN9l9vw,7343
15
+ t402/types.py,sha256=EjvOHPUCtTKwwd57sQMmBSzcSegGOg81xS7u4AsWSrw,8153
14
16
  t402/clients/__init__.py,sha256=AuVY9soEliECRQJhcSor4CeB8Z09aSptRSo79mHPNdQ,433
15
17
  t402/clients/base.py,sha256=Ha6ivbmE62LoRFMfSMzDv3dwA9V3Qr5TD5w5zft5EW0,6448
16
18
  t402/clients/httpx.py,sha256=v7q6ZJPCz_SIqRYbTDPbbMsCNnt1oax5uKvCDUsvaX0,4650
17
19
  t402/clients/requests.py,sha256=T4nrrXxQQ02kE7mpcDS0co4OqtrxUg7jZPVztk-mgXc,4934
18
20
  t402/fastapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- t402/fastapi/middleware.py,sha256=wzLOx-c3TgE5X5oW8IZ4AF7v4XdIDPyKEtsx_D4nutE,8598
21
+ t402/fastapi/middleware.py,sha256=Lxt7hOGEMg9wkAnakbb3wtNSv9r6KTk6_HVG1d4buIs,8638
20
22
  t402/flask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- t402/flask/middleware.py,sha256=eRzbFIjjjFpfIv6vjqYAeUtLO8O7qRx86XOEt7lEkPk,14070
22
- t402-1.0.0.dist-info/METADATA,sha256=2DuGnNicK35AX6s7KjX37BCkLDAJibx0loKpm-0UekY,6469
23
- t402-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
- t402-1.0.0.dist-info/RECORD,,
23
+ t402/flask/middleware.py,sha256=7xh7p7SceJHDgZK5s-hVT8lbdsIaO0CaSpPtqnKpp-4,14110
24
+ t402-1.1.0.dist-info/METADATA,sha256=mW403K3Fv8uyz2GoJOgI6ySpoHD5nLRHI5AVNXSbsQY,6495
25
+ t402-1.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
+ t402-1.1.0.dist-info/RECORD,,
File without changes