t402 1.6.1__py3-none-any.whl → 1.7.1__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 +169 -2
- t402/common.py +37 -8
- t402/encoding.py +298 -1
- t402/facilitator.py +1 -1
- t402/fastapi/__init__.py +79 -0
- t402/fastapi/dependencies.py +398 -0
- t402/fastapi/middleware.py +665 -130
- t402/schemes/__init__.py +125 -0
- t402/schemes/evm/__init__.py +25 -0
- t402/schemes/evm/exact/__init__.py +29 -0
- t402/schemes/evm/exact/client.py +265 -0
- t402/schemes/evm/exact/server.py +181 -0
- t402/schemes/interfaces.py +401 -0
- t402/schemes/registry.py +477 -0
- t402/schemes/ton/__init__.py +22 -0
- t402/schemes/ton/exact/__init__.py +27 -0
- t402/schemes/ton/exact/client.py +343 -0
- t402/schemes/ton/exact/server.py +201 -0
- t402/schemes/tron/__init__.py +22 -0
- t402/schemes/tron/exact/__init__.py +27 -0
- t402/schemes/tron/exact/client.py +260 -0
- t402/schemes/tron/exact/server.py +192 -0
- t402/types.py +178 -8
- {t402-1.6.1.dist-info → t402-1.7.1.dist-info}/METADATA +1 -1
- {t402-1.6.1.dist-info → t402-1.7.1.dist-info}/RECORD +27 -11
- {t402-1.6.1.dist-info → t402-1.7.1.dist-info}/WHEEL +0 -0
- {t402-1.6.1.dist-info → t402-1.7.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""TRON Exact Scheme - Client Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the client-side implementation of the exact payment scheme
|
|
4
|
+
for TRON network using TRC-20 token transfers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict, Optional, Protocol, Union
|
|
11
|
+
|
|
12
|
+
from t402.types import (
|
|
13
|
+
PaymentRequirementsV2,
|
|
14
|
+
T402_VERSION_V1,
|
|
15
|
+
T402_VERSION_V2,
|
|
16
|
+
)
|
|
17
|
+
from t402.tron import (
|
|
18
|
+
TronAuthorization,
|
|
19
|
+
TronPaymentPayload,
|
|
20
|
+
DEFAULT_FEE_LIMIT,
|
|
21
|
+
validate_tron_address,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Constants
|
|
26
|
+
SCHEME_EXACT = "exact"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BlockInfo(Protocol):
|
|
30
|
+
"""Protocol for TRON block reference info."""
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def ref_block_bytes(self) -> str:
|
|
34
|
+
"""Reference block bytes (4 bytes hex)."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def ref_block_hash(self) -> str:
|
|
39
|
+
"""Reference block hash (8 bytes hex)."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def expiration(self) -> int:
|
|
44
|
+
"""Transaction expiration timestamp in milliseconds."""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TronSigner(Protocol):
|
|
49
|
+
"""Protocol for TRON wallet signing operations.
|
|
50
|
+
|
|
51
|
+
Implementations should provide wallet address, block info retrieval,
|
|
52
|
+
and transaction signing capabilities.
|
|
53
|
+
|
|
54
|
+
Example implementation with tronpy:
|
|
55
|
+
```python
|
|
56
|
+
class MyTronSigner:
|
|
57
|
+
def __init__(self, private_key, client):
|
|
58
|
+
self._private_key = private_key
|
|
59
|
+
self._client = client
|
|
60
|
+
self._address = private_key_to_address(private_key)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def address(self) -> str:
|
|
64
|
+
return self._address
|
|
65
|
+
|
|
66
|
+
async def get_block_info(self) -> BlockInfo:
|
|
67
|
+
block = await self._client.get_latest_block()
|
|
68
|
+
return {
|
|
69
|
+
"ref_block_bytes": block.ref_block_bytes,
|
|
70
|
+
"ref_block_hash": block.ref_block_hash,
|
|
71
|
+
"expiration": int(time.time() * 1000) + 3600000,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async def sign_transaction(
|
|
75
|
+
self,
|
|
76
|
+
contract_address: str,
|
|
77
|
+
to: str,
|
|
78
|
+
amount: str,
|
|
79
|
+
fee_limit: int,
|
|
80
|
+
expiration: int,
|
|
81
|
+
) -> str:
|
|
82
|
+
# Build and sign TRC-20 transfer transaction
|
|
83
|
+
return signed_transaction_hex
|
|
84
|
+
```
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def address(self) -> str:
|
|
89
|
+
"""Return the wallet address (T-prefix base58check)."""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
async def get_block_info(self) -> Dict[str, Any]:
|
|
93
|
+
"""Get the current reference block info for transaction building.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dict with ref_block_bytes, ref_block_hash, and expiration
|
|
97
|
+
"""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
async def sign_transaction(
|
|
101
|
+
self,
|
|
102
|
+
contract_address: str,
|
|
103
|
+
to: str,
|
|
104
|
+
amount: str,
|
|
105
|
+
fee_limit: int,
|
|
106
|
+
expiration: int,
|
|
107
|
+
) -> str:
|
|
108
|
+
"""Sign a TRC-20 transfer transaction.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
contract_address: TRC-20 contract address
|
|
112
|
+
to: Recipient address
|
|
113
|
+
amount: Amount in smallest units
|
|
114
|
+
fee_limit: Fee limit in SUN
|
|
115
|
+
expiration: Transaction expiration in milliseconds
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Hex-encoded signed transaction
|
|
119
|
+
"""
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ExactTronClientScheme:
|
|
124
|
+
"""Client scheme for TRON exact payments using TRC-20 transfers.
|
|
125
|
+
|
|
126
|
+
Creates signed TRC-20 transfer transactions that can be verified and
|
|
127
|
+
broadcast by a facilitator to complete the payment.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
```python
|
|
131
|
+
scheme = ExactTronClientScheme(signer=my_tron_signer)
|
|
132
|
+
|
|
133
|
+
payload = await scheme.create_payment_payload(
|
|
134
|
+
t402_version=2,
|
|
135
|
+
requirements={
|
|
136
|
+
"scheme": "exact",
|
|
137
|
+
"network": "tron:mainnet",
|
|
138
|
+
"asset": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
139
|
+
"amount": "1000000",
|
|
140
|
+
"payTo": "TPayToAddress...",
|
|
141
|
+
"maxTimeoutSeconds": 300,
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
scheme = SCHEME_EXACT
|
|
148
|
+
caip_family = "tron:*"
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
signer: TronSigner,
|
|
153
|
+
fee_limit: Optional[int] = None,
|
|
154
|
+
):
|
|
155
|
+
"""Initialize the TRON client scheme.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
signer: TRON signer for signing transactions
|
|
159
|
+
fee_limit: Override fee limit in SUN (default: 100 TRX)
|
|
160
|
+
"""
|
|
161
|
+
self._signer = signer
|
|
162
|
+
self._fee_limit = fee_limit or DEFAULT_FEE_LIMIT
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def address(self) -> str:
|
|
166
|
+
"""Return the wallet address."""
|
|
167
|
+
return self._signer.address
|
|
168
|
+
|
|
169
|
+
async def create_payment_payload(
|
|
170
|
+
self,
|
|
171
|
+
t402_version: int,
|
|
172
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
173
|
+
) -> Dict[str, Any]:
|
|
174
|
+
"""Create a payment payload for TRC-20 transfer.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
t402_version: Protocol version (1 or 2)
|
|
178
|
+
requirements: Payment requirements
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Payment payload with signed transaction and authorization metadata
|
|
182
|
+
"""
|
|
183
|
+
# Convert to dict for easier access
|
|
184
|
+
if hasattr(requirements, "model_dump"):
|
|
185
|
+
req = requirements.model_dump(by_alias=True)
|
|
186
|
+
else:
|
|
187
|
+
req = dict(requirements)
|
|
188
|
+
|
|
189
|
+
# Extract fields
|
|
190
|
+
network = req.get("network", "")
|
|
191
|
+
asset = req.get("asset", "")
|
|
192
|
+
amount = req.get("amount", "0")
|
|
193
|
+
pay_to = req.get("payTo", "")
|
|
194
|
+
max_timeout = req.get("maxTimeoutSeconds", 300)
|
|
195
|
+
|
|
196
|
+
# Validate required fields
|
|
197
|
+
if not asset:
|
|
198
|
+
raise ValueError("Asset (TRC-20 contract address) is required")
|
|
199
|
+
if not pay_to:
|
|
200
|
+
raise ValueError("PayTo address is required")
|
|
201
|
+
if not amount:
|
|
202
|
+
raise ValueError("Amount is required")
|
|
203
|
+
|
|
204
|
+
# Validate addresses
|
|
205
|
+
if not validate_tron_address(asset):
|
|
206
|
+
raise ValueError(f"Invalid TRC-20 contract address: {asset}")
|
|
207
|
+
if not validate_tron_address(pay_to):
|
|
208
|
+
raise ValueError(f"Invalid payTo address: {pay_to}")
|
|
209
|
+
if not validate_tron_address(self._signer.address):
|
|
210
|
+
raise ValueError(f"Invalid signer address: {self._signer.address}")
|
|
211
|
+
|
|
212
|
+
# Get block info for transaction
|
|
213
|
+
block_info = await self._signer.get_block_info()
|
|
214
|
+
ref_block_bytes = block_info.get("ref_block_bytes", "")
|
|
215
|
+
ref_block_hash = block_info.get("ref_block_hash", "")
|
|
216
|
+
|
|
217
|
+
# Calculate expiration
|
|
218
|
+
now_ms = int(time.time() * 1000)
|
|
219
|
+
expiration = block_info.get("expiration") or (now_ms + max_timeout * 1000)
|
|
220
|
+
|
|
221
|
+
# Sign the transaction
|
|
222
|
+
signed_transaction = await self._signer.sign_transaction(
|
|
223
|
+
contract_address=asset,
|
|
224
|
+
to=pay_to,
|
|
225
|
+
amount=amount,
|
|
226
|
+
fee_limit=self._fee_limit,
|
|
227
|
+
expiration=expiration,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Build authorization metadata
|
|
231
|
+
authorization = TronAuthorization(
|
|
232
|
+
from_=self._signer.address,
|
|
233
|
+
to=pay_to,
|
|
234
|
+
contract_address=asset,
|
|
235
|
+
amount=amount,
|
|
236
|
+
expiration=expiration,
|
|
237
|
+
ref_block_bytes=ref_block_bytes,
|
|
238
|
+
ref_block_hash=ref_block_hash,
|
|
239
|
+
timestamp=now_ms,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Build payload
|
|
243
|
+
payload_data = TronPaymentPayload(
|
|
244
|
+
signed_transaction=signed_transaction,
|
|
245
|
+
authorization=authorization,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if t402_version == T402_VERSION_V1:
|
|
249
|
+
return {
|
|
250
|
+
"t402Version": T402_VERSION_V1,
|
|
251
|
+
"scheme": self.scheme,
|
|
252
|
+
"network": network,
|
|
253
|
+
"payload": payload_data.model_dump(by_alias=True),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# V2 format
|
|
257
|
+
return {
|
|
258
|
+
"t402Version": T402_VERSION_V2,
|
|
259
|
+
"payload": payload_data.model_dump(by_alias=True),
|
|
260
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""TRON Exact Scheme - Server Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the server-side implementation of the exact payment scheme
|
|
4
|
+
for TRON network.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from typing import Any, Dict, List, Union
|
|
11
|
+
|
|
12
|
+
from t402.types import (
|
|
13
|
+
PaymentRequirementsV2,
|
|
14
|
+
Network,
|
|
15
|
+
)
|
|
16
|
+
from t402.schemes.interfaces import AssetAmount, SupportedKindDict
|
|
17
|
+
from t402.tron import (
|
|
18
|
+
SCHEME_EXACT,
|
|
19
|
+
DEFAULT_DECIMALS,
|
|
20
|
+
get_network_config,
|
|
21
|
+
get_default_asset,
|
|
22
|
+
get_asset_info,
|
|
23
|
+
normalize_network as tron_normalize_network,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ExactTronServerScheme:
|
|
28
|
+
"""Server scheme for TRON exact payments.
|
|
29
|
+
|
|
30
|
+
Handles parsing user-friendly prices and enhancing payment requirements
|
|
31
|
+
with TRON-specific metadata for clients.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
```python
|
|
35
|
+
scheme = ExactTronServerScheme()
|
|
36
|
+
|
|
37
|
+
# Parse price
|
|
38
|
+
asset_amount = await scheme.parse_price("$0.10", "tron:mainnet")
|
|
39
|
+
# Returns: {"amount": "100000", "asset": "TR7N...", "extra": {...}}
|
|
40
|
+
|
|
41
|
+
# Enhance requirements
|
|
42
|
+
enhanced = await scheme.enhance_requirements(
|
|
43
|
+
requirements,
|
|
44
|
+
supported_kind,
|
|
45
|
+
facilitator_extensions,
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
scheme = SCHEME_EXACT
|
|
51
|
+
caip_family = "tron:*"
|
|
52
|
+
|
|
53
|
+
async def parse_price(
|
|
54
|
+
self,
|
|
55
|
+
price: Union[str, int, float, Dict[str, Any]],
|
|
56
|
+
network: Network,
|
|
57
|
+
) -> AssetAmount:
|
|
58
|
+
"""Parse a user-friendly price to atomic amount and asset.
|
|
59
|
+
|
|
60
|
+
Supports:
|
|
61
|
+
- String with $ prefix: "$0.10" -> 100000 (6 decimals)
|
|
62
|
+
- String without prefix: "0.10" -> 100000
|
|
63
|
+
- Integer/float: 0.10 -> 100000
|
|
64
|
+
- Dict (TokenAmount): {"amount": "100000", "asset": "TR7N..."}
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
price: User-friendly price
|
|
68
|
+
network: Network identifier (CAIP-2 format, e.g., "tron:mainnet")
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
AssetAmount dict with amount, asset, and extra metadata
|
|
72
|
+
"""
|
|
73
|
+
# Normalize network
|
|
74
|
+
network_str = self._normalize_network(network)
|
|
75
|
+
|
|
76
|
+
# Handle dict (already in TokenAmount format)
|
|
77
|
+
if isinstance(price, dict):
|
|
78
|
+
return {
|
|
79
|
+
"amount": str(price.get("amount", "0")),
|
|
80
|
+
"asset": price.get("asset", ""),
|
|
81
|
+
"extra": price.get("extra", {}),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Get default asset (USDT) for the network
|
|
85
|
+
default_asset = get_default_asset(network_str)
|
|
86
|
+
if not default_asset:
|
|
87
|
+
raise ValueError(f"Unsupported TRON network: {network}")
|
|
88
|
+
|
|
89
|
+
asset_address = default_asset["contract_address"]
|
|
90
|
+
decimals = default_asset.get("decimals", DEFAULT_DECIMALS)
|
|
91
|
+
|
|
92
|
+
# Parse price string/number
|
|
93
|
+
if isinstance(price, str):
|
|
94
|
+
if price.startswith("$"):
|
|
95
|
+
price = price[1:]
|
|
96
|
+
amount_decimal = Decimal(price)
|
|
97
|
+
else:
|
|
98
|
+
amount_decimal = Decimal(str(price))
|
|
99
|
+
|
|
100
|
+
# Convert to atomic units
|
|
101
|
+
atomic_amount = int(amount_decimal * Decimal(10 ** decimals))
|
|
102
|
+
|
|
103
|
+
# Build extra metadata
|
|
104
|
+
extra = {
|
|
105
|
+
"symbol": default_asset.get("symbol", "USDT"),
|
|
106
|
+
"name": default_asset.get("name", "Tether USD"),
|
|
107
|
+
"decimals": decimals,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"amount": str(atomic_amount),
|
|
112
|
+
"asset": asset_address,
|
|
113
|
+
"extra": extra,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async def enhance_requirements(
|
|
117
|
+
self,
|
|
118
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
119
|
+
supported_kind: SupportedKindDict,
|
|
120
|
+
facilitator_extensions: List[str],
|
|
121
|
+
) -> Union[PaymentRequirementsV2, Dict[str, Any]]:
|
|
122
|
+
"""Enhance payment requirements with TRON-specific metadata.
|
|
123
|
+
|
|
124
|
+
Adds TRC-20 token metadata to the extra field so clients can
|
|
125
|
+
properly build the transfer transaction.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
requirements: Base payment requirements
|
|
129
|
+
supported_kind: Matched SupportedKind from facilitator
|
|
130
|
+
facilitator_extensions: Extensions supported by facilitator
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Enhanced requirements with TRON metadata in extra
|
|
134
|
+
"""
|
|
135
|
+
# Convert to dict for modification
|
|
136
|
+
if hasattr(requirements, "model_dump"):
|
|
137
|
+
req = requirements.model_dump(by_alias=True)
|
|
138
|
+
else:
|
|
139
|
+
req = dict(requirements)
|
|
140
|
+
|
|
141
|
+
network = req.get("network", "")
|
|
142
|
+
asset = req.get("asset", "")
|
|
143
|
+
|
|
144
|
+
# Normalize network
|
|
145
|
+
network_str = self._normalize_network(network)
|
|
146
|
+
|
|
147
|
+
# Ensure extra exists
|
|
148
|
+
if "extra" not in req or req["extra"] is None:
|
|
149
|
+
req["extra"] = {}
|
|
150
|
+
|
|
151
|
+
# Add TRC-20 metadata if not present
|
|
152
|
+
asset_info = get_asset_info(network_str, asset)
|
|
153
|
+
if asset_info:
|
|
154
|
+
if "symbol" not in req["extra"]:
|
|
155
|
+
req["extra"]["symbol"] = asset_info.get("symbol", "UNKNOWN")
|
|
156
|
+
if "name" not in req["extra"]:
|
|
157
|
+
req["extra"]["name"] = asset_info.get("name", "Unknown TRC20")
|
|
158
|
+
if "decimals" not in req["extra"]:
|
|
159
|
+
req["extra"]["decimals"] = asset_info.get("decimals", DEFAULT_DECIMALS)
|
|
160
|
+
|
|
161
|
+
# Add network config info
|
|
162
|
+
network_config = get_network_config(network_str)
|
|
163
|
+
if network_config:
|
|
164
|
+
if "endpoint" not in req["extra"]:
|
|
165
|
+
req["extra"]["endpoint"] = network_config.get("endpoint", "")
|
|
166
|
+
|
|
167
|
+
# Add facilitator extra data if available
|
|
168
|
+
if supported_kind.get("extra"):
|
|
169
|
+
for key, value in supported_kind["extra"].items():
|
|
170
|
+
if key not in req["extra"]:
|
|
171
|
+
req["extra"][key] = value
|
|
172
|
+
|
|
173
|
+
return req
|
|
174
|
+
|
|
175
|
+
def _normalize_network(self, network: str) -> str:
|
|
176
|
+
"""Normalize network identifier to CAIP-2 format.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
network: Network identifier
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Normalized CAIP-2 network string
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
ValueError: If network is not supported
|
|
186
|
+
"""
|
|
187
|
+
# Use the tron module's normalize function
|
|
188
|
+
try:
|
|
189
|
+
return tron_normalize_network(network)
|
|
190
|
+
except ValueError:
|
|
191
|
+
# Re-raise with consistent error message
|
|
192
|
+
raise ValueError(f"Unknown TRON network: {network}")
|
t402/types.py
CHANGED
|
@@ -12,6 +12,14 @@ from pydantic.alias_generators import to_camel
|
|
|
12
12
|
|
|
13
13
|
from t402.networks import SupportedNetworks
|
|
14
14
|
|
|
15
|
+
# Protocol version constants
|
|
16
|
+
T402_VERSION_V1 = 1
|
|
17
|
+
T402_VERSION_V2 = 2
|
|
18
|
+
T402_VERSION = T402_VERSION_V2 # Current default version
|
|
19
|
+
|
|
20
|
+
# Network type alias (CAIP-2 format: "namespace:reference")
|
|
21
|
+
Network = str # e.g., "eip155:1", "solana:mainnet", "ton:mainnet"
|
|
22
|
+
|
|
15
23
|
|
|
16
24
|
# Add HTTP request structure types
|
|
17
25
|
class HTTPVerbs(str, Enum):
|
|
@@ -93,7 +101,14 @@ Money = Union[str, int] # e.g., "$0.01", 0.01, "0.001"
|
|
|
93
101
|
Price = Union[Money, TokenAmount]
|
|
94
102
|
|
|
95
103
|
|
|
96
|
-
|
|
104
|
+
# =============================================================================
|
|
105
|
+
# V1 Types (Legacy - for backward compatibility)
|
|
106
|
+
# =============================================================================
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PaymentRequirementsV1(BaseModel):
|
|
110
|
+
"""V1 Payment Requirements - Legacy format."""
|
|
111
|
+
|
|
97
112
|
scheme: str
|
|
98
113
|
network: SupportedNetworks
|
|
99
114
|
max_amount_required: str
|
|
@@ -123,10 +138,15 @@ class PaymentRequirements(BaseModel):
|
|
|
123
138
|
return v
|
|
124
139
|
|
|
125
140
|
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
# Alias for backward compatibility
|
|
142
|
+
PaymentRequirements = PaymentRequirementsV1
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class t402PaymentRequiredResponseV1(BaseModel):
|
|
146
|
+
"""V1 Payment Required Response - Legacy format (returned in response body)."""
|
|
147
|
+
|
|
148
|
+
t402_version: int = Field(default=T402_VERSION_V1, alias="t402Version")
|
|
149
|
+
accepts: list[PaymentRequirementsV1]
|
|
130
150
|
error: str
|
|
131
151
|
|
|
132
152
|
model_config = ConfigDict(
|
|
@@ -136,6 +156,80 @@ class t402PaymentRequiredResponse(BaseModel):
|
|
|
136
156
|
)
|
|
137
157
|
|
|
138
158
|
|
|
159
|
+
# Alias for backward compatibility
|
|
160
|
+
t402PaymentRequiredResponse = t402PaymentRequiredResponseV1
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# =============================================================================
|
|
164
|
+
# V2 Types (Current Protocol Version)
|
|
165
|
+
# =============================================================================
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ResourceInfo(BaseModel):
|
|
169
|
+
"""Resource information for V2 protocol.
|
|
170
|
+
|
|
171
|
+
Contains metadata about the protected resource.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
url: str
|
|
175
|
+
description: str = ""
|
|
176
|
+
mime_type: str = Field(default="", alias="mimeType")
|
|
177
|
+
|
|
178
|
+
model_config = ConfigDict(
|
|
179
|
+
alias_generator=to_camel,
|
|
180
|
+
populate_by_name=True,
|
|
181
|
+
from_attributes=True,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class PaymentRequirementsV2(BaseModel):
|
|
186
|
+
"""V2 Payment Requirements - Current format.
|
|
187
|
+
|
|
188
|
+
Represents a single payment option that a client can use to pay for access.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
scheme: str
|
|
192
|
+
network: Network
|
|
193
|
+
asset: str
|
|
194
|
+
amount: str
|
|
195
|
+
pay_to: str = Field(alias="payTo")
|
|
196
|
+
max_timeout_seconds: int = Field(alias="maxTimeoutSeconds")
|
|
197
|
+
extra: dict[str, Any] = Field(default_factory=dict)
|
|
198
|
+
|
|
199
|
+
model_config = ConfigDict(
|
|
200
|
+
alias_generator=to_camel,
|
|
201
|
+
populate_by_name=True,
|
|
202
|
+
from_attributes=True,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@field_validator("amount")
|
|
206
|
+
def validate_amount(cls, v):
|
|
207
|
+
try:
|
|
208
|
+
int(v)
|
|
209
|
+
except ValueError:
|
|
210
|
+
raise ValueError("amount must be an integer encoded as a string")
|
|
211
|
+
return v
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class PaymentRequiredV2(BaseModel):
|
|
215
|
+
"""V2 Payment Required Response - Current format.
|
|
216
|
+
|
|
217
|
+
Returned in the PAYMENT-REQUIRED header as base64-encoded JSON.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
t402_version: int = Field(default=T402_VERSION_V2, alias="t402Version")
|
|
221
|
+
resource: ResourceInfo
|
|
222
|
+
accepts: list[PaymentRequirementsV2]
|
|
223
|
+
error: Optional[str] = None
|
|
224
|
+
extensions: Optional[dict[str, Any]] = None
|
|
225
|
+
|
|
226
|
+
model_config = ConfigDict(
|
|
227
|
+
alias_generator=to_camel,
|
|
228
|
+
populate_by_name=True,
|
|
229
|
+
from_attributes=True,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
139
233
|
class ExactPaymentPayload(BaseModel):
|
|
140
234
|
signature: str
|
|
141
235
|
authorization: EIP3009Authorization
|
|
@@ -258,7 +352,7 @@ class VerifyResponse(BaseModel):
|
|
|
258
352
|
|
|
259
353
|
class SettleResponse(BaseModel):
|
|
260
354
|
success: bool
|
|
261
|
-
error_reason: Optional[str] = None
|
|
355
|
+
error_reason: Optional[str] = Field(None, alias="errorReason")
|
|
262
356
|
transaction: Optional[str] = None
|
|
263
357
|
network: Optional[str] = None
|
|
264
358
|
payer: Optional[str] = None
|
|
@@ -270,12 +364,65 @@ class SettleResponse(BaseModel):
|
|
|
270
364
|
)
|
|
271
365
|
|
|
272
366
|
|
|
367
|
+
class PaymentResponseV2(BaseModel):
|
|
368
|
+
"""V2 Payment Response - returned in PAYMENT-RESPONSE header after settlement."""
|
|
369
|
+
|
|
370
|
+
success: bool
|
|
371
|
+
error_reason: Optional[str] = Field(None, alias="errorReason")
|
|
372
|
+
payer: Optional[str] = None
|
|
373
|
+
transaction: str
|
|
374
|
+
network: Network
|
|
375
|
+
requirements: PaymentRequirementsV2
|
|
376
|
+
|
|
377
|
+
model_config = ConfigDict(
|
|
378
|
+
alias_generator=to_camel,
|
|
379
|
+
populate_by_name=True,
|
|
380
|
+
from_attributes=True,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# =============================================================================
|
|
385
|
+
# Facilitator Types
|
|
386
|
+
# =============================================================================
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class SupportedKind(BaseModel):
|
|
390
|
+
"""A single supported scheme/network combination from the facilitator."""
|
|
391
|
+
|
|
392
|
+
t402_version: int = Field(alias="t402Version")
|
|
393
|
+
scheme: str
|
|
394
|
+
network: Network
|
|
395
|
+
extra: Optional[dict[str, Any]] = None
|
|
396
|
+
|
|
397
|
+
model_config = ConfigDict(
|
|
398
|
+
alias_generator=to_camel,
|
|
399
|
+
populate_by_name=True,
|
|
400
|
+
from_attributes=True,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class SupportedResponse(BaseModel):
|
|
405
|
+
"""Response from facilitator's /supported endpoint."""
|
|
406
|
+
|
|
407
|
+
kinds: list[SupportedKind]
|
|
408
|
+
extensions: list[str] = Field(default_factory=list)
|
|
409
|
+
signers: dict[str, list[str]] = Field(default_factory=dict) # CAIP family → addresses
|
|
410
|
+
|
|
411
|
+
model_config = ConfigDict(
|
|
412
|
+
alias_generator=to_camel,
|
|
413
|
+
populate_by_name=True,
|
|
414
|
+
from_attributes=True,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
273
418
|
# Union of payloads for each scheme
|
|
274
419
|
SchemePayloads = Union[ExactPaymentPayload, TonPaymentPayload, TronPaymentPayload]
|
|
275
420
|
|
|
276
421
|
|
|
277
|
-
class
|
|
278
|
-
|
|
422
|
+
class PaymentPayloadV1(BaseModel):
|
|
423
|
+
"""V1 Payment Payload - Legacy format."""
|
|
424
|
+
|
|
425
|
+
t402_version: int = Field(default=T402_VERSION_V1, alias="t402Version")
|
|
279
426
|
scheme: str
|
|
280
427
|
network: str
|
|
281
428
|
payload: SchemePayloads
|
|
@@ -287,6 +434,29 @@ class PaymentPayload(BaseModel):
|
|
|
287
434
|
)
|
|
288
435
|
|
|
289
436
|
|
|
437
|
+
# Alias for backward compatibility
|
|
438
|
+
PaymentPayload = PaymentPayloadV1
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class PaymentPayloadV2(BaseModel):
|
|
442
|
+
"""V2 Payment Payload - Current format.
|
|
443
|
+
|
|
444
|
+
Sent in the PAYMENT-SIGNATURE header as base64-encoded JSON.
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
t402_version: int = Field(default=T402_VERSION_V2, alias="t402Version")
|
|
448
|
+
resource: ResourceInfo
|
|
449
|
+
accepted: PaymentRequirementsV2
|
|
450
|
+
payload: dict[str, Any]
|
|
451
|
+
extensions: Optional[dict[str, Any]] = None
|
|
452
|
+
|
|
453
|
+
model_config = ConfigDict(
|
|
454
|
+
alias_generator=to_camel,
|
|
455
|
+
populate_by_name=True,
|
|
456
|
+
from_attributes=True,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
290
460
|
class T402Headers(BaseModel):
|
|
291
461
|
x_payment: str
|
|
292
462
|
|