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.
@@ -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
- class PaymentRequirements(BaseModel):
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
- # Returned by a server as json alongside a 402 response code
127
- class t402PaymentRequiredResponse(BaseModel):
128
- t402_version: int
129
- accepts: list[PaymentRequirements]
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 PaymentPayload(BaseModel):
278
- t402_version: int
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t402
3
- Version: 1.6.1
3
+ Version: 1.7.1
4
4
  Summary: t402: An internet native payments protocol
5
5
  Author-email: T402 Team <dev@t402.io>
6
6
  License: Apache-2.0