t402 1.7.1__py3-none-any.whl → 1.9.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.
Files changed (102) hide show
  1. t402/__init__.py +2 -1
  2. t402/bridge/client.py +13 -5
  3. t402/bridge/constants.py +3 -1
  4. t402/bridge/router.py +1 -1
  5. t402/bridge/scan.py +3 -1
  6. t402/chains.py +268 -1
  7. t402/cli.py +31 -9
  8. t402/common.py +2 -0
  9. t402/cosmos_paywall_template.py +2 -0
  10. t402/encoding.py +9 -3
  11. t402/erc4337/accounts.py +56 -51
  12. t402/erc4337/bundlers.py +105 -99
  13. t402/erc4337/paymasters.py +100 -109
  14. t402/erc4337/types.py +39 -26
  15. t402/evm_paywall_template.py +1 -1
  16. t402/fastapi/middleware.py +1 -3
  17. t402/mcp/server.py +79 -46
  18. t402/near_paywall_template.py +2 -0
  19. t402/networks.py +34 -1
  20. t402/paywall.py +1 -3
  21. t402/schemes/__init__.py +164 -1
  22. t402/schemes/aptos/__init__.py +70 -0
  23. t402/schemes/aptos/constants.py +349 -0
  24. t402/schemes/aptos/exact_direct/__init__.py +44 -0
  25. t402/schemes/aptos/exact_direct/client.py +202 -0
  26. t402/schemes/aptos/exact_direct/facilitator.py +426 -0
  27. t402/schemes/aptos/exact_direct/server.py +272 -0
  28. t402/schemes/aptos/types.py +237 -0
  29. t402/schemes/evm/__init__.py +67 -1
  30. t402/schemes/evm/exact/__init__.py +11 -0
  31. t402/schemes/evm/exact/client.py +3 -1
  32. t402/schemes/evm/exact/facilitator.py +894 -0
  33. t402/schemes/evm/exact/server.py +1 -1
  34. t402/schemes/evm/exact_legacy/__init__.py +38 -0
  35. t402/schemes/evm/exact_legacy/client.py +291 -0
  36. t402/schemes/evm/exact_legacy/facilitator.py +777 -0
  37. t402/schemes/evm/exact_legacy/server.py +231 -0
  38. t402/schemes/evm/upto/__init__.py +70 -0
  39. t402/schemes/evm/upto/client.py +244 -0
  40. t402/schemes/evm/upto/facilitator.py +625 -0
  41. t402/schemes/evm/upto/server.py +243 -0
  42. t402/schemes/evm/upto/types.py +307 -0
  43. t402/schemes/interfaces.py +6 -2
  44. t402/schemes/near/__init__.py +112 -0
  45. t402/schemes/near/constants.py +189 -0
  46. t402/schemes/near/exact_direct/__init__.py +21 -0
  47. t402/schemes/near/exact_direct/client.py +204 -0
  48. t402/schemes/near/exact_direct/facilitator.py +455 -0
  49. t402/schemes/near/exact_direct/server.py +303 -0
  50. t402/schemes/near/types.py +419 -0
  51. t402/schemes/polkadot/__init__.py +72 -0
  52. t402/schemes/polkadot/constants.py +155 -0
  53. t402/schemes/polkadot/exact_direct/__init__.py +43 -0
  54. t402/schemes/polkadot/exact_direct/client.py +235 -0
  55. t402/schemes/polkadot/exact_direct/facilitator.py +428 -0
  56. t402/schemes/polkadot/exact_direct/server.py +292 -0
  57. t402/schemes/polkadot/types.py +385 -0
  58. t402/schemes/registry.py +6 -2
  59. t402/schemes/stacks/__init__.py +68 -0
  60. t402/schemes/stacks/constants.py +122 -0
  61. t402/schemes/stacks/exact_direct/__init__.py +43 -0
  62. t402/schemes/stacks/exact_direct/client.py +222 -0
  63. t402/schemes/stacks/exact_direct/facilitator.py +424 -0
  64. t402/schemes/stacks/exact_direct/server.py +292 -0
  65. t402/schemes/stacks/types.py +380 -0
  66. t402/schemes/svm/__init__.py +29 -0
  67. t402/schemes/svm/exact/__init__.py +35 -0
  68. t402/schemes/svm/exact/client.py +23 -0
  69. t402/schemes/svm/exact/facilitator.py +24 -0
  70. t402/schemes/svm/exact/server.py +20 -0
  71. t402/schemes/tezos/__init__.py +84 -0
  72. t402/schemes/tezos/constants.py +372 -0
  73. t402/schemes/tezos/exact_direct/__init__.py +22 -0
  74. t402/schemes/tezos/exact_direct/client.py +226 -0
  75. t402/schemes/tezos/exact_direct/facilitator.py +491 -0
  76. t402/schemes/tezos/exact_direct/server.py +277 -0
  77. t402/schemes/tezos/types.py +220 -0
  78. t402/schemes/ton/__init__.py +9 -2
  79. t402/schemes/ton/exact/__init__.py +7 -0
  80. t402/schemes/ton/exact/facilitator.py +730 -0
  81. t402/schemes/ton/exact/server.py +1 -1
  82. t402/schemes/tron/__init__.py +11 -2
  83. t402/schemes/tron/exact/__init__.py +9 -0
  84. t402/schemes/tron/exact/facilitator.py +673 -0
  85. t402/schemes/tron/exact/server.py +1 -1
  86. t402/schemes/upto/__init__.py +80 -0
  87. t402/schemes/upto/types.py +376 -0
  88. t402/stacks_paywall_template.py +2 -0
  89. t402/svm.py +45 -11
  90. t402/svm_paywall_template.py +1 -1
  91. t402/ton.py +5 -1
  92. t402/ton_paywall_template.py +1 -192
  93. t402/tron.py +2 -0
  94. t402/tron_paywall_template.py +2 -0
  95. t402/types.py +4 -2
  96. t402/wdk/errors.py +15 -5
  97. t402/wdk/signer.py +11 -2
  98. {t402-1.7.1.dist-info → t402-1.9.1.dist-info}/METADATA +42 -1
  99. t402-1.9.1.dist-info/RECORD +125 -0
  100. t402-1.7.1.dist-info/RECORD +0 -67
  101. {t402-1.7.1.dist-info → t402-1.9.1.dist-info}/WHEEL +0 -0
  102. {t402-1.7.1.dist-info → t402-1.9.1.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()