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,343 @@
1
+ """TON Exact Scheme - Client Implementation.
2
+
3
+ This module provides the client-side implementation of the exact payment scheme
4
+ for TON network using Jetton transfers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from typing import Any, Callable, Dict, Optional, Protocol, Union, Awaitable
11
+
12
+ from t402.types import (
13
+ PaymentRequirementsV2,
14
+ T402_VERSION_V1,
15
+ T402_VERSION_V2,
16
+ )
17
+ from t402.ton import (
18
+ TonAuthorization,
19
+ TonPaymentPayload,
20
+ DEFAULT_JETTON_TRANSFER_TON,
21
+ DEFAULT_FORWARD_TON,
22
+ validate_ton_address,
23
+ )
24
+
25
+
26
+ # Constants
27
+ SCHEME_EXACT = "exact"
28
+
29
+
30
+ class SignedMessage(Protocol):
31
+ """Protocol for a signed TON message."""
32
+
33
+ def to_boc_base64(self) -> str:
34
+ """Convert message to base64-encoded BOC."""
35
+ ...
36
+
37
+
38
+ class TonSigner(Protocol):
39
+ """Protocol for TON wallet signing operations.
40
+
41
+ Implementations should provide wallet address, seqno retrieval,
42
+ and message signing capabilities.
43
+
44
+ Example implementation with tonsdk:
45
+ ```python
46
+ class MyTonSigner:
47
+ def __init__(self, wallet, client):
48
+ self._wallet = wallet
49
+ self._client = client
50
+
51
+ @property
52
+ def address(self) -> str:
53
+ return self._wallet.address.to_string(True, True, True)
54
+
55
+ async def get_seqno(self) -> int:
56
+ return await self._client.get_seqno(self._wallet.address)
57
+
58
+ async def sign_message(
59
+ self,
60
+ to: str,
61
+ value: int,
62
+ body: bytes,
63
+ timeout: int,
64
+ ) -> SignedMessage:
65
+ # Build and sign the message
66
+ return signed_message
67
+ ```
68
+ """
69
+
70
+ @property
71
+ def address(self) -> str:
72
+ """Return the wallet address as a friendly string."""
73
+ ...
74
+
75
+ async def get_seqno(self) -> int:
76
+ """Get the current sequence number for replay protection."""
77
+ ...
78
+
79
+ async def sign_message(
80
+ self,
81
+ to: str,
82
+ value: int,
83
+ body: bytes,
84
+ timeout: int,
85
+ ) -> SignedMessage:
86
+ """Sign a message to be sent to the TON network.
87
+
88
+ Args:
89
+ to: Destination address
90
+ value: Amount of TON in nanoTON
91
+ body: Message body as bytes (BOC)
92
+ timeout: Message validity timeout in seconds
93
+
94
+ Returns:
95
+ SignedMessage that can be converted to BOC
96
+ """
97
+ ...
98
+
99
+
100
+ # Type for Jetton wallet address resolver
101
+ JettonWalletResolver = Callable[[str, str], Awaitable[str]]
102
+
103
+
104
+ def generate_query_id() -> int:
105
+ """Generate a unique query ID for Jetton transfers.
106
+
107
+ Returns:
108
+ Unique query ID based on timestamp
109
+ """
110
+ return int(time.time() * 1000000)
111
+
112
+
113
+ class ExactTonClientScheme:
114
+ """Client scheme for TON exact payments using Jetton transfers.
115
+
116
+ Creates signed BOC messages that can be broadcast by a facilitator
117
+ to complete the payment.
118
+
119
+ Example:
120
+ ```python
121
+ scheme = ExactTonClientScheme(
122
+ signer=my_ton_signer,
123
+ get_jetton_wallet_address=my_resolver,
124
+ )
125
+
126
+ payload = await scheme.create_payment_payload(
127
+ t402_version=2,
128
+ requirements={
129
+ "scheme": "exact",
130
+ "network": "ton:mainnet",
131
+ "asset": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
132
+ "amount": "1000000",
133
+ "payTo": "EQ...",
134
+ "maxTimeoutSeconds": 300,
135
+ },
136
+ )
137
+ ```
138
+ """
139
+
140
+ scheme = SCHEME_EXACT
141
+ caip_family = "ton:*"
142
+
143
+ def __init__(
144
+ self,
145
+ signer: TonSigner,
146
+ get_jetton_wallet_address: JettonWalletResolver,
147
+ gas_amount: Optional[int] = None,
148
+ forward_amount: Optional[int] = None,
149
+ ):
150
+ """Initialize the TON client scheme.
151
+
152
+ Args:
153
+ signer: TON signer for signing messages
154
+ get_jetton_wallet_address: Function to resolve Jetton wallet address
155
+ gas_amount: Override TON amount for gas (in nanoTON)
156
+ forward_amount: Override forward TON amount (in nanoTON)
157
+ """
158
+ self._signer = signer
159
+ self._get_jetton_wallet_address = get_jetton_wallet_address
160
+ self._gas_amount = gas_amount or DEFAULT_JETTON_TRANSFER_TON
161
+ self._forward_amount = forward_amount or DEFAULT_FORWARD_TON
162
+
163
+ @property
164
+ def address(self) -> str:
165
+ """Return the wallet address."""
166
+ return self._signer.address
167
+
168
+ async def create_payment_payload(
169
+ self,
170
+ t402_version: int,
171
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
172
+ ) -> Dict[str, Any]:
173
+ """Create a payment payload for TON Jetton transfer.
174
+
175
+ Args:
176
+ t402_version: Protocol version (1 or 2)
177
+ requirements: Payment requirements
178
+
179
+ Returns:
180
+ Payment payload with signed BOC and authorization metadata
181
+ """
182
+ # Convert to dict for easier access
183
+ if hasattr(requirements, "model_dump"):
184
+ req = requirements.model_dump(by_alias=True)
185
+ else:
186
+ req = dict(requirements)
187
+
188
+ # Extract fields
189
+ network = req.get("network", "")
190
+ asset = req.get("asset", "")
191
+ amount = req.get("amount", "0")
192
+ pay_to = req.get("payTo", "")
193
+ max_timeout = req.get("maxTimeoutSeconds", 300)
194
+
195
+ # Validate required fields
196
+ if not asset:
197
+ raise ValueError("Asset (Jetton master address) is required")
198
+ if not pay_to:
199
+ raise ValueError("PayTo address is required")
200
+ if not amount:
201
+ raise ValueError("Amount is required")
202
+ if not validate_ton_address(pay_to):
203
+ raise ValueError(f"Invalid payTo address: {pay_to}")
204
+
205
+ # Get sender's Jetton wallet address
206
+ sender_jetton_wallet = await self._get_jetton_wallet_address(
207
+ self._signer.address,
208
+ asset,
209
+ )
210
+
211
+ # Get current seqno for replay protection
212
+ seqno = await self._signer.get_seqno()
213
+
214
+ # Calculate validity period
215
+ now = int(time.time())
216
+ valid_until = now + max_timeout
217
+
218
+ # Generate unique query ID
219
+ query_id = generate_query_id()
220
+
221
+ # Parse amount
222
+ jetton_amount = int(amount)
223
+
224
+ # Build Jetton transfer body
225
+ jetton_body = self._build_jetton_transfer_body(
226
+ query_id=query_id,
227
+ amount=jetton_amount,
228
+ destination=pay_to,
229
+ response_destination=self._signer.address,
230
+ forward_amount=self._forward_amount,
231
+ )
232
+
233
+ # Sign the message
234
+ signed_message = await self._signer.sign_message(
235
+ to=sender_jetton_wallet,
236
+ value=self._gas_amount,
237
+ body=jetton_body,
238
+ timeout=max_timeout,
239
+ )
240
+
241
+ # Encode to base64
242
+ signed_boc = signed_message.to_boc_base64()
243
+
244
+ # Build authorization metadata
245
+ authorization = TonAuthorization(
246
+ from_=self._signer.address,
247
+ to=pay_to,
248
+ jetton_master=asset,
249
+ jetton_amount=str(jetton_amount),
250
+ ton_amount=str(self._gas_amount),
251
+ valid_until=valid_until,
252
+ seqno=seqno,
253
+ query_id=str(query_id),
254
+ )
255
+
256
+ # Build payload
257
+ payload_data = TonPaymentPayload(
258
+ signed_boc=signed_boc,
259
+ authorization=authorization,
260
+ )
261
+
262
+ if t402_version == T402_VERSION_V1:
263
+ return {
264
+ "t402Version": T402_VERSION_V1,
265
+ "scheme": self.scheme,
266
+ "network": network,
267
+ "payload": payload_data.model_dump(by_alias=True),
268
+ }
269
+
270
+ # V2 format
271
+ return {
272
+ "t402Version": T402_VERSION_V2,
273
+ "payload": payload_data.model_dump(by_alias=True),
274
+ }
275
+
276
+ def _build_jetton_transfer_body(
277
+ self,
278
+ query_id: int,
279
+ amount: int,
280
+ destination: str,
281
+ response_destination: str,
282
+ forward_amount: int,
283
+ ) -> bytes:
284
+ """Build Jetton transfer body as bytes.
285
+
286
+ This builds the TEP-74 Jetton transfer internal message body.
287
+
288
+ Args:
289
+ query_id: Unique query ID
290
+ amount: Jetton amount in smallest units
291
+ destination: Destination address
292
+ response_destination: Address for excess response
293
+ forward_amount: Forward TON amount
294
+
295
+ Returns:
296
+ Serialized message body as bytes
297
+
298
+ Note:
299
+ This returns a minimal placeholder. Real implementations should
300
+ use tonsdk or pytoniq to build proper BOC cells.
301
+ """
302
+ # Import here to avoid hard dependency
303
+ try:
304
+ from t402.ton import JETTON_TRANSFER_OP
305
+
306
+ # Build cell using available library
307
+ # This is a placeholder - real implementation needs tonsdk/pytoniq
308
+ #
309
+ # The actual cell structure should be:
310
+ # transfer#0f8a7ea5 query_id:uint64 amount:(VarUInteger 16)
311
+ # destination:MsgAddress response_destination:MsgAddress
312
+ # custom_payload:(Maybe ^Cell) forward_ton_amount:(VarUInteger 16)
313
+ # forward_payload:(Either Cell ^Cell) = InternalMsgBody;
314
+
315
+ # For now, return a placeholder that indicates the transfer params
316
+ # Real signers should handle cell building internally
317
+ import json
318
+
319
+ # Encode as JSON for testing/mocking purposes
320
+ # Real implementation would use proper BOC encoding
321
+ transfer_params = {
322
+ "op": JETTON_TRANSFER_OP,
323
+ "query_id": query_id,
324
+ "amount": amount,
325
+ "destination": destination,
326
+ "response_destination": response_destination,
327
+ "forward_amount": forward_amount,
328
+ }
329
+ return json.dumps(transfer_params).encode("utf-8")
330
+
331
+ except ImportError:
332
+ # Fallback if ton module not fully available
333
+ import json
334
+
335
+ transfer_params = {
336
+ "op": 0x0F8A7EA5,
337
+ "query_id": query_id,
338
+ "amount": amount,
339
+ "destination": destination,
340
+ "response_destination": response_destination,
341
+ "forward_amount": forward_amount,
342
+ }
343
+ return json.dumps(transfer_params).encode("utf-8")
@@ -0,0 +1,201 @@
1
+ """TON Exact Scheme - Server Implementation.
2
+
3
+ This module provides the server-side implementation of the exact payment scheme
4
+ for TON 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.ton import (
18
+ SCHEME_EXACT,
19
+ TON_MAINNET,
20
+ TON_TESTNET,
21
+ DEFAULT_DECIMALS,
22
+ get_network_config,
23
+ get_default_asset,
24
+ get_asset_info,
25
+ )
26
+
27
+
28
+ class ExactTonServerScheme:
29
+ """Server scheme for TON exact payments.
30
+
31
+ Handles parsing user-friendly prices and enhancing payment requirements
32
+ with TON-specific metadata for clients.
33
+
34
+ Example:
35
+ ```python
36
+ scheme = ExactTonServerScheme()
37
+
38
+ # Parse price
39
+ asset_amount = await scheme.parse_price("$0.10", "ton:mainnet")
40
+ # Returns: {"amount": "100000", "asset": "EQ...", "extra": {...}}
41
+
42
+ # Enhance requirements
43
+ enhanced = await scheme.enhance_requirements(
44
+ requirements,
45
+ supported_kind,
46
+ facilitator_extensions,
47
+ )
48
+ ```
49
+ """
50
+
51
+ scheme = SCHEME_EXACT
52
+ caip_family = "ton:*"
53
+
54
+ async def parse_price(
55
+ self,
56
+ price: Union[str, int, float, Dict[str, Any]],
57
+ network: Network,
58
+ ) -> AssetAmount:
59
+ """Parse a user-friendly price to atomic amount and asset.
60
+
61
+ Supports:
62
+ - String with $ prefix: "$0.10" -> 100000 (6 decimals)
63
+ - String without prefix: "0.10" -> 100000
64
+ - Integer/float: 0.10 -> 100000
65
+ - Dict (TokenAmount): {"amount": "100000", "asset": "EQ..."}
66
+
67
+ Args:
68
+ price: User-friendly price
69
+ network: Network identifier (CAIP-2 format, e.g., "ton:mainnet")
70
+
71
+ Returns:
72
+ AssetAmount dict with amount, asset, and extra metadata
73
+ """
74
+ # Validate network
75
+ network_str = self._normalize_network(network)
76
+
77
+ # Handle dict (already in TokenAmount format)
78
+ if isinstance(price, dict):
79
+ return {
80
+ "amount": str(price.get("amount", "0")),
81
+ "asset": price.get("asset", ""),
82
+ "extra": price.get("extra", {}),
83
+ }
84
+
85
+ # Get default asset (USDT) for the network
86
+ default_asset = get_default_asset(network_str)
87
+ if not default_asset:
88
+ raise ValueError(f"Unsupported TON network: {network}")
89
+
90
+ asset_address = default_asset["master_address"]
91
+ decimals = default_asset.get("decimals", DEFAULT_DECIMALS)
92
+
93
+ # Parse price string/number
94
+ if isinstance(price, str):
95
+ if price.startswith("$"):
96
+ price = price[1:]
97
+ amount_decimal = Decimal(price)
98
+ else:
99
+ amount_decimal = Decimal(str(price))
100
+
101
+ # Convert to atomic units
102
+ atomic_amount = int(amount_decimal * Decimal(10 ** decimals))
103
+
104
+ # Build extra metadata
105
+ extra = {
106
+ "symbol": default_asset.get("symbol", "USDT"),
107
+ "name": default_asset.get("name", "Tether USD"),
108
+ "decimals": decimals,
109
+ }
110
+
111
+ return {
112
+ "amount": str(atomic_amount),
113
+ "asset": asset_address,
114
+ "extra": extra,
115
+ }
116
+
117
+ async def enhance_requirements(
118
+ self,
119
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
120
+ supported_kind: SupportedKindDict,
121
+ facilitator_extensions: List[str],
122
+ ) -> Union[PaymentRequirementsV2, Dict[str, Any]]:
123
+ """Enhance payment requirements with TON-specific metadata.
124
+
125
+ Adds Jetton metadata to the extra field so clients can
126
+ properly build the transfer message.
127
+
128
+ Args:
129
+ requirements: Base payment requirements
130
+ supported_kind: Matched SupportedKind from facilitator
131
+ facilitator_extensions: Extensions supported by facilitator
132
+
133
+ Returns:
134
+ Enhanced requirements with TON metadata in extra
135
+ """
136
+ # Convert to dict for modification
137
+ if hasattr(requirements, "model_dump"):
138
+ req = requirements.model_dump(by_alias=True)
139
+ else:
140
+ req = dict(requirements)
141
+
142
+ network = req.get("network", "")
143
+ asset = req.get("asset", "")
144
+
145
+ # Normalize network
146
+ network_str = self._normalize_network(network)
147
+
148
+ # Ensure extra exists
149
+ if "extra" not in req or req["extra"] is None:
150
+ req["extra"] = {}
151
+
152
+ # Add Jetton metadata if not present
153
+ asset_info = get_asset_info(network_str, asset)
154
+ if asset_info:
155
+ if "symbol" not in req["extra"]:
156
+ req["extra"]["symbol"] = asset_info.get("symbol", "UNKNOWN")
157
+ if "name" not in req["extra"]:
158
+ req["extra"]["name"] = asset_info.get("name", "Unknown Jetton")
159
+ if "decimals" not in req["extra"]:
160
+ req["extra"]["decimals"] = asset_info.get("decimals", DEFAULT_DECIMALS)
161
+
162
+ # Add network config info
163
+ network_config = get_network_config(network_str)
164
+ if network_config:
165
+ if "endpoint" not in req["extra"]:
166
+ req["extra"]["endpoint"] = network_config.get("endpoint", "")
167
+
168
+ # Add facilitator extra data if available
169
+ if supported_kind.get("extra"):
170
+ for key, value in supported_kind["extra"].items():
171
+ if key not in req["extra"]:
172
+ req["extra"][key] = value
173
+
174
+ return req
175
+
176
+ def _normalize_network(self, network: str) -> str:
177
+ """Normalize network identifier to CAIP-2 format.
178
+
179
+ Args:
180
+ network: Network identifier
181
+
182
+ Returns:
183
+ Normalized CAIP-2 network string
184
+
185
+ Raises:
186
+ ValueError: If network is not supported
187
+ """
188
+ # Already in CAIP-2 format
189
+ if network.startswith("ton:"):
190
+ if network in (TON_MAINNET, TON_TESTNET):
191
+ return network
192
+ raise ValueError(f"Unknown TON network: {network}")
193
+
194
+ # Handle legacy format
195
+ lower = network.lower()
196
+ if lower in ("mainnet", "ton-mainnet"):
197
+ return TON_MAINNET
198
+ elif lower in ("testnet", "ton-testnet"):
199
+ return TON_TESTNET
200
+
201
+ raise ValueError(f"Unknown network: {network}")
@@ -0,0 +1,22 @@
1
+ """TRON Blockchain Payment Schemes.
2
+
3
+ This package provides payment scheme implementations for TRON blockchain.
4
+
5
+ Supported schemes:
6
+ - exact: TRC-20 token transfers with signed transactions
7
+ """
8
+
9
+ from t402.schemes.tron.exact import (
10
+ ExactTronClientScheme,
11
+ ExactTronServerScheme,
12
+ TronSigner,
13
+ SCHEME_EXACT,
14
+ )
15
+
16
+ __all__ = [
17
+ # Exact scheme
18
+ "ExactTronClientScheme",
19
+ "ExactTronServerScheme",
20
+ "TronSigner",
21
+ "SCHEME_EXACT",
22
+ ]
@@ -0,0 +1,27 @@
1
+ """TRON Exact Payment Scheme.
2
+
3
+ This package provides the exact payment scheme implementation for TRON
4
+ using TRC-20 token transfers.
5
+
6
+ The exact scheme allows users to sign TRC-20 transfer transactions that
7
+ can be verified and broadcast by a facilitator, enabling gasless payments.
8
+ """
9
+
10
+ from t402.schemes.tron.exact.client import (
11
+ ExactTronClientScheme,
12
+ TronSigner,
13
+ SCHEME_EXACT,
14
+ )
15
+ from t402.schemes.tron.exact.server import (
16
+ ExactTronServerScheme,
17
+ )
18
+
19
+ __all__ = [
20
+ # Client
21
+ "ExactTronClientScheme",
22
+ "TronSigner",
23
+ # Server
24
+ "ExactTronServerScheme",
25
+ # Constants
26
+ "SCHEME_EXACT",
27
+ ]