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,777 @@
|
|
|
1
|
+
"""EVM Exact-Legacy Scheme - Facilitator Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the facilitator-side implementation of the exact-legacy payment
|
|
4
|
+
scheme for EVM networks using the approve + transferFrom pattern.
|
|
5
|
+
|
|
6
|
+
This scheme is for legacy USDT and other tokens without EIP-3009 support.
|
|
7
|
+
|
|
8
|
+
.. deprecated:: 2.3.0
|
|
9
|
+
The exact-legacy scheme is deprecated in favor of using USDT0 with the "exact" scheme.
|
|
10
|
+
USDT0 supports EIP-3009 for gasless transfers on 19+ chains via LayerZero.
|
|
11
|
+
|
|
12
|
+
See server.py docstring for full deprecation details and migration guide.
|
|
13
|
+
|
|
14
|
+
The facilitator:
|
|
15
|
+
1. Verifies LegacyTransferAuthorization signatures off-chain
|
|
16
|
+
2. Checks that the user has approved the facilitator to spend their tokens
|
|
17
|
+
3. Settles payments by calling transferFrom on the token contract
|
|
18
|
+
4. Waits for transaction confirmation
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import time
|
|
25
|
+
from typing import Any, Dict, List, Optional, Protocol, Union, runtime_checkable
|
|
26
|
+
|
|
27
|
+
from t402.types import (
|
|
28
|
+
PaymentRequirementsV2,
|
|
29
|
+
PaymentPayloadV2,
|
|
30
|
+
VerifyResponse,
|
|
31
|
+
SettleResponse,
|
|
32
|
+
Network,
|
|
33
|
+
)
|
|
34
|
+
from t402.chains import KNOWN_TOKENS
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
# Constants
|
|
40
|
+
SCHEME_EXACT_LEGACY = "exact-legacy"
|
|
41
|
+
CAIP_FAMILY = "eip155:*"
|
|
42
|
+
|
|
43
|
+
# Minimum time buffer (seconds) before validBefore deadline
|
|
44
|
+
MIN_VALIDITY_BUFFER = 30
|
|
45
|
+
|
|
46
|
+
# Default timeout for transaction confirmation (milliseconds)
|
|
47
|
+
DEFAULT_CONFIRMATION_TIMEOUT = 60000
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@runtime_checkable
|
|
51
|
+
class FacilitatorLegacyEvmSigner(Protocol):
|
|
52
|
+
"""Protocol for EVM legacy facilitator signer operations.
|
|
53
|
+
|
|
54
|
+
Implementations should provide address retrieval, legacy authorization
|
|
55
|
+
signature verification, allowance checking, transferFrom execution,
|
|
56
|
+
transaction confirmation, and balance checking capabilities.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def get_addresses(self, network: str) -> List[str]:
|
|
60
|
+
"""Return all facilitator addresses for the given network.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
network: Network identifier (CAIP-2 format, e.g., "eip155:1")
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of Ethereum addresses (checksummed or lowercase hex)
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
async def verify_legacy_authorization(
|
|
71
|
+
self,
|
|
72
|
+
from_address: str,
|
|
73
|
+
to_address: str,
|
|
74
|
+
value: str,
|
|
75
|
+
valid_after: str,
|
|
76
|
+
valid_before: str,
|
|
77
|
+
nonce: str,
|
|
78
|
+
spender: str,
|
|
79
|
+
signature: str,
|
|
80
|
+
token_address: str,
|
|
81
|
+
chain_id: int,
|
|
82
|
+
token_name: str,
|
|
83
|
+
token_version: str,
|
|
84
|
+
) -> "LegacyVerifyResult":
|
|
85
|
+
"""Verify a LegacyTransferAuthorization signature.
|
|
86
|
+
|
|
87
|
+
Reconstructs the EIP-712 typed data hash and recovers the signer
|
|
88
|
+
address from the signature, comparing it with the expected from_address.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
from_address: Expected signer/payer address
|
|
92
|
+
to_address: Recipient address
|
|
93
|
+
value: Transfer amount in token's smallest unit
|
|
94
|
+
valid_after: Unix timestamp after which authorization is valid
|
|
95
|
+
valid_before: Unix timestamp before which authorization is valid
|
|
96
|
+
nonce: 32-byte nonce as hex string (0x-prefixed)
|
|
97
|
+
spender: Authorized spender address (facilitator)
|
|
98
|
+
signature: ECDSA signature as hex string (0x-prefixed, 65 bytes)
|
|
99
|
+
token_address: ERC-20 token contract address
|
|
100
|
+
chain_id: EVM chain ID
|
|
101
|
+
token_name: Token name for EIP-712 domain
|
|
102
|
+
token_version: Token version for EIP-712 domain
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
LegacyVerifyResult indicating validity and recovered address
|
|
106
|
+
"""
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
async def get_allowance(
|
|
110
|
+
self,
|
|
111
|
+
owner_address: str,
|
|
112
|
+
spender_address: str,
|
|
113
|
+
token_address: str,
|
|
114
|
+
network: str,
|
|
115
|
+
) -> str:
|
|
116
|
+
"""Get the ERC-20 allowance for a spender.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
owner_address: Token owner address
|
|
120
|
+
spender_address: Spender address (facilitator)
|
|
121
|
+
token_address: ERC-20 token contract address
|
|
122
|
+
network: Network identifier
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Allowance amount as string
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
async def execute_transfer_from(
|
|
130
|
+
self,
|
|
131
|
+
from_address: str,
|
|
132
|
+
to_address: str,
|
|
133
|
+
value: str,
|
|
134
|
+
token_address: str,
|
|
135
|
+
network: str,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""Execute transferFrom on the token contract.
|
|
138
|
+
|
|
139
|
+
Calls the ERC-20 transferFrom function to transfer tokens
|
|
140
|
+
from the payer to the recipient.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
from_address: Payer address (token holder who approved)
|
|
144
|
+
to_address: Recipient address
|
|
145
|
+
value: Transfer amount in token's smallest unit
|
|
146
|
+
token_address: ERC-20 token contract address
|
|
147
|
+
network: Network identifier (CAIP-2 format)
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Transaction hash as hex string (0x-prefixed)
|
|
151
|
+
"""
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
async def wait_for_confirmation(
|
|
155
|
+
self,
|
|
156
|
+
tx_hash: str,
|
|
157
|
+
network: str,
|
|
158
|
+
timeout_ms: int = 60000,
|
|
159
|
+
) -> "LegacyTransactionConfirmation":
|
|
160
|
+
"""Wait for a transaction to be confirmed (mined and successful).
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
tx_hash: Transaction hash to monitor
|
|
164
|
+
network: Network identifier
|
|
165
|
+
timeout_ms: Maximum wait time in milliseconds
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
LegacyTransactionConfirmation with status and details
|
|
169
|
+
"""
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
async def get_balance(
|
|
173
|
+
self,
|
|
174
|
+
owner_address: str,
|
|
175
|
+
token_address: str,
|
|
176
|
+
network: str,
|
|
177
|
+
) -> str:
|
|
178
|
+
"""Get the ERC-20 token balance for an address.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
owner_address: Address to check balance for
|
|
182
|
+
token_address: ERC-20 token contract address
|
|
183
|
+
network: Network identifier
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Balance in token's smallest unit as string
|
|
187
|
+
"""
|
|
188
|
+
...
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class LegacyVerifyResult:
|
|
192
|
+
"""Result of legacy authorization signature verification.
|
|
193
|
+
|
|
194
|
+
Attributes:
|
|
195
|
+
valid: Whether the signature is valid
|
|
196
|
+
recovered_address: Address recovered from the signature (if successful)
|
|
197
|
+
reason: Reason for failure (if invalid)
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
valid: bool,
|
|
203
|
+
recovered_address: Optional[str] = None,
|
|
204
|
+
reason: Optional[str] = None,
|
|
205
|
+
):
|
|
206
|
+
self.valid = valid
|
|
207
|
+
self.recovered_address = recovered_address
|
|
208
|
+
self.reason = reason
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class LegacyTransactionConfirmation:
|
|
212
|
+
"""Result of waiting for transaction confirmation.
|
|
213
|
+
|
|
214
|
+
Attributes:
|
|
215
|
+
success: Whether the transaction was successfully confirmed
|
|
216
|
+
tx_hash: The confirmed transaction hash
|
|
217
|
+
block_number: Block number where the transaction was mined
|
|
218
|
+
error: Error message if confirmation failed
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
success: bool,
|
|
224
|
+
tx_hash: Optional[str] = None,
|
|
225
|
+
block_number: Optional[int] = None,
|
|
226
|
+
error: Optional[str] = None,
|
|
227
|
+
):
|
|
228
|
+
self.success = success
|
|
229
|
+
self.tx_hash = tx_hash
|
|
230
|
+
self.block_number = block_number
|
|
231
|
+
self.error = error
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class ExactLegacyEvmFacilitatorScheme:
|
|
235
|
+
"""Facilitator scheme for EVM exact-legacy payments.
|
|
236
|
+
|
|
237
|
+
Verifies LegacyTransferAuthorization signatures and settles payments
|
|
238
|
+
by calling transferFrom on the token contract.
|
|
239
|
+
|
|
240
|
+
The verification process checks:
|
|
241
|
+
1. Scheme and network validity
|
|
242
|
+
2. Payload structure (signature + authorization fields)
|
|
243
|
+
3. LegacyTransferAuthorization signature recovery
|
|
244
|
+
4. Deadline validity (validBefore with 30-second buffer)
|
|
245
|
+
5. Valid-after constraint
|
|
246
|
+
6. Spender matches facilitator address
|
|
247
|
+
7. Token allowance sufficiency
|
|
248
|
+
8. Token balance sufficiency
|
|
249
|
+
9. Amount >= required amount
|
|
250
|
+
10. Recipient matches payTo
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
```python
|
|
254
|
+
facilitator = ExactLegacyEvmFacilitatorScheme(signer=my_legacy_signer)
|
|
255
|
+
|
|
256
|
+
# Verify a payment
|
|
257
|
+
result = await facilitator.verify(payload, requirements)
|
|
258
|
+
if result.is_valid:
|
|
259
|
+
# Settle the payment on-chain
|
|
260
|
+
settlement = await facilitator.settle(payload, requirements)
|
|
261
|
+
if settlement.success:
|
|
262
|
+
print(f"Settled: {settlement.transaction}")
|
|
263
|
+
```
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
scheme = SCHEME_EXACT_LEGACY
|
|
267
|
+
caip_family = CAIP_FAMILY
|
|
268
|
+
|
|
269
|
+
def __init__(self, signer: FacilitatorLegacyEvmSigner):
|
|
270
|
+
"""Initialize the EVM legacy facilitator scheme.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
signer: EVM legacy facilitator signer for signature verification,
|
|
274
|
+
allowance/balance checking, and transaction execution.
|
|
275
|
+
"""
|
|
276
|
+
self._signer = signer
|
|
277
|
+
|
|
278
|
+
def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
|
|
279
|
+
"""Get mechanism-specific extra data for supported kinds.
|
|
280
|
+
|
|
281
|
+
Returns asset metadata and spender address for the specified network.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
network: The network identifier (e.g., "eip155:1")
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Dict with asset metadata and spender address if supported, else None
|
|
288
|
+
"""
|
|
289
|
+
chain_id_str = self._get_chain_id_str(network)
|
|
290
|
+
if chain_id_str is None:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
tokens = KNOWN_TOKENS.get(chain_id_str)
|
|
294
|
+
if not tokens or len(tokens) == 0:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
token = tokens[0]
|
|
298
|
+
signers = self._signer.get_addresses(network)
|
|
299
|
+
spender = signers[0] if signers else ""
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
"defaultAsset": token["address"],
|
|
303
|
+
"name": token.get("name", "T402LegacyTransfer"),
|
|
304
|
+
"version": token.get("version", "1"),
|
|
305
|
+
"decimals": token["decimals"],
|
|
306
|
+
"spender": spender,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
def get_signers(self, network: Network) -> List[str]:
|
|
310
|
+
"""Get signer addresses for this facilitator on the given network.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
network: The network identifier
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
List of facilitator Ethereum addresses
|
|
317
|
+
"""
|
|
318
|
+
return self._signer.get_addresses(network)
|
|
319
|
+
|
|
320
|
+
async def verify(
|
|
321
|
+
self,
|
|
322
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
323
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
324
|
+
) -> VerifyResponse:
|
|
325
|
+
"""Verify an EVM legacy authorization payment payload.
|
|
326
|
+
|
|
327
|
+
Performs comprehensive validation including signature recovery,
|
|
328
|
+
allowance checks, balance checks, and constraint validation.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
payload: The payment payload containing signature and authorization
|
|
332
|
+
requirements: The payment requirements to verify against
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
VerifyResponse indicating validity and payer address
|
|
336
|
+
"""
|
|
337
|
+
try:
|
|
338
|
+
# Extract data from payload and requirements
|
|
339
|
+
payload_data = self._extract_payload(payload)
|
|
340
|
+
req_data = self._extract_requirements(requirements)
|
|
341
|
+
|
|
342
|
+
network = req_data.get("network", "")
|
|
343
|
+
scheme = req_data.get("scheme", "")
|
|
344
|
+
|
|
345
|
+
# Step 1: Validate scheme
|
|
346
|
+
if scheme != SCHEME_EXACT_LEGACY:
|
|
347
|
+
return VerifyResponse(
|
|
348
|
+
is_valid=False,
|
|
349
|
+
invalid_reason="unsupported_scheme",
|
|
350
|
+
payer=None,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Step 2: Validate network (must be eip155:*)
|
|
354
|
+
if not self._is_valid_network(network):
|
|
355
|
+
return VerifyResponse(
|
|
356
|
+
is_valid=False,
|
|
357
|
+
invalid_reason="unsupported_network",
|
|
358
|
+
payer=None,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Step 3: Parse legacy payload
|
|
362
|
+
legacy_payload = self._parse_legacy_payload(payload_data)
|
|
363
|
+
if legacy_payload is None:
|
|
364
|
+
return VerifyResponse(
|
|
365
|
+
is_valid=False,
|
|
366
|
+
invalid_reason="invalid_payload",
|
|
367
|
+
payer=None,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
authorization = legacy_payload["authorization"]
|
|
371
|
+
signature = legacy_payload["signature"]
|
|
372
|
+
payer = authorization["from"]
|
|
373
|
+
|
|
374
|
+
# Step 4: Get chain ID and token info
|
|
375
|
+
chain_id = self._get_chain_id(network)
|
|
376
|
+
if chain_id is None:
|
|
377
|
+
return VerifyResponse(
|
|
378
|
+
is_valid=False,
|
|
379
|
+
invalid_reason="unsupported_network",
|
|
380
|
+
payer=payer,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
asset = req_data.get("asset", "")
|
|
384
|
+
extra = req_data.get("extra", {})
|
|
385
|
+
token_name = extra.get("name", "T402LegacyTransfer")
|
|
386
|
+
token_version = extra.get("version", "1")
|
|
387
|
+
|
|
388
|
+
# Step 5: Verify spender is a facilitator address
|
|
389
|
+
spender = authorization.get("spender", "")
|
|
390
|
+
facilitator_addresses = self._signer.get_addresses(network)
|
|
391
|
+
if not any(self._addresses_equal(spender, addr) for addr in facilitator_addresses):
|
|
392
|
+
return VerifyResponse(
|
|
393
|
+
is_valid=False,
|
|
394
|
+
invalid_reason="invalid_spender",
|
|
395
|
+
payer=payer,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Step 6: Verify legacy authorization signature
|
|
399
|
+
try:
|
|
400
|
+
verify_result = await self._signer.verify_legacy_authorization(
|
|
401
|
+
from_address=payer,
|
|
402
|
+
to_address=authorization["to"],
|
|
403
|
+
value=authorization["value"],
|
|
404
|
+
valid_after=authorization["validAfter"],
|
|
405
|
+
valid_before=authorization["validBefore"],
|
|
406
|
+
nonce=authorization["nonce"],
|
|
407
|
+
spender=spender,
|
|
408
|
+
signature=signature,
|
|
409
|
+
token_address=asset,
|
|
410
|
+
chain_id=chain_id,
|
|
411
|
+
token_name=token_name,
|
|
412
|
+
token_version=token_version,
|
|
413
|
+
)
|
|
414
|
+
except Exception as e:
|
|
415
|
+
logger.error(f"Signature verification error: {e}")
|
|
416
|
+
return VerifyResponse(
|
|
417
|
+
is_valid=False,
|
|
418
|
+
invalid_reason=f"signature_verification_error: {str(e)}",
|
|
419
|
+
payer=payer,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if not verify_result.valid:
|
|
423
|
+
reason = verify_result.reason or "invalid_signature"
|
|
424
|
+
return VerifyResponse(
|
|
425
|
+
is_valid=False,
|
|
426
|
+
invalid_reason=f"invalid_signature: {reason}",
|
|
427
|
+
payer=payer,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Step 7: Check validBefore deadline (with buffer)
|
|
431
|
+
now = int(time.time())
|
|
432
|
+
try:
|
|
433
|
+
valid_before = int(authorization["validBefore"])
|
|
434
|
+
except (ValueError, TypeError):
|
|
435
|
+
return VerifyResponse(
|
|
436
|
+
is_valid=False,
|
|
437
|
+
invalid_reason="invalid_valid_before",
|
|
438
|
+
payer=payer,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if valid_before < now + MIN_VALIDITY_BUFFER:
|
|
442
|
+
return VerifyResponse(
|
|
443
|
+
is_valid=False,
|
|
444
|
+
invalid_reason="authorization_expired",
|
|
445
|
+
payer=payer,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Step 8: Check validAfter constraint
|
|
449
|
+
try:
|
|
450
|
+
valid_after = int(authorization["validAfter"])
|
|
451
|
+
except (ValueError, TypeError):
|
|
452
|
+
return VerifyResponse(
|
|
453
|
+
is_valid=False,
|
|
454
|
+
invalid_reason="invalid_valid_after",
|
|
455
|
+
payer=payer,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
if valid_after > now:
|
|
459
|
+
return VerifyResponse(
|
|
460
|
+
is_valid=False,
|
|
461
|
+
invalid_reason="authorization_not_yet_valid",
|
|
462
|
+
payer=payer,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Step 9: Check allowance
|
|
466
|
+
required_amount_str = req_data.get("amount", "0")
|
|
467
|
+
try:
|
|
468
|
+
required_amount = int(required_amount_str)
|
|
469
|
+
except (ValueError, TypeError):
|
|
470
|
+
return VerifyResponse(
|
|
471
|
+
is_valid=False,
|
|
472
|
+
invalid_reason="invalid_required_amount",
|
|
473
|
+
payer=payer,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
allowance_str = await self._signer.get_allowance(
|
|
478
|
+
owner_address=payer,
|
|
479
|
+
spender_address=spender,
|
|
480
|
+
token_address=asset,
|
|
481
|
+
network=network,
|
|
482
|
+
)
|
|
483
|
+
allowance = int(allowance_str)
|
|
484
|
+
except (ValueError, TypeError) as e:
|
|
485
|
+
logger.error(f"Allowance check failed: {e}")
|
|
486
|
+
return VerifyResponse(
|
|
487
|
+
is_valid=False,
|
|
488
|
+
invalid_reason="allowance_check_failed",
|
|
489
|
+
payer=payer,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
if allowance < required_amount:
|
|
493
|
+
return VerifyResponse(
|
|
494
|
+
is_valid=False,
|
|
495
|
+
invalid_reason="insufficient_allowance",
|
|
496
|
+
payer=payer,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# Step 10: Verify token balance
|
|
500
|
+
try:
|
|
501
|
+
balance_str = await self._signer.get_balance(
|
|
502
|
+
owner_address=payer,
|
|
503
|
+
token_address=asset,
|
|
504
|
+
network=network,
|
|
505
|
+
)
|
|
506
|
+
balance = int(balance_str)
|
|
507
|
+
except (ValueError, TypeError) as e:
|
|
508
|
+
logger.error(f"Balance check failed: {e}")
|
|
509
|
+
return VerifyResponse(
|
|
510
|
+
is_valid=False,
|
|
511
|
+
invalid_reason="balance_check_failed",
|
|
512
|
+
payer=payer,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
if balance < required_amount:
|
|
516
|
+
return VerifyResponse(
|
|
517
|
+
is_valid=False,
|
|
518
|
+
invalid_reason="insufficient_balance",
|
|
519
|
+
payer=payer,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
# Step 11: Verify amount sufficiency
|
|
523
|
+
try:
|
|
524
|
+
payload_value = int(authorization["value"])
|
|
525
|
+
except (ValueError, TypeError):
|
|
526
|
+
return VerifyResponse(
|
|
527
|
+
is_valid=False,
|
|
528
|
+
invalid_reason="invalid_payload_amount",
|
|
529
|
+
payer=payer,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
if payload_value < required_amount:
|
|
533
|
+
return VerifyResponse(
|
|
534
|
+
is_valid=False,
|
|
535
|
+
invalid_reason="insufficient_amount",
|
|
536
|
+
payer=payer,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Step 12: Verify recipient matches payTo
|
|
540
|
+
pay_to = req_data.get("payTo", "")
|
|
541
|
+
auth_to = authorization.get("to", "")
|
|
542
|
+
if not self._addresses_equal(auth_to, pay_to):
|
|
543
|
+
return VerifyResponse(
|
|
544
|
+
is_valid=False,
|
|
545
|
+
invalid_reason="recipient_mismatch",
|
|
546
|
+
payer=payer,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# All checks passed
|
|
550
|
+
return VerifyResponse(
|
|
551
|
+
is_valid=True,
|
|
552
|
+
invalid_reason=None,
|
|
553
|
+
payer=payer,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
except Exception as e:
|
|
557
|
+
logger.error(f"EVM legacy verification failed: {e}")
|
|
558
|
+
return VerifyResponse(
|
|
559
|
+
is_valid=False,
|
|
560
|
+
invalid_reason=f"verification_error: {str(e)}",
|
|
561
|
+
payer=None,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
async def settle(
|
|
565
|
+
self,
|
|
566
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
567
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
568
|
+
) -> SettleResponse:
|
|
569
|
+
"""Settle an EVM legacy payment on-chain.
|
|
570
|
+
|
|
571
|
+
Verifies the payment first, then calls transferFrom on the token
|
|
572
|
+
contract and waits for transaction confirmation.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
payload: The verified payment payload
|
|
576
|
+
requirements: The payment requirements
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
SettleResponse with transaction hash and status
|
|
580
|
+
"""
|
|
581
|
+
req_data = self._extract_requirements(requirements)
|
|
582
|
+
network = req_data.get("network", "")
|
|
583
|
+
|
|
584
|
+
# Step 1: Verify the payment first
|
|
585
|
+
verify_result = await self.verify(payload, requirements)
|
|
586
|
+
|
|
587
|
+
if not verify_result.is_valid:
|
|
588
|
+
return SettleResponse(
|
|
589
|
+
success=False,
|
|
590
|
+
error_reason=verify_result.invalid_reason,
|
|
591
|
+
transaction=None,
|
|
592
|
+
network=network,
|
|
593
|
+
payer=verify_result.payer,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
# Step 2: Extract payload data for on-chain execution
|
|
597
|
+
try:
|
|
598
|
+
payload_data = self._extract_payload(payload)
|
|
599
|
+
legacy_payload = self._parse_legacy_payload(payload_data)
|
|
600
|
+
|
|
601
|
+
if legacy_payload is None:
|
|
602
|
+
return SettleResponse(
|
|
603
|
+
success=False,
|
|
604
|
+
error_reason="invalid_payload",
|
|
605
|
+
transaction=None,
|
|
606
|
+
network=network,
|
|
607
|
+
payer=verify_result.payer,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
authorization = legacy_payload["authorization"]
|
|
611
|
+
payer = authorization["from"]
|
|
612
|
+
|
|
613
|
+
except Exception as e:
|
|
614
|
+
logger.error(f"Payload extraction failed: {e}")
|
|
615
|
+
return SettleResponse(
|
|
616
|
+
success=False,
|
|
617
|
+
error_reason=f"invalid_payload: {str(e)}",
|
|
618
|
+
transaction=None,
|
|
619
|
+
network=network,
|
|
620
|
+
payer=verify_result.payer,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Step 3: Execute transferFrom on-chain
|
|
624
|
+
asset = req_data.get("asset", "")
|
|
625
|
+
try:
|
|
626
|
+
tx_hash = await self._signer.execute_transfer_from(
|
|
627
|
+
from_address=payer,
|
|
628
|
+
to_address=authorization["to"],
|
|
629
|
+
value=authorization["value"],
|
|
630
|
+
token_address=asset,
|
|
631
|
+
network=network,
|
|
632
|
+
)
|
|
633
|
+
except Exception as e:
|
|
634
|
+
logger.error(f"Transaction execution failed: {e}")
|
|
635
|
+
return SettleResponse(
|
|
636
|
+
success=False,
|
|
637
|
+
error_reason=f"transaction_failed: {str(e)}",
|
|
638
|
+
transaction=None,
|
|
639
|
+
network=network,
|
|
640
|
+
payer=payer,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
# Step 4: Wait for transaction confirmation
|
|
644
|
+
try:
|
|
645
|
+
confirmation = await self._signer.wait_for_confirmation(
|
|
646
|
+
tx_hash=tx_hash,
|
|
647
|
+
network=network,
|
|
648
|
+
timeout_ms=DEFAULT_CONFIRMATION_TIMEOUT,
|
|
649
|
+
)
|
|
650
|
+
except Exception as e:
|
|
651
|
+
logger.error(f"Transaction confirmation failed: {e}")
|
|
652
|
+
return SettleResponse(
|
|
653
|
+
success=False,
|
|
654
|
+
error_reason=f"confirmation_failed: {str(e)}",
|
|
655
|
+
transaction=tx_hash,
|
|
656
|
+
network=network,
|
|
657
|
+
payer=payer,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
if not confirmation.success:
|
|
661
|
+
return SettleResponse(
|
|
662
|
+
success=False,
|
|
663
|
+
error_reason=confirmation.error or "transaction_reverted",
|
|
664
|
+
transaction=tx_hash,
|
|
665
|
+
network=network,
|
|
666
|
+
payer=payer,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
final_tx_hash = confirmation.tx_hash if confirmation.tx_hash else tx_hash
|
|
670
|
+
|
|
671
|
+
return SettleResponse(
|
|
672
|
+
success=True,
|
|
673
|
+
error_reason=None,
|
|
674
|
+
transaction=final_tx_hash,
|
|
675
|
+
network=network,
|
|
676
|
+
payer=payer,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
def _extract_payload(
|
|
680
|
+
self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
|
|
681
|
+
) -> Dict[str, Any]:
|
|
682
|
+
"""Extract payload data as a dict."""
|
|
683
|
+
if hasattr(payload, "model_dump"):
|
|
684
|
+
data = payload.model_dump(by_alias=True)
|
|
685
|
+
return data.get("payload", data)
|
|
686
|
+
elif isinstance(payload, dict):
|
|
687
|
+
return payload.get("payload", payload)
|
|
688
|
+
return dict(payload)
|
|
689
|
+
|
|
690
|
+
def _extract_requirements(
|
|
691
|
+
self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
|
|
692
|
+
) -> Dict[str, Any]:
|
|
693
|
+
"""Extract requirements data as a dict."""
|
|
694
|
+
if hasattr(requirements, "model_dump"):
|
|
695
|
+
return requirements.model_dump(by_alias=True)
|
|
696
|
+
return dict(requirements)
|
|
697
|
+
|
|
698
|
+
def _parse_legacy_payload(
|
|
699
|
+
self, payload_data: Dict[str, Any]
|
|
700
|
+
) -> Optional[Dict[str, Any]]:
|
|
701
|
+
"""Parse and validate legacy payload fields.
|
|
702
|
+
|
|
703
|
+
The LegacyTransferAuthorization contains:
|
|
704
|
+
- from: payer address
|
|
705
|
+
- to: recipient address
|
|
706
|
+
- value: amount in token units
|
|
707
|
+
- validAfter: earliest validity timestamp
|
|
708
|
+
- validBefore: latest validity timestamp
|
|
709
|
+
- nonce: random 32-byte nonce (hex)
|
|
710
|
+
- spender: authorized spender address
|
|
711
|
+
"""
|
|
712
|
+
signature = payload_data.get("signature", "")
|
|
713
|
+
if not signature:
|
|
714
|
+
return None
|
|
715
|
+
|
|
716
|
+
auth_data = payload_data.get("authorization")
|
|
717
|
+
if not auth_data:
|
|
718
|
+
return None
|
|
719
|
+
|
|
720
|
+
from_addr = auth_data.get("from", "")
|
|
721
|
+
to_addr = auth_data.get("to", "")
|
|
722
|
+
value = auth_data.get("value", "0")
|
|
723
|
+
valid_after = auth_data.get("validAfter", auth_data.get("valid_after", "0"))
|
|
724
|
+
valid_before = auth_data.get("validBefore", auth_data.get("valid_before", "0"))
|
|
725
|
+
nonce = auth_data.get("nonce", "")
|
|
726
|
+
spender = auth_data.get("spender", "")
|
|
727
|
+
|
|
728
|
+
if not from_addr or not spender:
|
|
729
|
+
return None
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
"signature": signature,
|
|
733
|
+
"authorization": {
|
|
734
|
+
"from": from_addr,
|
|
735
|
+
"to": to_addr,
|
|
736
|
+
"value": str(value),
|
|
737
|
+
"validAfter": str(valid_after),
|
|
738
|
+
"validBefore": str(valid_before),
|
|
739
|
+
"nonce": str(nonce),
|
|
740
|
+
"spender": spender,
|
|
741
|
+
},
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
def _is_valid_network(self, network: str) -> bool:
|
|
745
|
+
"""Check if the network is a valid EVM network."""
|
|
746
|
+
if not network.startswith("eip155:"):
|
|
747
|
+
return False
|
|
748
|
+
|
|
749
|
+
try:
|
|
750
|
+
chain_id_str = network.split(":")[1]
|
|
751
|
+
chain_id = int(chain_id_str)
|
|
752
|
+
return chain_id > 0
|
|
753
|
+
except (IndexError, ValueError):
|
|
754
|
+
return False
|
|
755
|
+
|
|
756
|
+
def _get_chain_id(self, network: str) -> Optional[int]:
|
|
757
|
+
"""Get the chain ID from a network identifier."""
|
|
758
|
+
if not network.startswith("eip155:"):
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
try:
|
|
762
|
+
return int(network.split(":")[1])
|
|
763
|
+
except (IndexError, ValueError):
|
|
764
|
+
return None
|
|
765
|
+
|
|
766
|
+
def _get_chain_id_str(self, network: str) -> Optional[str]:
|
|
767
|
+
"""Get the chain ID as string for KNOWN_TOKENS lookup."""
|
|
768
|
+
chain_id = self._get_chain_id(network)
|
|
769
|
+
if chain_id is None:
|
|
770
|
+
return None
|
|
771
|
+
return str(chain_id)
|
|
772
|
+
|
|
773
|
+
def _addresses_equal(self, addr1: str, addr2: str) -> bool:
|
|
774
|
+
"""Compare two Ethereum addresses case-insensitively."""
|
|
775
|
+
if not addr1 or not addr2:
|
|
776
|
+
return False
|
|
777
|
+
return addr1.lower() == addr2.lower()
|