t402 1.6.1__py3-none-any.whl → 1.9.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.
@@ -0,0 +1,58 @@
1
+ """EVM Up-To Payment Scheme.
2
+
3
+ This package provides the upto payment scheme implementation for EVM networks
4
+ using EIP-2612 Permit for gasless token approvals.
5
+
6
+ The upto scheme allows clients to authorize a maximum amount that can be
7
+ settled later based on actual usage.
8
+ """
9
+
10
+ from t402.schemes.evm.upto.types import (
11
+ # Type definitions
12
+ PERMIT_TYPES,
13
+ PERMIT_DOMAIN_TYPES,
14
+ # Models
15
+ PermitSignature,
16
+ PermitAuthorization,
17
+ UptoEIP2612Payload,
18
+ UptoCompactPayload,
19
+ UptoEvmExtra,
20
+ UptoEvmSettlement,
21
+ UptoEvmUsageDetails,
22
+ # Type guards
23
+ is_eip2612_payload,
24
+ # Helper functions
25
+ create_permit_domain,
26
+ create_permit_message,
27
+ payload_from_dict,
28
+ )
29
+
30
+ from t402.schemes.evm.upto.client import (
31
+ UptoEvmClientScheme,
32
+ create_payment_nonce,
33
+ SCHEME_UPTO,
34
+ )
35
+
36
+ __all__ = [
37
+ # Constants
38
+ "SCHEME_UPTO",
39
+ "PERMIT_TYPES",
40
+ "PERMIT_DOMAIN_TYPES",
41
+ # Client
42
+ "UptoEvmClientScheme",
43
+ "create_payment_nonce",
44
+ # Types
45
+ "PermitSignature",
46
+ "PermitAuthorization",
47
+ "UptoEIP2612Payload",
48
+ "UptoCompactPayload",
49
+ "UptoEvmExtra",
50
+ "UptoEvmSettlement",
51
+ "UptoEvmUsageDetails",
52
+ # Type guards
53
+ "is_eip2612_payload",
54
+ # Helper functions
55
+ "create_permit_domain",
56
+ "create_permit_message",
57
+ "payload_from_dict",
58
+ ]
@@ -0,0 +1,240 @@
1
+ """EVM Up-To Scheme - Client Implementation.
2
+
3
+ This module provides the client-side implementation of the upto payment scheme
4
+ for EVM networks using EIP-2612 Permit.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import secrets
10
+ import time
11
+ from typing import Any, Dict, Optional, Union
12
+
13
+ from t402.types import PaymentRequirementsV2
14
+ from t402.chains import get_chain_id
15
+ from t402.schemes.evm.exact.client import EvmSigner
16
+ from t402.schemes.upto.types import UptoPaymentRequirements
17
+
18
+
19
+ # Constants
20
+ SCHEME_UPTO = "upto"
21
+
22
+
23
+ def create_payment_nonce() -> bytes:
24
+ """Create a random 32-byte payment nonce."""
25
+ return secrets.token_bytes(32)
26
+
27
+
28
+ class UptoEvmClientScheme:
29
+ """Client scheme for EVM upto payments using EIP-2612.
30
+
31
+ This scheme creates payment payloads using EIP-2612 Permit,
32
+ which allows gasless token approvals for up-to payments.
33
+
34
+ Example:
35
+ ```python
36
+ from eth_account import Account
37
+
38
+ # Create signer from private key
39
+ account = Account.from_key("0x...")
40
+
41
+ # Create scheme
42
+ scheme = UptoEvmClientScheme(account)
43
+
44
+ # Create payment payload
45
+ payload = await scheme.create_payment_payload(
46
+ t402_version=2,
47
+ requirements=requirements,
48
+ )
49
+ ```
50
+ """
51
+
52
+ scheme = SCHEME_UPTO
53
+ caip_family = "eip155:*"
54
+
55
+ def __init__(
56
+ self,
57
+ signer: EvmSigner,
58
+ router_address: Optional[str] = None,
59
+ ):
60
+ """Initialize with an EVM signer.
61
+
62
+ Args:
63
+ signer: Any object implementing the EvmSigner protocol
64
+ router_address: Optional default router contract address
65
+ """
66
+ self._signer = signer
67
+ self._router_address = router_address
68
+
69
+ @property
70
+ def address(self) -> str:
71
+ """Get the signer's address."""
72
+ return self._signer.address
73
+
74
+ async def create_payment_payload(
75
+ self,
76
+ t402_version: int,
77
+ requirements: Union[UptoPaymentRequirements, PaymentRequirementsV2, Dict[str, Any]],
78
+ ) -> Dict[str, Any]:
79
+ """Create a payment payload for EVM upto scheme.
80
+
81
+ Creates an EIP-2612 Permit authorization and signs it.
82
+
83
+ Args:
84
+ t402_version: Protocol version (1 or 2)
85
+ requirements: Payment requirements with maxAmount, asset, payTo, etc.
86
+
87
+ Returns:
88
+ Dict with t402Version and payload containing permit signature
89
+ and authorization data.
90
+ """
91
+ # Extract requirements (handle both model and dict)
92
+ if hasattr(requirements, "model_dump"):
93
+ req = requirements.model_dump(by_alias=True)
94
+ else:
95
+ req = dict(requirements)
96
+
97
+ # Get network and chain ID
98
+ network = req.get("network", "")
99
+ chain_id = self._get_chain_id(network)
100
+
101
+ # Get maxAmount for upto scheme
102
+ max_amount = req.get("maxAmount") or req.get("max_amount", "0")
103
+
104
+ # Get router/spender address
105
+ extra = req.get("extra", {})
106
+ router_address = (
107
+ extra.get("routerAddress")
108
+ or extra.get("router_address")
109
+ or self._router_address
110
+ or req.get("payTo") # Fallback to payTo
111
+ )
112
+
113
+ # Get asset address
114
+ asset = req.get("asset", "")
115
+
116
+ # Get timeout
117
+ max_timeout = req.get("maxTimeoutSeconds") or req.get("max_timeout_seconds", 300)
118
+
119
+ # Get EIP-712 domain info
120
+ token_name = extra.get("name", "USD Coin")
121
+ token_version = extra.get("version", "2")
122
+
123
+ # Create payment nonce
124
+ payment_nonce = create_payment_nonce()
125
+
126
+ # Calculate deadline
127
+ deadline = int(time.time()) + max_timeout
128
+
129
+ # Get permit nonce from token contract (would need RPC call in production)
130
+ # For now, use 0 as placeholder - real implementation needs contract call
131
+ permit_nonce = 0
132
+
133
+ # Create authorization
134
+ authorization = {
135
+ "owner": self._signer.address,
136
+ "spender": router_address,
137
+ "value": str(max_amount),
138
+ "deadline": str(deadline),
139
+ "nonce": permit_nonce,
140
+ }
141
+
142
+ # Sign the permit
143
+ signature = self._sign_permit(
144
+ authorization=authorization,
145
+ chain_id=chain_id,
146
+ asset_address=asset,
147
+ token_name=token_name,
148
+ token_version=token_version,
149
+ )
150
+
151
+ # Build payload
152
+ payload = {
153
+ "signature": signature,
154
+ "authorization": authorization,
155
+ "paymentNonce": f"0x{payment_nonce.hex()}",
156
+ }
157
+
158
+ return {
159
+ "t402Version": t402_version,
160
+ "payload": payload,
161
+ }
162
+
163
+ def _get_chain_id(self, network: str) -> int:
164
+ """Get chain ID from network identifier."""
165
+ if network.startswith("eip155:"):
166
+ return int(network.split(":")[1])
167
+
168
+ try:
169
+ return get_chain_id(network)
170
+ except (KeyError, ValueError):
171
+ raise ValueError(f"Unknown network: {network}")
172
+
173
+ def _sign_permit(
174
+ self,
175
+ authorization: Dict[str, Any],
176
+ chain_id: int,
177
+ asset_address: str,
178
+ token_name: str,
179
+ token_version: str,
180
+ ) -> Dict[str, Any]:
181
+ """Sign an EIP-2612 Permit.
182
+
183
+ Args:
184
+ authorization: Permit authorization data
185
+ chain_id: EVM chain ID
186
+ asset_address: Token contract address
187
+ token_name: Token name for EIP-712 domain
188
+ token_version: Token version for EIP-712 domain
189
+
190
+ Returns:
191
+ Signature as dict with v, r, s components
192
+ """
193
+ # Build EIP-712 typed data
194
+ domain = {
195
+ "name": token_name,
196
+ "version": token_version,
197
+ "chainId": chain_id,
198
+ "verifyingContract": asset_address,
199
+ }
200
+
201
+ types = {
202
+ "Permit": [
203
+ {"name": "owner", "type": "address"},
204
+ {"name": "spender", "type": "address"},
205
+ {"name": "value", "type": "uint256"},
206
+ {"name": "nonce", "type": "uint256"},
207
+ {"name": "deadline", "type": "uint256"},
208
+ ]
209
+ }
210
+
211
+ message = {
212
+ "owner": authorization["owner"],
213
+ "spender": authorization["spender"],
214
+ "value": int(authorization["value"]),
215
+ "nonce": authorization["nonce"],
216
+ "deadline": int(authorization["deadline"]),
217
+ }
218
+
219
+ # Sign
220
+ signed = self._signer.sign_typed_data(
221
+ domain_data=domain,
222
+ message_types=types,
223
+ message_data=message,
224
+ )
225
+
226
+ # Extract signature components
227
+ sig_hex = signed.signature.hex()
228
+ if sig_hex.startswith("0x"):
229
+ sig_hex = sig_hex[2:]
230
+
231
+ # Split into v, r, s
232
+ r = f"0x{sig_hex[:64]}"
233
+ s = f"0x{sig_hex[64:128]}"
234
+ v = int(sig_hex[128:], 16) if len(sig_hex) > 128 else 27
235
+
236
+ return {
237
+ "v": v,
238
+ "r": r,
239
+ "s": s,
240
+ }
@@ -0,0 +1,305 @@
1
+ """EVM Up-To Scheme Types.
2
+
3
+ EVM-specific types for the up-to payment scheme using EIP-2612 Permit.
4
+ The Permit standard allows gasless token approvals via signature.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, List, Optional
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+ from pydantic.alias_generators import to_camel
12
+
13
+
14
+ # EIP-712 Permit type definitions
15
+ PERMIT_TYPES: Dict[str, List[Dict[str, str]]] = {
16
+ "Permit": [
17
+ {"name": "owner", "type": "address"},
18
+ {"name": "spender", "type": "address"},
19
+ {"name": "value", "type": "uint256"},
20
+ {"name": "nonce", "type": "uint256"},
21
+ {"name": "deadline", "type": "uint256"},
22
+ ]
23
+ }
24
+
25
+ PERMIT_DOMAIN_TYPES: List[Dict[str, str]] = [
26
+ {"name": "name", "type": "string"},
27
+ {"name": "version", "type": "string"},
28
+ {"name": "chainId", "type": "uint256"},
29
+ {"name": "verifyingContract", "type": "address"},
30
+ ]
31
+
32
+
33
+ class PermitSignature(BaseModel):
34
+ """EIP-2612 permit signature components."""
35
+
36
+ v: int = Field(description="Recovery id")
37
+ r: str = Field(description="First 32 bytes of signature")
38
+ s: str = Field(description="Second 32 bytes of signature")
39
+
40
+ model_config = ConfigDict(
41
+ alias_generator=to_camel,
42
+ populate_by_name=True,
43
+ from_attributes=True,
44
+ )
45
+
46
+
47
+ class PermitAuthorization(BaseModel):
48
+ """EIP-2612 permit authorization parameters."""
49
+
50
+ owner: str = Field(description="Token owner address")
51
+ spender: str = Field(description="Address authorized to spend (router contract)")
52
+ value: str = Field(description="Maximum authorized value")
53
+ deadline: str = Field(description="Permit deadline (unix timestamp)")
54
+ nonce: int = Field(description="Permit nonce from token contract")
55
+
56
+ model_config = ConfigDict(
57
+ alias_generator=to_camel,
58
+ populate_by_name=True,
59
+ from_attributes=True,
60
+ )
61
+
62
+
63
+ class UptoEIP2612Payload(BaseModel):
64
+ """Up-to payment payload using EIP-2612 Permit."""
65
+
66
+ signature: PermitSignature = Field(description="Permit signature components")
67
+ authorization: PermitAuthorization = Field(description="Permit parameters")
68
+ payment_nonce: str = Field(
69
+ alias="paymentNonce",
70
+ description="Unique nonce to prevent replay attacks",
71
+ )
72
+
73
+ model_config = ConfigDict(
74
+ alias_generator=to_camel,
75
+ populate_by_name=True,
76
+ from_attributes=True,
77
+ )
78
+
79
+ def to_dict(self) -> Dict[str, Any]:
80
+ """Convert to dictionary for JSON serialization."""
81
+ return {
82
+ "signature": {
83
+ "v": self.signature.v,
84
+ "r": self.signature.r,
85
+ "s": self.signature.s,
86
+ },
87
+ "authorization": {
88
+ "owner": self.authorization.owner,
89
+ "spender": self.authorization.spender,
90
+ "value": self.authorization.value,
91
+ "deadline": self.authorization.deadline,
92
+ "nonce": self.authorization.nonce,
93
+ },
94
+ "paymentNonce": self.payment_nonce,
95
+ }
96
+
97
+
98
+ class UptoCompactPayload(BaseModel):
99
+ """Alternative payload with combined signature."""
100
+
101
+ signature: str = Field(description="Combined EIP-2612 permit signature (65 bytes hex)")
102
+ authorization: PermitAuthorization = Field(description="Permit parameters")
103
+ payment_nonce: str = Field(
104
+ alias="paymentNonce",
105
+ description="Unique nonce to prevent replay attacks",
106
+ )
107
+
108
+ model_config = ConfigDict(
109
+ alias_generator=to_camel,
110
+ populate_by_name=True,
111
+ from_attributes=True,
112
+ )
113
+
114
+
115
+ class UptoEvmExtra(BaseModel):
116
+ """EVM-specific extra fields for the upto scheme."""
117
+
118
+ name: str = Field(description="EIP-712 domain name (token name)")
119
+ version: str = Field(description="EIP-712 domain version")
120
+ router_address: Optional[str] = Field(
121
+ default=None,
122
+ alias="routerAddress",
123
+ description="Upto router contract address",
124
+ )
125
+ unit: Optional[str] = Field(
126
+ default=None,
127
+ description="Billing unit (e.g., 'token', 'request')",
128
+ )
129
+ unit_price: Optional[str] = Field(
130
+ default=None,
131
+ alias="unitPrice",
132
+ description="Price per unit in smallest denomination",
133
+ )
134
+
135
+ model_config = ConfigDict(
136
+ alias_generator=to_camel,
137
+ populate_by_name=True,
138
+ from_attributes=True,
139
+ )
140
+
141
+
142
+ class UptoEvmSettlement(BaseModel):
143
+ """EVM-specific settlement request."""
144
+
145
+ settle_amount: str = Field(
146
+ alias="settleAmount",
147
+ description="Actual amount to settle",
148
+ )
149
+ usage_details: Optional["UptoEvmUsageDetails"] = Field(
150
+ default=None,
151
+ alias="usageDetails",
152
+ description="Optional usage information",
153
+ )
154
+
155
+ model_config = ConfigDict(
156
+ alias_generator=to_camel,
157
+ populate_by_name=True,
158
+ from_attributes=True,
159
+ )
160
+
161
+
162
+ class UptoEvmUsageDetails(BaseModel):
163
+ """Usage details for EVM settlement."""
164
+
165
+ units_consumed: Optional[int] = Field(
166
+ default=None,
167
+ alias="unitsConsumed",
168
+ description="Number of units consumed",
169
+ )
170
+ unit_price: Optional[str] = Field(
171
+ default=None,
172
+ alias="unitPrice",
173
+ description="Price per unit used",
174
+ )
175
+ unit_type: Optional[str] = Field(
176
+ default=None,
177
+ alias="unitType",
178
+ description="Type of unit",
179
+ )
180
+ start_time: Optional[int] = Field(
181
+ default=None,
182
+ alias="startTime",
183
+ description="Start timestamp",
184
+ )
185
+ end_time: Optional[int] = Field(
186
+ default=None,
187
+ alias="endTime",
188
+ description="End timestamp",
189
+ )
190
+
191
+ model_config = ConfigDict(
192
+ alias_generator=to_camel,
193
+ populate_by_name=True,
194
+ from_attributes=True,
195
+ )
196
+
197
+
198
+ # Update forward reference
199
+ UptoEvmSettlement.model_rebuild()
200
+
201
+
202
+ def is_eip2612_payload(data: Dict[str, Any]) -> bool:
203
+ """Check if the given data represents an EIP-2612 permit payload.
204
+
205
+ Args:
206
+ data: Dictionary to check
207
+
208
+ Returns:
209
+ True if data has the correct EIP-2612 structure
210
+ """
211
+ if not isinstance(data, dict):
212
+ return False
213
+
214
+ sig = data.get("signature")
215
+ auth = data.get("authorization")
216
+
217
+ if not sig or not auth:
218
+ return False
219
+
220
+ # Check signature structure (should be object with v, r, s)
221
+ if not isinstance(sig, dict):
222
+ return False
223
+ if not all(k in sig for k in ["v", "r", "s"]):
224
+ return False
225
+
226
+ # Check authorization structure
227
+ if not isinstance(auth, dict):
228
+ return False
229
+ required_auth_fields = ["owner", "spender", "value", "deadline"]
230
+ if not all(k in auth for k in required_auth_fields):
231
+ return False
232
+
233
+ return True
234
+
235
+
236
+ def create_permit_domain(
237
+ name: str,
238
+ version: str,
239
+ chain_id: int,
240
+ token_address: str,
241
+ ) -> Dict[str, Any]:
242
+ """Create an EIP-712 domain for permit signing.
243
+
244
+ Args:
245
+ name: Token name
246
+ version: Token version
247
+ chain_id: Chain ID
248
+ token_address: Token contract address
249
+
250
+ Returns:
251
+ EIP-712 domain dictionary
252
+ """
253
+ return {
254
+ "name": name,
255
+ "version": version,
256
+ "chainId": chain_id,
257
+ "verifyingContract": token_address,
258
+ }
259
+
260
+
261
+ def create_permit_message(authorization: PermitAuthorization) -> Dict[str, Any]:
262
+ """Create an EIP-712 message for permit signing.
263
+
264
+ Args:
265
+ authorization: Permit authorization parameters
266
+
267
+ Returns:
268
+ EIP-712 message dictionary
269
+ """
270
+ return {
271
+ "owner": authorization.owner,
272
+ "spender": authorization.spender,
273
+ "value": int(authorization.value),
274
+ "nonce": authorization.nonce,
275
+ "deadline": int(authorization.deadline),
276
+ }
277
+
278
+
279
+ def payload_from_dict(data: Dict[str, Any]) -> UptoEIP2612Payload:
280
+ """Create an UptoEIP2612Payload from a dictionary.
281
+
282
+ Args:
283
+ data: Dictionary containing payload data
284
+
285
+ Returns:
286
+ UptoEIP2612Payload instance
287
+ """
288
+ sig_data = data.get("signature", {})
289
+ auth_data = data.get("authorization", {})
290
+
291
+ return UptoEIP2612Payload(
292
+ signature=PermitSignature(
293
+ v=sig_data.get("v", 0),
294
+ r=sig_data.get("r", ""),
295
+ s=sig_data.get("s", ""),
296
+ ),
297
+ authorization=PermitAuthorization(
298
+ owner=auth_data.get("owner", ""),
299
+ spender=auth_data.get("spender", ""),
300
+ value=auth_data.get("value", ""),
301
+ deadline=auth_data.get("deadline", ""),
302
+ nonce=auth_data.get("nonce", 0),
303
+ ),
304
+ payment_nonce=data.get("paymentNonce", ""),
305
+ )