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,491 @@
1
+ """Tezos Exact-Direct Scheme - Facilitator Implementation.
2
+
3
+ This module provides the facilitator-side implementation of the exact-direct
4
+ payment scheme for Tezos.
5
+
6
+ The facilitator:
7
+ 1. Receives a payment payload containing an operation hash
8
+ 2. Queries the Tezos blockchain (via TzKT indexer) for operation details
9
+ 3. Verifies: status="applied", correct sender/recipient/amount/contract
10
+ 4. For settle: the operation is already executed, so settle confirms verification
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import Any, Dict, List, Optional, Union
17
+
18
+ from t402.types import (
19
+ PaymentRequirementsV2,
20
+ PaymentPayloadV2,
21
+ VerifyResponse,
22
+ SettleResponse,
23
+ Network,
24
+ )
25
+ from t402.schemes.tezos.constants import (
26
+ SCHEME_EXACT_DIRECT,
27
+ FA2_TRANSFER_ENTRYPOINT,
28
+ is_valid_address,
29
+ is_valid_operation_hash,
30
+ parse_asset_identifier,
31
+ )
32
+ from t402.schemes.tezos.types import FacilitatorTezosSigner
33
+
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class ExactDirectTezosFacilitator:
39
+ """Facilitator scheme for Tezos exact-direct payments.
40
+
41
+ Verifies on-chain FA2 transfer operations by querying the Tezos blockchain
42
+ and checking that the operation matches the payment requirements.
43
+
44
+ In the exact-direct scheme, the client has already executed the transfer,
45
+ so the facilitator's role is purely verification. Settlement confirms that
46
+ the operation was successfully applied on-chain.
47
+
48
+ Example:
49
+ ```python
50
+ from t402.schemes.tezos import ExactDirectTezosFacilitator
51
+
52
+ class MyTezosQuerier:
53
+ async def get_operation(self, op_hash, network):
54
+ # Query TzKT indexer
55
+ return {...}
56
+
57
+ facilitator = ExactDirectTezosFacilitator(
58
+ signer=MyTezosQuerier(),
59
+ addresses={"tezos:NetXdQprcVkpaWU": "tz1..."},
60
+ )
61
+
62
+ result = await facilitator.verify(payload, requirements)
63
+ if result.is_valid:
64
+ settlement = await facilitator.settle(payload, requirements)
65
+ ```
66
+ """
67
+
68
+ scheme = SCHEME_EXACT_DIRECT
69
+ caip_family = "tezos:*"
70
+
71
+ def __init__(
72
+ self,
73
+ signer: FacilitatorTezosSigner,
74
+ addresses: Optional[Dict[str, str]] = None,
75
+ ):
76
+ """Initialize the Tezos exact-direct facilitator.
77
+
78
+ Args:
79
+ signer: A Tezos operation querier implementing FacilitatorTezosSigner.
80
+ Must provide get_operation() method.
81
+ addresses: Mapping of network -> facilitator Tezos address.
82
+ Used for get_signers() responses. Keys are CAIP-2 network IDs.
83
+ """
84
+ self._signer = signer
85
+ self._addresses = addresses or {}
86
+
87
+ def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
88
+ """Get mechanism-specific extra data for supported kinds.
89
+
90
+ Args:
91
+ network: The network identifier
92
+
93
+ Returns:
94
+ None (no extra data needed for exact-direct scheme)
95
+ """
96
+ return None
97
+
98
+ def get_signers(self, network: Network) -> List[str]:
99
+ """Get signer addresses for this facilitator on a given network.
100
+
101
+ Args:
102
+ network: The network identifier (CAIP-2 format)
103
+
104
+ Returns:
105
+ List of facilitator addresses for the given network
106
+ """
107
+ address = self._addresses.get(network)
108
+ if address:
109
+ return [address]
110
+ return []
111
+
112
+ async def verify(
113
+ self,
114
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
115
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
116
+ ) -> VerifyResponse:
117
+ """Verify a Tezos exact-direct payment payload.
118
+
119
+ Queries the Tezos blockchain for the operation hash and verifies:
120
+ 1. Operation exists and has status "applied"
121
+ 2. Target contract matches the expected FA2 contract
122
+ 3. Entrypoint is "transfer"
123
+ 4. Sender matches the payload's "from" field
124
+ 5. Recipient matches the requirements' "payTo" address
125
+ 6. Amount is >= the required amount
126
+ 7. Token ID matches
127
+
128
+ Args:
129
+ payload: The payment payload containing the operation hash
130
+ requirements: The payment requirements to verify against
131
+
132
+ Returns:
133
+ VerifyResponse indicating validity and payer address
134
+ """
135
+ try:
136
+ # Extract payload and requirements data
137
+ payload_data = self._extract_payload(payload)
138
+ req_data = self._extract_requirements(requirements)
139
+
140
+ # Get the inner payload fields
141
+ op_hash = payload_data.get("opHash", "")
142
+ from_address = payload_data.get("from", "")
143
+ _to_address = payload_data.get("to", "") # noqa: F841
144
+ _amount_str = payload_data.get("amount", "0") # noqa: F841
145
+ contract_address = payload_data.get("contractAddress", "")
146
+ token_id = payload_data.get("tokenId", 0)
147
+
148
+ # Validate operation hash format
149
+ if not is_valid_operation_hash(op_hash):
150
+ return VerifyResponse(
151
+ is_valid=False,
152
+ invalid_reason=f"Invalid operation hash format: {op_hash}",
153
+ payer=from_address or None,
154
+ )
155
+
156
+ # Validate from address
157
+ if not is_valid_address(from_address):
158
+ return VerifyResponse(
159
+ is_valid=False,
160
+ invalid_reason=f"Invalid sender address: {from_address}",
161
+ payer=None,
162
+ )
163
+
164
+ # Get required fields from requirements
165
+ req_network = req_data.get("network", "")
166
+ req_pay_to = req_data.get("payTo") or req_data.get("pay_to", "")
167
+ req_amount = req_data.get("amount", "0")
168
+ req_asset = req_data.get("asset", "")
169
+
170
+ # Parse expected asset info from requirements
171
+ if req_asset:
172
+ try:
173
+ expected_asset = parse_asset_identifier(req_asset)
174
+ expected_contract = expected_asset["contract_address"]
175
+ expected_token_id = expected_asset["token_id"]
176
+ except ValueError as e:
177
+ return VerifyResponse(
178
+ is_valid=False,
179
+ invalid_reason=f"Invalid asset in requirements: {e}",
180
+ payer=from_address,
181
+ )
182
+ else:
183
+ expected_contract = contract_address
184
+ expected_token_id = token_id
185
+
186
+ # Query the operation on-chain
187
+ try:
188
+ operation = await self._signer.get_operation(op_hash, req_network)
189
+ except Exception as e:
190
+ logger.error("Failed to query operation %s: %s", op_hash, e)
191
+ return VerifyResponse(
192
+ is_valid=False,
193
+ invalid_reason=f"Failed to query operation: {str(e)}",
194
+ payer=from_address,
195
+ )
196
+
197
+ if not operation:
198
+ return VerifyResponse(
199
+ is_valid=False,
200
+ invalid_reason=f"Operation not found: {op_hash}",
201
+ payer=from_address,
202
+ )
203
+
204
+ # Check operation status
205
+ status = operation.get("status", "")
206
+ if status != "applied":
207
+ return VerifyResponse(
208
+ is_valid=False,
209
+ invalid_reason=(
210
+ f"Operation status is '{status}', expected 'applied'"
211
+ ),
212
+ payer=from_address,
213
+ )
214
+
215
+ # Check entrypoint
216
+ entrypoint = operation.get("entrypoint", "")
217
+ if entrypoint != FA2_TRANSFER_ENTRYPOINT:
218
+ return VerifyResponse(
219
+ is_valid=False,
220
+ invalid_reason=(
221
+ f"Operation entrypoint is '{entrypoint}', "
222
+ f"expected '{FA2_TRANSFER_ENTRYPOINT}'"
223
+ ),
224
+ payer=from_address,
225
+ )
226
+
227
+ # Check target contract
228
+ target = operation.get("target", {})
229
+ target_address = target.get("address", "") if isinstance(target, dict) else ""
230
+ if target_address != expected_contract:
231
+ return VerifyResponse(
232
+ is_valid=False,
233
+ invalid_reason=(
234
+ f"Operation target contract '{target_address}' does not match "
235
+ f"expected '{expected_contract}'"
236
+ ),
237
+ payer=from_address,
238
+ )
239
+
240
+ # Check sender
241
+ sender = operation.get("sender", {})
242
+ sender_address = sender.get("address", "") if isinstance(sender, dict) else ""
243
+ if sender_address != from_address:
244
+ return VerifyResponse(
245
+ is_valid=False,
246
+ invalid_reason=(
247
+ f"Operation sender '{sender_address}' does not match "
248
+ f"payload sender '{from_address}'"
249
+ ),
250
+ payer=from_address,
251
+ )
252
+
253
+ # Extract and verify transfer parameters
254
+ transfer_details = self._extract_transfer_details(operation)
255
+ if transfer_details is None:
256
+ return VerifyResponse(
257
+ is_valid=False,
258
+ invalid_reason="Failed to parse FA2 transfer parameters",
259
+ payer=from_address,
260
+ )
261
+
262
+ # Verify recipient
263
+ if req_pay_to and transfer_details["to"] != req_pay_to:
264
+ return VerifyResponse(
265
+ is_valid=False,
266
+ invalid_reason=(
267
+ f"Transfer recipient '{transfer_details['to']}' does not match "
268
+ f"required payTo '{req_pay_to}'"
269
+ ),
270
+ payer=from_address,
271
+ )
272
+
273
+ # Verify amount (must be >= required)
274
+ try:
275
+ transfer_amount = int(transfer_details["amount"])
276
+ required_amount = int(req_amount)
277
+ if transfer_amount < required_amount:
278
+ return VerifyResponse(
279
+ is_valid=False,
280
+ invalid_reason=(
281
+ f"Transfer amount {transfer_amount} is less than "
282
+ f"required amount {required_amount}"
283
+ ),
284
+ payer=from_address,
285
+ )
286
+ except (ValueError, TypeError) as e:
287
+ return VerifyResponse(
288
+ is_valid=False,
289
+ invalid_reason=f"Invalid amount in transfer: {e}",
290
+ payer=from_address,
291
+ )
292
+
293
+ # Verify token ID
294
+ if transfer_details.get("token_id") != expected_token_id:
295
+ return VerifyResponse(
296
+ is_valid=False,
297
+ invalid_reason=(
298
+ f"Token ID {transfer_details.get('token_id')} does not match "
299
+ f"expected {expected_token_id}"
300
+ ),
301
+ payer=from_address,
302
+ )
303
+
304
+ # All checks passed
305
+ return VerifyResponse(
306
+ is_valid=True,
307
+ invalid_reason=None,
308
+ payer=from_address,
309
+ )
310
+
311
+ except Exception as e:
312
+ logger.error("Tezos verification failed: %s", e)
313
+ return VerifyResponse(
314
+ is_valid=False,
315
+ invalid_reason=f"Verification error: {str(e)}",
316
+ payer=None,
317
+ )
318
+
319
+ async def settle(
320
+ self,
321
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
322
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
323
+ ) -> SettleResponse:
324
+ """Settle a verified Tezos exact-direct payment.
325
+
326
+ In the exact-direct scheme, the transfer has already been executed by
327
+ the client. Settlement simply confirms the verification was successful
328
+ and returns the operation hash as the transaction reference.
329
+
330
+ Args:
331
+ payload: The verified payment payload with operation hash
332
+ requirements: The payment requirements
333
+
334
+ Returns:
335
+ SettleResponse with operation hash and status
336
+ """
337
+ try:
338
+ # First verify the payment
339
+ verify_result = await self.verify(payload, requirements)
340
+
341
+ if not verify_result.is_valid:
342
+ return SettleResponse(
343
+ success=False,
344
+ error_reason=verify_result.invalid_reason,
345
+ transaction=None,
346
+ network=self._get_network(requirements),
347
+ payer=verify_result.payer,
348
+ )
349
+
350
+ # Extract operation hash from payload
351
+ payload_data = self._extract_payload(payload)
352
+ op_hash = payload_data.get("opHash", "")
353
+ network = self._get_network(requirements)
354
+
355
+ return SettleResponse(
356
+ success=True,
357
+ error_reason=None,
358
+ transaction=op_hash,
359
+ network=network,
360
+ payer=verify_result.payer,
361
+ )
362
+
363
+ except Exception as e:
364
+ logger.error("Tezos settlement failed: %s", e)
365
+ return SettleResponse(
366
+ success=False,
367
+ error_reason=f"Settlement error: {str(e)}",
368
+ transaction=None,
369
+ network=self._get_network(requirements),
370
+ payer=None,
371
+ )
372
+
373
+ def _extract_payload(
374
+ self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
375
+ ) -> Dict[str, Any]:
376
+ """Extract payload data as a dict.
377
+
378
+ Handles both PaymentPayloadV2 models (where the inner payload is
379
+ in the 'payload' field) and plain dicts.
380
+
381
+ Args:
382
+ payload: Payment payload (model or dict)
383
+
384
+ Returns:
385
+ Dict containing the inner payload fields
386
+ """
387
+ if hasattr(payload, "model_dump"):
388
+ data = payload.model_dump(by_alias=True)
389
+ return data.get("payload", data)
390
+ elif isinstance(payload, dict):
391
+ return payload.get("payload", payload)
392
+ return dict(payload)
393
+
394
+ def _extract_requirements(
395
+ self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
396
+ ) -> Dict[str, Any]:
397
+ """Extract requirements data as a dict.
398
+
399
+ Args:
400
+ requirements: Payment requirements (model or dict)
401
+
402
+ Returns:
403
+ Dict containing requirement fields
404
+ """
405
+ if hasattr(requirements, "model_dump"):
406
+ return requirements.model_dump(by_alias=True)
407
+ return dict(requirements)
408
+
409
+ def _get_network(
410
+ self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
411
+ ) -> Optional[str]:
412
+ """Extract network from requirements.
413
+
414
+ Args:
415
+ requirements: Payment requirements
416
+
417
+ Returns:
418
+ Network string or None
419
+ """
420
+ if hasattr(requirements, "model_dump"):
421
+ data = requirements.model_dump(by_alias=True)
422
+ return data.get("network")
423
+ elif isinstance(requirements, dict):
424
+ return requirements.get("network")
425
+ return None
426
+
427
+ def _extract_transfer_details(
428
+ self, operation: Dict[str, Any]
429
+ ) -> Optional[Dict[str, Any]]:
430
+ """Extract FA2 transfer details from an operation.
431
+
432
+ Parses the FA2 transfer parameter to extract sender, recipient,
433
+ amount, and token ID from the first transfer in the batch.
434
+
435
+ The parameter structure follows the FA2 standard:
436
+ [{"from_": "tz1...", "txs": [{"to_": "tz1...", "token_id": 0, "amount": "1000000"}]}]
437
+
438
+ Args:
439
+ operation: Operation dict from the indexer
440
+
441
+ Returns:
442
+ Dict with "from", "to", "amount", "token_id" if parsing succeeds,
443
+ None if the parameter cannot be parsed
444
+ """
445
+ parameter = operation.get("parameter")
446
+ if parameter is None:
447
+ return None
448
+
449
+ try:
450
+ # Parameter can be a list of transfer batches or a single batch
451
+ if isinstance(parameter, list):
452
+ params = parameter
453
+ elif isinstance(parameter, dict):
454
+ # Some indexers wrap in a value field
455
+ value = parameter.get("value", parameter)
456
+ if isinstance(value, list):
457
+ params = value
458
+ else:
459
+ params = [value]
460
+ else:
461
+ return None
462
+
463
+ if not params:
464
+ return None
465
+
466
+ first_param = params[0]
467
+ from_address = first_param.get("from_") or first_param.get("from", "")
468
+
469
+ txs = first_param.get("txs", [])
470
+ if not txs:
471
+ return None
472
+
473
+ first_tx = txs[0]
474
+ to_address = first_tx.get("to_") or first_tx.get("to", "")
475
+ amount = str(first_tx.get("amount", "0"))
476
+ token_id = first_tx.get("token_id", 0)
477
+
478
+ # Handle token_id as string or int
479
+ if isinstance(token_id, str):
480
+ token_id = int(token_id)
481
+
482
+ return {
483
+ "from": from_address,
484
+ "to": to_address,
485
+ "amount": amount,
486
+ "token_id": token_id,
487
+ }
488
+
489
+ except (KeyError, IndexError, TypeError, ValueError) as e:
490
+ logger.debug("Failed to parse FA2 transfer parameters: %s", e)
491
+ return None