t402 1.9.0__py3-none-any.whl → 1.10.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.
- t402/__init__.py +2 -1
- t402/a2a/__init__.py +73 -0
- t402/a2a/helpers.py +158 -0
- t402/a2a/types.py +145 -0
- t402/bridge/client.py +13 -5
- t402/bridge/constants.py +4 -2
- t402/bridge/router.py +1 -1
- t402/bridge/scan.py +3 -1
- t402/chains.py +268 -1
- t402/cli.py +31 -9
- t402/common.py +2 -0
- t402/cosmos_paywall_template.py +2 -0
- t402/django/__init__.py +42 -0
- t402/django/middleware.py +596 -0
- t402/encoding.py +9 -3
- t402/erc4337/accounts.py +56 -51
- t402/erc4337/bundlers.py +105 -99
- t402/erc4337/paymasters.py +100 -109
- t402/erc4337/types.py +39 -26
- t402/errors.py +213 -0
- t402/evm_paywall_template.py +1 -1
- t402/facilitator.py +125 -0
- t402/fastapi/middleware.py +1 -3
- t402/mcp/constants.py +3 -6
- t402/mcp/server.py +501 -84
- t402/mcp/web3_utils.py +493 -0
- t402/multisig/__init__.py +120 -0
- t402/multisig/constants.py +54 -0
- t402/multisig/safe.py +441 -0
- t402/multisig/signature.py +228 -0
- t402/multisig/transaction.py +238 -0
- t402/multisig/types.py +108 -0
- t402/multisig/utils.py +77 -0
- t402/near_paywall_template.py +2 -0
- t402/networks.py +34 -1
- t402/paywall.py +1 -3
- t402/schemes/__init__.py +143 -0
- t402/schemes/aptos/__init__.py +70 -0
- t402/schemes/aptos/constants.py +349 -0
- t402/schemes/aptos/exact_direct/__init__.py +44 -0
- t402/schemes/aptos/exact_direct/client.py +202 -0
- t402/schemes/aptos/exact_direct/facilitator.py +426 -0
- t402/schemes/aptos/exact_direct/server.py +272 -0
- t402/schemes/aptos/types.py +237 -0
- t402/schemes/cosmos/__init__.py +114 -0
- t402/schemes/cosmos/constants.py +211 -0
- t402/schemes/cosmos/exact_direct/__init__.py +21 -0
- t402/schemes/cosmos/exact_direct/client.py +198 -0
- t402/schemes/cosmos/exact_direct/facilitator.py +493 -0
- t402/schemes/cosmos/exact_direct/server.py +315 -0
- t402/schemes/cosmos/types.py +501 -0
- t402/schemes/evm/__init__.py +46 -1
- t402/schemes/evm/exact/__init__.py +11 -0
- t402/schemes/evm/exact/client.py +3 -1
- t402/schemes/evm/exact/facilitator.py +894 -0
- t402/schemes/evm/exact/server.py +1 -1
- t402/schemes/evm/exact_legacy/__init__.py +38 -0
- t402/schemes/evm/exact_legacy/client.py +291 -0
- t402/schemes/evm/exact_legacy/facilitator.py +777 -0
- t402/schemes/evm/exact_legacy/server.py +231 -0
- t402/schemes/evm/upto/__init__.py +12 -0
- t402/schemes/evm/upto/client.py +6 -2
- t402/schemes/evm/upto/facilitator.py +625 -0
- t402/schemes/evm/upto/server.py +243 -0
- t402/schemes/evm/upto/types.py +3 -1
- t402/schemes/interfaces.py +6 -2
- t402/schemes/near/__init__.py +137 -0
- t402/schemes/near/constants.py +189 -0
- t402/schemes/near/exact_direct/__init__.py +21 -0
- t402/schemes/near/exact_direct/client.py +204 -0
- t402/schemes/near/exact_direct/facilitator.py +455 -0
- t402/schemes/near/exact_direct/server.py +303 -0
- t402/schemes/near/types.py +419 -0
- t402/schemes/near/upto/__init__.py +54 -0
- t402/schemes/near/upto/types.py +272 -0
- t402/schemes/polkadot/__init__.py +72 -0
- t402/schemes/polkadot/constants.py +155 -0
- t402/schemes/polkadot/exact_direct/__init__.py +43 -0
- t402/schemes/polkadot/exact_direct/client.py +235 -0
- t402/schemes/polkadot/exact_direct/facilitator.py +428 -0
- t402/schemes/polkadot/exact_direct/server.py +292 -0
- t402/schemes/polkadot/types.py +385 -0
- t402/schemes/registry.py +6 -2
- t402/schemes/stacks/__init__.py +68 -0
- t402/schemes/stacks/constants.py +122 -0
- t402/schemes/stacks/exact_direct/__init__.py +43 -0
- t402/schemes/stacks/exact_direct/client.py +222 -0
- t402/schemes/stacks/exact_direct/facilitator.py +424 -0
- t402/schemes/stacks/exact_direct/server.py +292 -0
- t402/schemes/stacks/types.py +380 -0
- t402/schemes/svm/__init__.py +44 -0
- t402/schemes/svm/exact/__init__.py +35 -0
- t402/schemes/svm/exact/client.py +23 -0
- t402/schemes/svm/exact/facilitator.py +24 -0
- t402/schemes/svm/exact/server.py +20 -0
- t402/schemes/svm/upto/__init__.py +23 -0
- t402/schemes/svm/upto/types.py +193 -0
- t402/schemes/tezos/__init__.py +84 -0
- t402/schemes/tezos/constants.py +372 -0
- t402/schemes/tezos/exact_direct/__init__.py +22 -0
- t402/schemes/tezos/exact_direct/client.py +226 -0
- t402/schemes/tezos/exact_direct/facilitator.py +491 -0
- t402/schemes/tezos/exact_direct/server.py +277 -0
- t402/schemes/tezos/types.py +220 -0
- t402/schemes/ton/__init__.py +24 -2
- t402/schemes/ton/exact/__init__.py +7 -0
- t402/schemes/ton/exact/facilitator.py +730 -0
- t402/schemes/ton/exact/server.py +1 -1
- t402/schemes/ton/upto/__init__.py +31 -0
- t402/schemes/ton/upto/types.py +215 -0
- t402/schemes/tron/__init__.py +28 -2
- t402/schemes/tron/exact/__init__.py +9 -0
- t402/schemes/tron/exact/facilitator.py +673 -0
- t402/schemes/tron/exact/server.py +1 -1
- t402/schemes/tron/upto/__init__.py +30 -0
- t402/schemes/tron/upto/types.py +213 -0
- t402/stacks_paywall_template.py +2 -0
- t402/starlette/__init__.py +38 -0
- t402/starlette/middleware.py +522 -0
- t402/svm.py +45 -11
- t402/svm_paywall_template.py +1 -1
- t402/ton.py +6 -2
- t402/ton_paywall_template.py +1 -192
- t402/tron.py +2 -0
- t402/tron_paywall_template.py +2 -0
- t402/types.py +103 -3
- t402/wdk/chains.py +1 -1
- t402/wdk/errors.py +15 -5
- t402/wdk/signer.py +11 -2
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/METADATA +42 -1
- t402-1.10.0.dist-info/RECORD +156 -0
- t402-1.9.0.dist-info/RECORD +0 -72
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/WHEEL +0 -0
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"""EVM Up-To Scheme - Facilitator Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the facilitator-side implementation of the upto payment
|
|
4
|
+
scheme for EVM networks using EIP-2612 Permit.
|
|
5
|
+
|
|
6
|
+
The facilitator:
|
|
7
|
+
1. Verifies EIP-2612 Permit signatures by recovering the signer via EIP-712
|
|
8
|
+
2. Validates permit parameters (value, spender, deadline)
|
|
9
|
+
3. Settles payments by calling permit() then transferFrom() on the token contract
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any, Dict, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
from eth_account.messages import encode_typed_data
|
|
19
|
+
from eth_account import Account
|
|
20
|
+
|
|
21
|
+
from t402.types import (
|
|
22
|
+
PaymentRequirementsV2,
|
|
23
|
+
PaymentPayloadV2,
|
|
24
|
+
VerifyResponse,
|
|
25
|
+
SettleResponse,
|
|
26
|
+
Network,
|
|
27
|
+
)
|
|
28
|
+
from t402.schemes.evm.upto.types import (
|
|
29
|
+
PERMIT_TYPES,
|
|
30
|
+
is_eip2612_payload,
|
|
31
|
+
create_permit_domain,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# Constants
|
|
38
|
+
SCHEME_UPTO = "upto"
|
|
39
|
+
|
|
40
|
+
# Minimal ERC-20 + EIP-2612 ABI for permit and transferFrom
|
|
41
|
+
ERC20_PERMIT_ABI = [
|
|
42
|
+
{
|
|
43
|
+
"inputs": [
|
|
44
|
+
{"name": "owner", "type": "address"},
|
|
45
|
+
{"name": "spender", "type": "address"},
|
|
46
|
+
{"name": "value", "type": "uint256"},
|
|
47
|
+
{"name": "deadline", "type": "uint256"},
|
|
48
|
+
{"name": "v", "type": "uint8"},
|
|
49
|
+
{"name": "r", "type": "bytes32"},
|
|
50
|
+
{"name": "s", "type": "bytes32"},
|
|
51
|
+
],
|
|
52
|
+
"name": "permit",
|
|
53
|
+
"outputs": [],
|
|
54
|
+
"stateMutability": "nonpayable",
|
|
55
|
+
"type": "function",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"inputs": [
|
|
59
|
+
{"name": "from", "type": "address"},
|
|
60
|
+
{"name": "to", "type": "address"},
|
|
61
|
+
{"name": "amount", "type": "uint256"},
|
|
62
|
+
],
|
|
63
|
+
"name": "transferFrom",
|
|
64
|
+
"outputs": [{"name": "", "type": "bool"}],
|
|
65
|
+
"stateMutability": "nonpayable",
|
|
66
|
+
"type": "function",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"inputs": [{"name": "owner", "type": "address"}],
|
|
70
|
+
"name": "nonces",
|
|
71
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
72
|
+
"stateMutability": "view",
|
|
73
|
+
"type": "function",
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class UptoEvmFacilitatorScheme:
|
|
79
|
+
"""Facilitator scheme for EVM upto payments using EIP-2612 Permit.
|
|
80
|
+
|
|
81
|
+
Verifies EIP-2612 Permit signatures off-chain and settles payments
|
|
82
|
+
on-chain by calling permit() followed by transferFrom() on the
|
|
83
|
+
token contract.
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
```python
|
|
87
|
+
from web3 import Web3
|
|
88
|
+
|
|
89
|
+
w3 = Web3(Web3.HTTPProvider("https://mainnet.base.org"))
|
|
90
|
+
facilitator = UptoEvmFacilitatorScheme(
|
|
91
|
+
web3=w3,
|
|
92
|
+
private_key="0x...",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Verify a permit signature
|
|
96
|
+
result = await facilitator.verify(payload, requirements)
|
|
97
|
+
if result.is_valid:
|
|
98
|
+
# Settle the payment
|
|
99
|
+
settlement = await facilitator.settle(payload, requirements)
|
|
100
|
+
```
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
scheme = SCHEME_UPTO
|
|
104
|
+
caip_family = "eip155:*"
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
web3: Optional[Any] = None,
|
|
109
|
+
private_key: Optional[str] = None,
|
|
110
|
+
address: Optional[str] = None,
|
|
111
|
+
):
|
|
112
|
+
"""Initialize the facilitator.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
web3: Web3 instance for on-chain interactions.
|
|
116
|
+
Required for settle(), optional for verify().
|
|
117
|
+
private_key: Private key for signing transactions.
|
|
118
|
+
Required for settle().
|
|
119
|
+
address: Facilitator address (derived from private_key if not provided).
|
|
120
|
+
This is the address that acts as the spender in permits.
|
|
121
|
+
"""
|
|
122
|
+
self._web3 = web3
|
|
123
|
+
self._private_key = private_key
|
|
124
|
+
|
|
125
|
+
if address:
|
|
126
|
+
self._address = address
|
|
127
|
+
elif private_key:
|
|
128
|
+
acct = Account.from_key(private_key)
|
|
129
|
+
self._address = acct.address
|
|
130
|
+
else:
|
|
131
|
+
self._address = None
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def address(self) -> Optional[str]:
|
|
135
|
+
"""Get the facilitator's address (spender in permits)."""
|
|
136
|
+
return self._address
|
|
137
|
+
|
|
138
|
+
def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
|
|
139
|
+
"""Get mechanism-specific extra data for supported kinds.
|
|
140
|
+
|
|
141
|
+
Returns the router/spender address that clients should use
|
|
142
|
+
in their Permit authorization.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
network: The network identifier
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dict with routerAddress if address is configured, else None
|
|
149
|
+
"""
|
|
150
|
+
if self._address:
|
|
151
|
+
return {"routerAddress": self._address}
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def get_signers(self, network: Network) -> List[str]:
|
|
155
|
+
"""Get signer addresses for this facilitator.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
network: The network identifier
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List containing the facilitator address
|
|
162
|
+
"""
|
|
163
|
+
if self._address:
|
|
164
|
+
return [self._address]
|
|
165
|
+
return []
|
|
166
|
+
|
|
167
|
+
async def verify(
|
|
168
|
+
self,
|
|
169
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
170
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
171
|
+
) -> VerifyResponse:
|
|
172
|
+
"""Verify an EIP-2612 Permit payment payload.
|
|
173
|
+
|
|
174
|
+
Validates:
|
|
175
|
+
1. Payload has correct EIP-2612 structure
|
|
176
|
+
2. Permit value >= required amount
|
|
177
|
+
3. Spender matches facilitator address (if configured)
|
|
178
|
+
4. Deadline is in the future
|
|
179
|
+
5. Signature recovers to the claimed owner
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
payload: The payment payload containing permit signature
|
|
183
|
+
requirements: The payment requirements to verify against
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
VerifyResponse indicating validity and payer address
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
# Extract payload data
|
|
190
|
+
payload_data = self._extract_payload(payload)
|
|
191
|
+
req_data = self._extract_requirements(requirements)
|
|
192
|
+
|
|
193
|
+
# Validate payload structure
|
|
194
|
+
if not is_eip2612_payload(payload_data):
|
|
195
|
+
return VerifyResponse(
|
|
196
|
+
is_valid=False,
|
|
197
|
+
invalid_reason="Invalid EIP-2612 payload structure",
|
|
198
|
+
payer=None,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
signature = payload_data["signature"]
|
|
202
|
+
authorization = payload_data["authorization"]
|
|
203
|
+
|
|
204
|
+
owner = authorization["owner"]
|
|
205
|
+
spender = authorization["spender"]
|
|
206
|
+
value = int(authorization["value"])
|
|
207
|
+
deadline = int(authorization["deadline"])
|
|
208
|
+
nonce = authorization.get("nonce", 0)
|
|
209
|
+
|
|
210
|
+
# Validate deadline is in the future
|
|
211
|
+
now = int(time.time())
|
|
212
|
+
if deadline <= now:
|
|
213
|
+
return VerifyResponse(
|
|
214
|
+
is_valid=False,
|
|
215
|
+
invalid_reason=f"Permit deadline has passed: {deadline} <= {now}",
|
|
216
|
+
payer=owner,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Validate value >= required amount
|
|
220
|
+
required_amount = int(
|
|
221
|
+
req_data.get("amount")
|
|
222
|
+
or req_data.get("maxAmount")
|
|
223
|
+
or req_data.get("max_amount", "0")
|
|
224
|
+
)
|
|
225
|
+
if value < required_amount:
|
|
226
|
+
return VerifyResponse(
|
|
227
|
+
is_valid=False,
|
|
228
|
+
invalid_reason=(
|
|
229
|
+
f"Permit value {value} is less than required amount "
|
|
230
|
+
f"{required_amount}"
|
|
231
|
+
),
|
|
232
|
+
payer=owner,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Validate spender matches facilitator address
|
|
236
|
+
if self._address and spender.lower() != self._address.lower():
|
|
237
|
+
return VerifyResponse(
|
|
238
|
+
is_valid=False,
|
|
239
|
+
invalid_reason=(
|
|
240
|
+
f"Permit spender {spender} does not match facilitator "
|
|
241
|
+
f"address {self._address}"
|
|
242
|
+
),
|
|
243
|
+
payer=owner,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Recover signer from EIP-712 signature
|
|
247
|
+
network = req_data.get("network", "")
|
|
248
|
+
asset = req_data.get("asset", "")
|
|
249
|
+
extra = req_data.get("extra", {})
|
|
250
|
+
|
|
251
|
+
chain_id = self._get_chain_id(network)
|
|
252
|
+
token_name = extra.get("name", "TetherToken")
|
|
253
|
+
token_version = extra.get("version", "1")
|
|
254
|
+
|
|
255
|
+
recovered = self._recover_permit_signer(
|
|
256
|
+
owner=owner,
|
|
257
|
+
spender=spender,
|
|
258
|
+
value=value,
|
|
259
|
+
nonce=nonce,
|
|
260
|
+
deadline=deadline,
|
|
261
|
+
signature=signature,
|
|
262
|
+
chain_id=chain_id,
|
|
263
|
+
token_address=asset,
|
|
264
|
+
token_name=token_name,
|
|
265
|
+
token_version=token_version,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if recovered is None:
|
|
269
|
+
return VerifyResponse(
|
|
270
|
+
is_valid=False,
|
|
271
|
+
invalid_reason="Failed to recover signer from permit signature",
|
|
272
|
+
payer=owner,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Validate recovered address matches owner
|
|
276
|
+
if recovered.lower() != owner.lower():
|
|
277
|
+
return VerifyResponse(
|
|
278
|
+
is_valid=False,
|
|
279
|
+
invalid_reason=(
|
|
280
|
+
f"Recovered signer {recovered} does not match "
|
|
281
|
+
f"claimed owner {owner}"
|
|
282
|
+
),
|
|
283
|
+
payer=owner,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return VerifyResponse(
|
|
287
|
+
is_valid=True,
|
|
288
|
+
invalid_reason=None,
|
|
289
|
+
payer=owner,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.error(f"Permit verification failed: {e}")
|
|
294
|
+
return VerifyResponse(
|
|
295
|
+
is_valid=False,
|
|
296
|
+
invalid_reason=f"Verification error: {str(e)}",
|
|
297
|
+
payer=None,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
async def settle(
|
|
301
|
+
self,
|
|
302
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
303
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
304
|
+
settle_amount: Optional[str] = None,
|
|
305
|
+
) -> SettleResponse:
|
|
306
|
+
"""Settle an EIP-2612 Permit payment on-chain.
|
|
307
|
+
|
|
308
|
+
Executes two transactions:
|
|
309
|
+
1. token.permit(owner, spender, value, deadline, v, r, s)
|
|
310
|
+
2. token.transferFrom(owner, payTo, settleAmount)
|
|
311
|
+
|
|
312
|
+
The settle_amount can be less than or equal to the permitted value,
|
|
313
|
+
enabling usage-based billing.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
payload: The verified payment payload with permit signature
|
|
317
|
+
requirements: The payment requirements
|
|
318
|
+
settle_amount: Amount to actually settle (defaults to required amount).
|
|
319
|
+
Must be <= permitted value.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
SettleResponse with transaction hash and status
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
RuntimeError: If web3 or private_key is not configured
|
|
326
|
+
"""
|
|
327
|
+
if not self._web3:
|
|
328
|
+
return SettleResponse(
|
|
329
|
+
success=False,
|
|
330
|
+
error_reason="Web3 instance not configured",
|
|
331
|
+
transaction=None,
|
|
332
|
+
network=None,
|
|
333
|
+
payer=None,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if not self._private_key:
|
|
337
|
+
return SettleResponse(
|
|
338
|
+
success=False,
|
|
339
|
+
error_reason="Private key not configured for settlement",
|
|
340
|
+
transaction=None,
|
|
341
|
+
network=None,
|
|
342
|
+
payer=None,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
# Extract data
|
|
347
|
+
payload_data = self._extract_payload(payload)
|
|
348
|
+
req_data = self._extract_requirements(requirements)
|
|
349
|
+
|
|
350
|
+
signature = payload_data["signature"]
|
|
351
|
+
authorization = payload_data["authorization"]
|
|
352
|
+
|
|
353
|
+
owner = authorization["owner"]
|
|
354
|
+
spender = authorization["spender"]
|
|
355
|
+
value = int(authorization["value"])
|
|
356
|
+
deadline = int(authorization["deadline"])
|
|
357
|
+
|
|
358
|
+
# Get signature components
|
|
359
|
+
v = signature["v"]
|
|
360
|
+
r = signature["r"]
|
|
361
|
+
s = signature["s"]
|
|
362
|
+
|
|
363
|
+
# Convert r, s to bytes32
|
|
364
|
+
r_bytes = bytes.fromhex(r[2:] if r.startswith("0x") else r).rjust(32, b'\x00')
|
|
365
|
+
s_bytes = bytes.fromhex(s[2:] if s.startswith("0x") else s).rjust(32, b'\x00')
|
|
366
|
+
|
|
367
|
+
# Determine settle amount
|
|
368
|
+
network = req_data.get("network", "")
|
|
369
|
+
asset = req_data.get("asset", "")
|
|
370
|
+
pay_to = req_data.get("payTo") or req_data.get("pay_to", "")
|
|
371
|
+
|
|
372
|
+
if settle_amount is not None:
|
|
373
|
+
actual_amount = int(settle_amount)
|
|
374
|
+
else:
|
|
375
|
+
actual_amount = int(
|
|
376
|
+
req_data.get("amount")
|
|
377
|
+
or req_data.get("maxAmount")
|
|
378
|
+
or req_data.get("max_amount", "0")
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Validate settle amount doesn't exceed permit value
|
|
382
|
+
if actual_amount > value:
|
|
383
|
+
return SettleResponse(
|
|
384
|
+
success=False,
|
|
385
|
+
error_reason=(
|
|
386
|
+
f"Settle amount {actual_amount} exceeds permitted "
|
|
387
|
+
f"value {value}"
|
|
388
|
+
),
|
|
389
|
+
transaction=None,
|
|
390
|
+
network=network,
|
|
391
|
+
payer=owner,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Get token contract
|
|
395
|
+
token_contract = self._web3.eth.contract(
|
|
396
|
+
address=self._web3.to_checksum_address(asset),
|
|
397
|
+
abi=ERC20_PERMIT_ABI,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Get account from private key
|
|
401
|
+
account = Account.from_key(self._private_key)
|
|
402
|
+
nonce = self._web3.eth.get_transaction_count(account.address)
|
|
403
|
+
|
|
404
|
+
# Build and send permit transaction
|
|
405
|
+
permit_tx = token_contract.functions.permit(
|
|
406
|
+
self._web3.to_checksum_address(owner),
|
|
407
|
+
self._web3.to_checksum_address(spender),
|
|
408
|
+
value,
|
|
409
|
+
deadline,
|
|
410
|
+
v,
|
|
411
|
+
r_bytes,
|
|
412
|
+
s_bytes,
|
|
413
|
+
).build_transaction({
|
|
414
|
+
"from": account.address,
|
|
415
|
+
"nonce": nonce,
|
|
416
|
+
"gas": 100000,
|
|
417
|
+
"gasPrice": self._web3.eth.gas_price,
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
signed_permit = self._web3.eth.account.sign_transaction(
|
|
421
|
+
permit_tx, self._private_key
|
|
422
|
+
)
|
|
423
|
+
permit_tx_hash = self._web3.eth.send_raw_transaction(
|
|
424
|
+
signed_permit.raw_transaction
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Wait for permit confirmation
|
|
428
|
+
self._web3.eth.wait_for_transaction_receipt(permit_tx_hash)
|
|
429
|
+
|
|
430
|
+
# Build and send transferFrom transaction
|
|
431
|
+
nonce += 1
|
|
432
|
+
transfer_tx = token_contract.functions.transferFrom(
|
|
433
|
+
self._web3.to_checksum_address(owner),
|
|
434
|
+
self._web3.to_checksum_address(pay_to),
|
|
435
|
+
actual_amount,
|
|
436
|
+
).build_transaction({
|
|
437
|
+
"from": account.address,
|
|
438
|
+
"nonce": nonce,
|
|
439
|
+
"gas": 100000,
|
|
440
|
+
"gasPrice": self._web3.eth.gas_price,
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
signed_transfer = self._web3.eth.account.sign_transaction(
|
|
444
|
+
transfer_tx, self._private_key
|
|
445
|
+
)
|
|
446
|
+
transfer_tx_hash = self._web3.eth.send_raw_transaction(
|
|
447
|
+
signed_transfer.raw_transaction
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Wait for transfer confirmation
|
|
451
|
+
receipt = self._web3.eth.wait_for_transaction_receipt(transfer_tx_hash)
|
|
452
|
+
|
|
453
|
+
tx_hash_hex = receipt.transactionHash.hex()
|
|
454
|
+
if not tx_hash_hex.startswith("0x"):
|
|
455
|
+
tx_hash_hex = f"0x{tx_hash_hex}"
|
|
456
|
+
|
|
457
|
+
return SettleResponse(
|
|
458
|
+
success=True,
|
|
459
|
+
error_reason=None,
|
|
460
|
+
transaction=tx_hash_hex,
|
|
461
|
+
network=network,
|
|
462
|
+
payer=owner,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
except Exception as e:
|
|
466
|
+
logger.error(f"Permit settlement failed: {e}")
|
|
467
|
+
return SettleResponse(
|
|
468
|
+
success=False,
|
|
469
|
+
error_reason=f"Settlement error: {str(e)}",
|
|
470
|
+
transaction=None,
|
|
471
|
+
network=req_data.get("network") if 'req_data' in dir() else None,
|
|
472
|
+
payer=None,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def _recover_permit_signer(
|
|
476
|
+
self,
|
|
477
|
+
owner: str,
|
|
478
|
+
spender: str,
|
|
479
|
+
value: int,
|
|
480
|
+
nonce: int,
|
|
481
|
+
deadline: int,
|
|
482
|
+
signature: Dict[str, Any],
|
|
483
|
+
chain_id: int,
|
|
484
|
+
token_address: str,
|
|
485
|
+
token_name: str,
|
|
486
|
+
token_version: str,
|
|
487
|
+
) -> Optional[str]:
|
|
488
|
+
"""Recover the signer address from an EIP-2612 Permit signature.
|
|
489
|
+
|
|
490
|
+
Uses EIP-712 typed data to reconstruct the signing payload and
|
|
491
|
+
recover the signer's address from the v, r, s signature components.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
owner: Token owner address
|
|
495
|
+
spender: Approved spender address
|
|
496
|
+
value: Permitted value
|
|
497
|
+
nonce: Permit nonce
|
|
498
|
+
deadline: Permit deadline
|
|
499
|
+
signature: Dict with v, r, s components
|
|
500
|
+
chain_id: EVM chain ID
|
|
501
|
+
token_address: Token contract address
|
|
502
|
+
token_name: Token name for EIP-712 domain
|
|
503
|
+
token_version: Token version for EIP-712 domain
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
Recovered address as string, or None if recovery fails
|
|
507
|
+
"""
|
|
508
|
+
try:
|
|
509
|
+
# Build EIP-712 domain
|
|
510
|
+
domain = create_permit_domain(
|
|
511
|
+
name=token_name,
|
|
512
|
+
version=token_version,
|
|
513
|
+
chain_id=chain_id,
|
|
514
|
+
token_address=token_address,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Build EIP-712 message
|
|
518
|
+
message = {
|
|
519
|
+
"owner": owner,
|
|
520
|
+
"spender": spender,
|
|
521
|
+
"value": value,
|
|
522
|
+
"nonce": nonce,
|
|
523
|
+
"deadline": deadline,
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# Reconstruct the signature bytes
|
|
527
|
+
v = signature["v"]
|
|
528
|
+
r = signature["r"]
|
|
529
|
+
s = signature["s"]
|
|
530
|
+
|
|
531
|
+
# Normalize r and s to hex without 0x prefix
|
|
532
|
+
r_hex = r[2:] if isinstance(r, str) and r.startswith("0x") else str(r)
|
|
533
|
+
s_hex = s[2:] if isinstance(s, str) and s.startswith("0x") else str(s)
|
|
534
|
+
|
|
535
|
+
# Pad to 64 hex chars (32 bytes)
|
|
536
|
+
r_hex = r_hex.zfill(64)
|
|
537
|
+
s_hex = s_hex.zfill(64)
|
|
538
|
+
|
|
539
|
+
# Build combined signature: r (32 bytes) + s (32 bytes) + v (1 byte)
|
|
540
|
+
v_hex = format(v, "02x")
|
|
541
|
+
sig_hex = f"0x{r_hex}{s_hex}{v_hex}"
|
|
542
|
+
|
|
543
|
+
# Encode EIP-712 typed data (full_types kept for reference)
|
|
544
|
+
_full_types = {
|
|
545
|
+
"EIP712Domain": [
|
|
546
|
+
{"name": "name", "type": "string"},
|
|
547
|
+
{"name": "version", "type": "string"},
|
|
548
|
+
{"name": "chainId", "type": "uint256"},
|
|
549
|
+
{"name": "verifyingContract", "type": "address"},
|
|
550
|
+
],
|
|
551
|
+
"Permit": PERMIT_TYPES["Permit"],
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
signable = encode_typed_data(
|
|
555
|
+
domain_data=domain,
|
|
556
|
+
message_types={"Permit": PERMIT_TYPES["Permit"]},
|
|
557
|
+
message_data=message,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Recover signer
|
|
561
|
+
recovered = Account.recover_message(
|
|
562
|
+
signable,
|
|
563
|
+
signature=bytes.fromhex(sig_hex[2:]),
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
return recovered
|
|
567
|
+
|
|
568
|
+
except Exception as e:
|
|
569
|
+
logger.debug(f"Permit signer recovery failed: {e}")
|
|
570
|
+
return None
|
|
571
|
+
|
|
572
|
+
def _extract_payload(self, payload: Union[PaymentPayloadV2, Dict[str, Any]]) -> Dict[str, Any]:
|
|
573
|
+
"""Extract payload data as a dict.
|
|
574
|
+
|
|
575
|
+
Handles both PaymentPayloadV2 models (where the inner payload is
|
|
576
|
+
in the 'payload' field) and plain dicts.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
payload: Payment payload (model or dict)
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Dict containing signature and authorization data
|
|
583
|
+
"""
|
|
584
|
+
if hasattr(payload, "model_dump"):
|
|
585
|
+
data = payload.model_dump(by_alias=True)
|
|
586
|
+
return data.get("payload", data)
|
|
587
|
+
elif isinstance(payload, dict):
|
|
588
|
+
return payload.get("payload", payload)
|
|
589
|
+
return dict(payload)
|
|
590
|
+
|
|
591
|
+
def _extract_requirements(
|
|
592
|
+
self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
|
|
593
|
+
) -> Dict[str, Any]:
|
|
594
|
+
"""Extract requirements data as a dict.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
requirements: Payment requirements (model or dict)
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Dict containing requirement fields
|
|
601
|
+
"""
|
|
602
|
+
if hasattr(requirements, "model_dump"):
|
|
603
|
+
return requirements.model_dump(by_alias=True)
|
|
604
|
+
return dict(requirements)
|
|
605
|
+
|
|
606
|
+
def _get_chain_id(self, network: str) -> int:
|
|
607
|
+
"""Get chain ID from network identifier.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
network: Network identifier (CAIP-2 or legacy format)
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
Chain ID as integer
|
|
614
|
+
|
|
615
|
+
Raises:
|
|
616
|
+
ValueError: If the network format is unrecognized
|
|
617
|
+
"""
|
|
618
|
+
if network.startswith("eip155:"):
|
|
619
|
+
return int(network.split(":")[1])
|
|
620
|
+
|
|
621
|
+
from t402.chains import get_chain_id
|
|
622
|
+
try:
|
|
623
|
+
return int(get_chain_id(network))
|
|
624
|
+
except (KeyError, ValueError):
|
|
625
|
+
raise ValueError(f"Unknown network: {network}")
|