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 +96 -0
- t402/common.py +35 -5
- t402/fastapi/middleware.py +3 -3
- t402/flask/middleware.py +3 -3
- t402/networks.py +49 -2
- t402/paywall.py +3 -0
- t402/ton.py +474 -0
- t402/ton_paywall_template.py +193 -0
- t402/types.py +41 -1
- {t402-1.0.0.dist-info → t402-1.1.0.dist-info}/METADATA +4 -1
- {t402-1.0.0.dist-info → t402-1.1.0.dist-info}/RECORD +12 -10
- {t402-1.0.0.dist-info → t402-1.1.0.dist-info}/WHEEL +0 -0
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
|
-
|
|
26
|
-
|
|
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,
|
|
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
|
|
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
|
-
#
|
|
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)
|
t402/fastapi/middleware.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Any, Callable, Optional,
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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=
|
|
1
|
+
t402/__init__.py,sha256=UCMjld6jAiYAeUqwxDa-SQFaE7w_AICzBTjpEzZuFBY,2250
|
|
2
2
|
t402/chains.py,sha256=Hyn3bfubNpiWE1fqkyJulkonzMu3lncSt-_RCgagdro,2832
|
|
3
|
-
t402/common.py,sha256=
|
|
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=
|
|
8
|
+
t402/networks.py,sha256=Qq0dbPcTOrM9kkxm-ARbQGF7j21nWNytkxdyjb8iZF8,1491
|
|
9
9
|
t402/path.py,sha256=G3oxm12FS1OsxXK_BPopS4bVCFrei5MOMnQAvDbgZsY,1311
|
|
10
|
-
t402/paywall.py,sha256=
|
|
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/
|
|
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=
|
|
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=
|
|
22
|
-
t402-1.
|
|
23
|
-
t402-1.
|
|
24
|
-
t402-1.
|
|
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
|