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,125 @@
1
+ """T402 Payment Scheme Architecture.
2
+
3
+ This package provides the interfaces and registry for managing payment schemes
4
+ in the T402 protocol.
5
+
6
+ Modules:
7
+ interfaces: Abstract interfaces for scheme implementations
8
+ registry: Scheme registration and lookup functionality
9
+
10
+ Usage:
11
+ ```python
12
+ from t402.schemes import (
13
+ # Interfaces (for implementing new schemes)
14
+ SchemeNetworkClient,
15
+ SchemeNetworkServer,
16
+ SchemeNetworkFacilitator,
17
+
18
+ # Registry classes
19
+ SchemeRegistry,
20
+ ClientSchemeRegistry,
21
+ ServerSchemeRegistry,
22
+ FacilitatorSchemeRegistry,
23
+
24
+ # Global registry access
25
+ get_client_registry,
26
+ get_server_registry,
27
+ get_facilitator_registry,
28
+ )
29
+
30
+ # Register a scheme
31
+ registry = get_client_registry()
32
+ registry.register("eip155:8453", my_evm_client)
33
+
34
+ # Or create your own registry
35
+ my_registry = ClientSchemeRegistry()
36
+ my_registry.register("eip155:*", my_evm_client) # Wildcard for all EVM
37
+ ```
38
+ """
39
+
40
+ from t402.schemes.interfaces import (
41
+ # Type aliases
42
+ Price,
43
+ AssetAmount,
44
+ SupportedKindDict,
45
+ # Protocols (duck typing)
46
+ SchemeNetworkClient,
47
+ SchemeNetworkServer,
48
+ SchemeNetworkFacilitator,
49
+ # Abstract Base Classes (inheritance)
50
+ BaseSchemeNetworkClient,
51
+ BaseSchemeNetworkServer,
52
+ BaseSchemeNetworkFacilitator,
53
+ )
54
+
55
+ from t402.schemes.registry import (
56
+ # Core registry
57
+ SchemeRegistry,
58
+ # Typed registries
59
+ ClientSchemeRegistry,
60
+ ServerSchemeRegistry,
61
+ FacilitatorSchemeRegistry,
62
+ # Global registry accessors
63
+ get_client_registry,
64
+ get_server_registry,
65
+ get_facilitator_registry,
66
+ reset_global_registries,
67
+ )
68
+
69
+ # EVM Schemes
70
+ from t402.schemes.evm import (
71
+ ExactEvmClientScheme,
72
+ ExactEvmServerScheme,
73
+ EvmSigner,
74
+ )
75
+
76
+ # TON Schemes
77
+ from t402.schemes.ton import (
78
+ ExactTonClientScheme,
79
+ ExactTonServerScheme,
80
+ TonSigner,
81
+ )
82
+
83
+ # TRON Schemes
84
+ from t402.schemes.tron import (
85
+ ExactTronClientScheme,
86
+ ExactTronServerScheme,
87
+ TronSigner,
88
+ )
89
+
90
+ __all__ = [
91
+ # Type aliases
92
+ "Price",
93
+ "AssetAmount",
94
+ "SupportedKindDict",
95
+ # Protocols
96
+ "SchemeNetworkClient",
97
+ "SchemeNetworkServer",
98
+ "SchemeNetworkFacilitator",
99
+ # ABCs
100
+ "BaseSchemeNetworkClient",
101
+ "BaseSchemeNetworkServer",
102
+ "BaseSchemeNetworkFacilitator",
103
+ # Registry classes
104
+ "SchemeRegistry",
105
+ "ClientSchemeRegistry",
106
+ "ServerSchemeRegistry",
107
+ "FacilitatorSchemeRegistry",
108
+ # Global registry functions
109
+ "get_client_registry",
110
+ "get_server_registry",
111
+ "get_facilitator_registry",
112
+ "reset_global_registries",
113
+ # EVM Schemes
114
+ "ExactEvmClientScheme",
115
+ "ExactEvmServerScheme",
116
+ "EvmSigner",
117
+ # TON Schemes
118
+ "ExactTonClientScheme",
119
+ "ExactTonServerScheme",
120
+ "TonSigner",
121
+ # TRON Schemes
122
+ "ExactTronClientScheme",
123
+ "ExactTronServerScheme",
124
+ "TronSigner",
125
+ ]
@@ -0,0 +1,25 @@
1
+ """EVM Blockchain Payment Schemes.
2
+
3
+ This package provides payment scheme implementations for EVM-compatible
4
+ blockchains (Ethereum, Base, Avalanche, etc.).
5
+
6
+ Supported schemes:
7
+ - exact: EIP-3009 TransferWithAuthorization
8
+ """
9
+
10
+ from t402.schemes.evm.exact import (
11
+ ExactEvmClientScheme,
12
+ ExactEvmServerScheme,
13
+ EvmSigner,
14
+ create_nonce,
15
+ SCHEME_EXACT,
16
+ )
17
+
18
+ __all__ = [
19
+ # Exact scheme
20
+ "ExactEvmClientScheme",
21
+ "ExactEvmServerScheme",
22
+ "EvmSigner",
23
+ "create_nonce",
24
+ "SCHEME_EXACT",
25
+ ]
@@ -0,0 +1,29 @@
1
+ """EVM Exact Payment Scheme.
2
+
3
+ This package provides the exact payment scheme implementation for EVM networks
4
+ using EIP-3009 TransferWithAuthorization.
5
+
6
+ The exact scheme allows users to authorize token transfers that can be executed
7
+ by a facilitator, enabling gasless payments.
8
+ """
9
+
10
+ from t402.schemes.evm.exact.client import (
11
+ ExactEvmClientScheme,
12
+ EvmSigner,
13
+ create_nonce,
14
+ SCHEME_EXACT,
15
+ )
16
+ from t402.schemes.evm.exact.server import (
17
+ ExactEvmServerScheme,
18
+ )
19
+
20
+ __all__ = [
21
+ # Client
22
+ "ExactEvmClientScheme",
23
+ "EvmSigner",
24
+ "create_nonce",
25
+ # Server
26
+ "ExactEvmServerScheme",
27
+ # Constants
28
+ "SCHEME_EXACT",
29
+ ]
@@ -0,0 +1,265 @@
1
+ """EVM Exact Scheme - Client Implementation.
2
+
3
+ This module provides the client-side implementation of the exact payment scheme
4
+ for EVM networks using EIP-3009 TransferWithAuthorization.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import secrets
10
+ import time
11
+ from typing import Any, Dict, Protocol, Union, runtime_checkable
12
+
13
+ from t402.types import (
14
+ PaymentRequirementsV2,
15
+ T402_VERSION_V1,
16
+ )
17
+ from t402.chains import get_chain_id
18
+
19
+
20
+ # Constants
21
+ SCHEME_EXACT = "exact"
22
+
23
+
24
+ @runtime_checkable
25
+ class EvmSigner(Protocol):
26
+ """Protocol for EVM signing operations."""
27
+
28
+ @property
29
+ def address(self) -> str:
30
+ """Get the signer's address."""
31
+ ...
32
+
33
+ def sign_typed_data(
34
+ self,
35
+ domain_data: Dict[str, Any],
36
+ message_types: Dict[str, Any],
37
+ message_data: Dict[str, Any],
38
+ ) -> Any:
39
+ """Sign EIP-712 typed data."""
40
+ ...
41
+
42
+
43
+ def create_nonce() -> bytes:
44
+ """Create a random 32-byte nonce for authorization signatures."""
45
+ return secrets.token_bytes(32)
46
+
47
+
48
+ class ExactEvmClientScheme:
49
+ """Client scheme for EVM exact payments using EIP-3009.
50
+
51
+ This scheme creates payment payloads using EIP-3009 TransferWithAuthorization,
52
+ which allows gasless token transfers with signature authorization.
53
+
54
+ Example:
55
+ ```python
56
+ from eth_account import Account
57
+
58
+ # Create signer from private key
59
+ account = Account.from_key("0x...")
60
+
61
+ # Create scheme
62
+ scheme = ExactEvmClientScheme(account)
63
+
64
+ # Create payment payload
65
+ payload = await scheme.create_payment_payload(
66
+ t402_version=2,
67
+ requirements=requirements,
68
+ )
69
+ ```
70
+ """
71
+
72
+ scheme = SCHEME_EXACT
73
+ caip_family = "eip155:*"
74
+
75
+ def __init__(self, signer: EvmSigner):
76
+ """Initialize with an EVM signer.
77
+
78
+ Args:
79
+ signer: Any object implementing the EvmSigner protocol
80
+ (e.g., eth_account.Account)
81
+ """
82
+ self._signer = signer
83
+
84
+ @property
85
+ def address(self) -> str:
86
+ """Get the signer's address."""
87
+ return self._signer.address
88
+
89
+ async def create_payment_payload(
90
+ self,
91
+ t402_version: int,
92
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
93
+ ) -> Dict[str, Any]:
94
+ """Create a payment payload for EVM exact scheme.
95
+
96
+ Creates an EIP-3009 TransferWithAuthorization and signs it.
97
+
98
+ Args:
99
+ t402_version: Protocol version (1 or 2)
100
+ requirements: Payment requirements with amount, asset, payTo, etc.
101
+
102
+ Returns:
103
+ Dict with t402Version, scheme, network, and payload containing
104
+ signature and authorization data.
105
+ """
106
+ # Extract requirements (handle both model and dict)
107
+ if hasattr(requirements, "model_dump"):
108
+ req = requirements.model_dump(by_alias=True)
109
+ else:
110
+ req = dict(requirements)
111
+
112
+ # Get network and chain ID
113
+ network = req.get("network", "")
114
+ chain_id = self._get_chain_id(network)
115
+
116
+ # Get amount - V2 uses 'amount', V1 uses 'maxAmountRequired'
117
+ amount = req.get("amount") or req.get("maxAmountRequired", "0")
118
+
119
+ # Get pay_to address
120
+ pay_to = req.get("payTo") or req.get("pay_to", "")
121
+
122
+ # Get asset address
123
+ asset = req.get("asset", "")
124
+
125
+ # Get timeout
126
+ max_timeout = req.get("maxTimeoutSeconds") or req.get("max_timeout_seconds", 300)
127
+
128
+ # Get EIP-712 domain info from extra
129
+ extra = req.get("extra", {})
130
+ token_name = extra.get("name", "USD Coin")
131
+ token_version = extra.get("version", "2")
132
+
133
+ # Create authorization
134
+ nonce = create_nonce()
135
+ now = int(time.time())
136
+ valid_after = str(now - 60) # 60 seconds buffer
137
+ valid_before = str(now + max_timeout)
138
+
139
+ authorization = {
140
+ "from": self._signer.address,
141
+ "to": pay_to,
142
+ "value": str(amount),
143
+ "validAfter": valid_after,
144
+ "validBefore": valid_before,
145
+ "nonce": nonce,
146
+ }
147
+
148
+ # Sign the authorization
149
+ signature = self._sign_authorization(
150
+ authorization=authorization,
151
+ chain_id=chain_id,
152
+ asset_address=asset,
153
+ token_name=token_name,
154
+ token_version=token_version,
155
+ )
156
+
157
+ # Convert nonce to hex string
158
+ authorization["nonce"] = f"0x{nonce.hex()}"
159
+
160
+ # Build payload based on version
161
+ if t402_version == T402_VERSION_V1:
162
+ return {
163
+ "t402Version": t402_version,
164
+ "scheme": self.scheme,
165
+ "network": network,
166
+ "payload": {
167
+ "signature": signature,
168
+ "authorization": authorization,
169
+ },
170
+ }
171
+ else:
172
+ # V2 - return just the partial payload
173
+ return {
174
+ "t402Version": t402_version,
175
+ "payload": {
176
+ "signature": signature,
177
+ "authorization": authorization,
178
+ },
179
+ }
180
+
181
+ def _get_chain_id(self, network: str) -> int:
182
+ """Get chain ID from network identifier.
183
+
184
+ Args:
185
+ network: Network identifier (CAIP-2 or legacy format)
186
+
187
+ Returns:
188
+ Chain ID as integer
189
+ """
190
+ # Handle CAIP-2 format (eip155:8453)
191
+ if network.startswith("eip155:"):
192
+ return int(network.split(":")[1])
193
+
194
+ # Handle legacy format
195
+ try:
196
+ return get_chain_id(network)
197
+ except (KeyError, ValueError):
198
+ raise ValueError(f"Unknown network: {network}")
199
+
200
+ def _sign_authorization(
201
+ self,
202
+ authorization: Dict[str, Any],
203
+ chain_id: int,
204
+ asset_address: str,
205
+ token_name: str,
206
+ token_version: str,
207
+ ) -> str:
208
+ """Sign an EIP-3009 authorization.
209
+
210
+ Args:
211
+ authorization: Authorization data
212
+ chain_id: EVM chain ID
213
+ asset_address: Token contract address
214
+ token_name: Token name for EIP-712 domain
215
+ token_version: Token version for EIP-712 domain
216
+
217
+ Returns:
218
+ Signature as hex string
219
+ """
220
+ # Get nonce as bytes
221
+ nonce = authorization["nonce"]
222
+ if isinstance(nonce, str):
223
+ nonce = bytes.fromhex(nonce.replace("0x", ""))
224
+
225
+ # Build EIP-712 typed data
226
+ domain = {
227
+ "name": token_name,
228
+ "version": token_version,
229
+ "chainId": chain_id,
230
+ "verifyingContract": asset_address,
231
+ }
232
+
233
+ types = {
234
+ "TransferWithAuthorization": [
235
+ {"name": "from", "type": "address"},
236
+ {"name": "to", "type": "address"},
237
+ {"name": "value", "type": "uint256"},
238
+ {"name": "validAfter", "type": "uint256"},
239
+ {"name": "validBefore", "type": "uint256"},
240
+ {"name": "nonce", "type": "bytes32"},
241
+ ]
242
+ }
243
+
244
+ message = {
245
+ "from": authorization["from"],
246
+ "to": authorization["to"],
247
+ "value": int(authorization["value"]),
248
+ "validAfter": int(authorization["validAfter"]),
249
+ "validBefore": int(authorization["validBefore"]),
250
+ "nonce": nonce,
251
+ }
252
+
253
+ # Sign
254
+ signed = self._signer.sign_typed_data(
255
+ domain_data=domain,
256
+ message_types=types,
257
+ message_data=message,
258
+ )
259
+
260
+ # Extract signature
261
+ signature = signed.signature.hex()
262
+ if not signature.startswith("0x"):
263
+ signature = f"0x{signature}"
264
+
265
+ return signature
@@ -0,0 +1,181 @@
1
+ """EVM Exact Scheme - Server Implementation.
2
+
3
+ This module provides the server-side implementation of the exact payment scheme
4
+ for EVM networks.
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.chains import (
18
+ get_chain_id,
19
+ get_token_decimals,
20
+ get_token_name,
21
+ get_token_version,
22
+ get_default_token_address,
23
+ )
24
+
25
+
26
+ # Constants
27
+ SCHEME_EXACT = "exact"
28
+
29
+
30
+ class ExactEvmServerScheme:
31
+ """Server scheme for EVM exact payments.
32
+
33
+ Handles parsing user-friendly prices and enhancing payment requirements
34
+ with EIP-712 domain information needed for clients.
35
+
36
+ Example:
37
+ ```python
38
+ scheme = ExactEvmServerScheme()
39
+
40
+ # Parse price
41
+ asset_amount = await scheme.parse_price("$0.10", "eip155:8453")
42
+ # Returns: {"amount": "100000", "asset": "0x833589...", "extra": {...}}
43
+
44
+ # Enhance requirements
45
+ enhanced = await scheme.enhance_requirements(
46
+ requirements,
47
+ supported_kind,
48
+ facilitator_extensions,
49
+ )
50
+ ```
51
+ """
52
+
53
+ scheme = SCHEME_EXACT
54
+ caip_family = "eip155:*"
55
+
56
+ async def parse_price(
57
+ self,
58
+ price: Union[str, int, float, Dict[str, Any]],
59
+ network: Network,
60
+ ) -> AssetAmount:
61
+ """Parse a user-friendly price to atomic amount and asset.
62
+
63
+ Supports:
64
+ - String with $ prefix: "$0.10" -> 100000 (6 decimals)
65
+ - String without prefix: "0.10" -> 100000
66
+ - Integer/float: 0.10 -> 100000
67
+ - Dict (TokenAmount): {"amount": "100000", "asset": "0x..."}
68
+
69
+ Args:
70
+ price: User-friendly price
71
+ network: Network identifier (CAIP-2 format)
72
+
73
+ Returns:
74
+ AssetAmount dict with amount, asset, and extra metadata
75
+ """
76
+ chain_id = self._get_chain_id(network)
77
+
78
+ # Handle dict (already in TokenAmount format)
79
+ if isinstance(price, dict):
80
+ return {
81
+ "amount": str(price.get("amount", "0")),
82
+ "asset": price.get("asset", ""),
83
+ "extra": price.get("extra", {}),
84
+ }
85
+
86
+ # Get default token (USDC) for the network
87
+ chain_id_str = str(chain_id)
88
+ asset_address = get_default_token_address(chain_id_str, "usdc")
89
+ decimals = get_token_decimals(chain_id_str, asset_address)
90
+
91
+ # Parse price string/number
92
+ if isinstance(price, str):
93
+ if price.startswith("$"):
94
+ price = price[1:]
95
+ amount_decimal = Decimal(price)
96
+ else:
97
+ amount_decimal = Decimal(str(price))
98
+
99
+ # Convert to atomic units
100
+ atomic_amount = int(amount_decimal * Decimal(10 ** decimals))
101
+
102
+ # Get EIP-712 domain info
103
+ extra = {
104
+ "name": get_token_name(chain_id_str, asset_address),
105
+ "version": get_token_version(chain_id_str, asset_address),
106
+ "decimals": decimals,
107
+ }
108
+
109
+ return {
110
+ "amount": str(atomic_amount),
111
+ "asset": asset_address,
112
+ "extra": extra,
113
+ }
114
+
115
+ async def enhance_requirements(
116
+ self,
117
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
118
+ supported_kind: SupportedKindDict,
119
+ facilitator_extensions: List[str],
120
+ ) -> Union[PaymentRequirementsV2, Dict[str, Any]]:
121
+ """Enhance payment requirements with EVM-specific metadata.
122
+
123
+ Adds EIP-712 domain information to the extra field so clients
124
+ can properly sign the authorization.
125
+
126
+ Args:
127
+ requirements: Base payment requirements
128
+ supported_kind: Matched SupportedKind from facilitator
129
+ facilitator_extensions: Extensions supported by facilitator
130
+
131
+ Returns:
132
+ Enhanced requirements with EIP-712 domain in extra
133
+ """
134
+ # Convert to dict for modification
135
+ if hasattr(requirements, "model_dump"):
136
+ req = requirements.model_dump(by_alias=True)
137
+ else:
138
+ req = dict(requirements)
139
+
140
+ network = req.get("network", "")
141
+ asset = req.get("asset", "")
142
+
143
+ # Get chain ID as string
144
+ chain_id = str(self._get_chain_id(network))
145
+
146
+ # Ensure extra exists
147
+ if "extra" not in req or req["extra"] is None:
148
+ req["extra"] = {}
149
+
150
+ # Add EIP-712 domain info if not present
151
+ if "name" not in req["extra"]:
152
+ req["extra"]["name"] = get_token_name(chain_id, asset)
153
+ if "version" not in req["extra"]:
154
+ req["extra"]["version"] = get_token_version(chain_id, asset)
155
+
156
+ # Add facilitator extra data if available
157
+ if supported_kind.get("extra"):
158
+ for key, value in supported_kind["extra"].items():
159
+ if key not in req["extra"]:
160
+ req["extra"][key] = value
161
+
162
+ return req
163
+
164
+ def _get_chain_id(self, network: str) -> int:
165
+ """Get chain ID from network identifier.
166
+
167
+ Args:
168
+ network: Network identifier (CAIP-2 or legacy format)
169
+
170
+ Returns:
171
+ Chain ID as integer
172
+ """
173
+ # Handle CAIP-2 format (eip155:8453)
174
+ if network.startswith("eip155:"):
175
+ return int(network.split(":")[1])
176
+
177
+ # Handle legacy format
178
+ try:
179
+ return get_chain_id(network)
180
+ except (KeyError, ValueError):
181
+ raise ValueError(f"Unknown network: {network}")