t402 1.9.1__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.
Files changed (51) hide show
  1. t402/__init__.py +1 -1
  2. t402/a2a/__init__.py +73 -0
  3. t402/a2a/helpers.py +158 -0
  4. t402/a2a/types.py +145 -0
  5. t402/bridge/constants.py +1 -1
  6. t402/django/__init__.py +42 -0
  7. t402/django/middleware.py +596 -0
  8. t402/errors.py +213 -0
  9. t402/facilitator.py +125 -0
  10. t402/mcp/constants.py +3 -6
  11. t402/mcp/server.py +428 -44
  12. t402/mcp/web3_utils.py +493 -0
  13. t402/multisig/__init__.py +120 -0
  14. t402/multisig/constants.py +54 -0
  15. t402/multisig/safe.py +441 -0
  16. t402/multisig/signature.py +228 -0
  17. t402/multisig/transaction.py +238 -0
  18. t402/multisig/types.py +108 -0
  19. t402/multisig/utils.py +77 -0
  20. t402/schemes/__init__.py +19 -0
  21. t402/schemes/cosmos/__init__.py +114 -0
  22. t402/schemes/cosmos/constants.py +211 -0
  23. t402/schemes/cosmos/exact_direct/__init__.py +21 -0
  24. t402/schemes/cosmos/exact_direct/client.py +198 -0
  25. t402/schemes/cosmos/exact_direct/facilitator.py +493 -0
  26. t402/schemes/cosmos/exact_direct/server.py +315 -0
  27. t402/schemes/cosmos/types.py +501 -0
  28. t402/schemes/evm/__init__.py +1 -1
  29. t402/schemes/evm/exact_legacy/server.py +1 -1
  30. t402/schemes/near/__init__.py +25 -0
  31. t402/schemes/near/upto/__init__.py +54 -0
  32. t402/schemes/near/upto/types.py +272 -0
  33. t402/schemes/svm/__init__.py +15 -0
  34. t402/schemes/svm/upto/__init__.py +23 -0
  35. t402/schemes/svm/upto/types.py +193 -0
  36. t402/schemes/ton/__init__.py +15 -0
  37. t402/schemes/ton/upto/__init__.py +31 -0
  38. t402/schemes/ton/upto/types.py +215 -0
  39. t402/schemes/tron/__init__.py +21 -4
  40. t402/schemes/tron/upto/__init__.py +30 -0
  41. t402/schemes/tron/upto/types.py +213 -0
  42. t402/starlette/__init__.py +38 -0
  43. t402/starlette/middleware.py +522 -0
  44. t402/ton.py +1 -1
  45. t402/ton_paywall_template.py +1 -1
  46. t402/types.py +100 -2
  47. t402/wdk/chains.py +1 -1
  48. {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/METADATA +3 -3
  49. {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/RECORD +51 -20
  50. {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/WHEEL +0 -0
  51. {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,493 @@
1
+ """Cosmos/Noble Exact-Direct Scheme - Facilitator Implementation.
2
+
3
+ This module provides the facilitator-side implementation of the exact-direct
4
+ payment scheme for Cosmos/Noble networks.
5
+
6
+ The facilitator:
7
+ 1. Verifies the on-chain transaction by querying the Cosmos REST API.
8
+ 2. Confirms the transaction was a successful MsgSend with correct parameters.
9
+ 3. Marks the transaction as settled (already executed in exact-direct).
10
+
11
+ Replay protection is built-in via an in-memory cache of used transaction hashes.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import threading
18
+ import time
19
+ from typing import Any, Dict, List, Optional, Union
20
+
21
+ from t402.types import (
22
+ PaymentRequirementsV2,
23
+ PaymentPayloadV2,
24
+ VerifyResponse,
25
+ SettleResponse,
26
+ Network,
27
+ )
28
+ from t402.schemes.cosmos.constants import (
29
+ SCHEME_EXACT_DIRECT,
30
+ CAIP_FAMILY,
31
+ USDC_DENOM,
32
+ get_network_config,
33
+ is_valid_address,
34
+ is_valid_network,
35
+ )
36
+ from t402.schemes.cosmos.types import (
37
+ FacilitatorCosmosSigner,
38
+ ExactDirectPayload,
39
+ )
40
+
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ class ExactDirectCosmosFacilitatorConfig:
46
+ """Configuration for the ExactDirectCosmosFacilitatorScheme.
47
+
48
+ Attributes:
49
+ max_transaction_age_seconds: Maximum age (in seconds) of a transaction to accept.
50
+ Default: 300 (5 minutes).
51
+ used_tx_cache_duration_seconds: How long (in seconds) to cache used transaction
52
+ hashes for replay protection. Default: 86400 (24 hours).
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ max_transaction_age_seconds: int = 300,
58
+ used_tx_cache_duration_seconds: int = 86400,
59
+ ) -> None:
60
+ self.max_transaction_age_seconds = max_transaction_age_seconds
61
+ self.used_tx_cache_duration_seconds = used_tx_cache_duration_seconds
62
+
63
+
64
+ class ExactDirectCosmosFacilitatorScheme:
65
+ """Facilitator scheme for Cosmos/Noble exact-direct payments.
66
+
67
+ Verifies that an on-chain bank MsgSend transaction was executed correctly
68
+ and marks it as settled. Since the client already executed the transfer,
69
+ settlement simply confirms the transaction is valid.
70
+
71
+ Features:
72
+ - Replay protection via used transaction hash cache.
73
+ - Validates transaction status, recipient, sender, denom, and amount.
74
+
75
+ Example:
76
+ ```python
77
+ class MyCosmosRPC:
78
+ def get_addresses(self, network: str) -> List[str]:
79
+ return ["noble1facilitator..."]
80
+
81
+ async def query_transaction(
82
+ self, network, tx_hash
83
+ ) -> TransactionResult:
84
+ # Query Cosmos REST API
85
+ ...
86
+
87
+ async def get_balance(
88
+ self, network, address, denom
89
+ ) -> str:
90
+ return "1000000"
91
+
92
+ rpc = MyCosmosRPC()
93
+ facilitator = ExactDirectCosmosFacilitatorScheme(rpc)
94
+
95
+ result = await facilitator.verify(payload, requirements)
96
+ if result.is_valid:
97
+ settlement = await facilitator.settle(payload, requirements)
98
+ ```
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ signer: FacilitatorCosmosSigner,
104
+ config: Optional[ExactDirectCosmosFacilitatorConfig] = None,
105
+ ) -> None:
106
+ """Initialize the facilitator scheme.
107
+
108
+ Args:
109
+ signer: Any object implementing the FacilitatorCosmosSigner protocol.
110
+ config: Optional configuration. If not provided, defaults are used.
111
+ """
112
+ self._signer = signer
113
+ self._config = config or ExactDirectCosmosFacilitatorConfig()
114
+
115
+ # Used transaction cache for replay protection
116
+ self._used_txs: Dict[str, float] = {}
117
+ self._used_txs_lock = threading.Lock()
118
+
119
+ # Start cleanup thread
120
+ self._cleanup_thread = threading.Thread(
121
+ target=self._cleanup_used_txs,
122
+ daemon=True,
123
+ name="cosmos-facilitator-cleanup",
124
+ )
125
+ self._cleanup_thread.start()
126
+
127
+ @property
128
+ def scheme(self) -> str:
129
+ """The scheme identifier."""
130
+ return SCHEME_EXACT_DIRECT
131
+
132
+ @property
133
+ def caip_family(self) -> str:
134
+ """CAIP-2 family pattern for network matching."""
135
+ return CAIP_FAMILY
136
+
137
+ def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
138
+ """Get mechanism-specific extra data for supported kinds.
139
+
140
+ Returns the default token symbol, decimals, and denom for the network.
141
+
142
+ Args:
143
+ network: The network identifier.
144
+
145
+ Returns:
146
+ Dict with assetSymbol, assetDecimals, and assetDenom, or None if network unknown.
147
+ """
148
+ config = get_network_config(network)
149
+ if not config:
150
+ return None
151
+
152
+ return {
153
+ "assetSymbol": config.default_token.symbol,
154
+ "assetDecimals": config.default_token.decimals,
155
+ "assetDenom": config.default_token.denom,
156
+ }
157
+
158
+ def get_signers(self, network: Network) -> List[str]:
159
+ """Get signer addresses for this facilitator.
160
+
161
+ Args:
162
+ network: The network identifier.
163
+
164
+ Returns:
165
+ List of bech32-encoded Cosmos addresses.
166
+ """
167
+ return self._signer.get_addresses(network)
168
+
169
+ async def verify(
170
+ self,
171
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
172
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
173
+ ) -> VerifyResponse:
174
+ """Verify a payment payload by checking the on-chain transaction.
175
+
176
+ Validates:
177
+ 1. Payload has correct structure with txHash and from fields.
178
+ 2. Network is supported and addresses have correct bech32 prefix.
179
+ 3. Transaction has not been used before (replay protection).
180
+ 4. Transaction was successful on-chain (code == 0).
181
+ 5. Transaction contains a MsgSend with correct recipient and sender.
182
+ 6. The transfer amount for the expected denom is >= the required amount.
183
+
184
+ Args:
185
+ payload: The payment payload containing txHash and from.
186
+ requirements: The payment requirements to verify against.
187
+
188
+ Returns:
189
+ VerifyResponse indicating validity and payer address.
190
+ """
191
+ try:
192
+ payload_data = self._extract_payload(payload)
193
+ req_data = self._extract_requirements(requirements)
194
+
195
+ network = req_data.get("network", "")
196
+
197
+ # Validate network
198
+ if not is_valid_network(network):
199
+ return VerifyResponse(
200
+ is_valid=False,
201
+ invalid_reason=f"Unsupported network: {network}",
202
+ payer=None,
203
+ )
204
+
205
+ network_config = get_network_config(network)
206
+ if network_config is None:
207
+ return VerifyResponse(
208
+ is_valid=False,
209
+ invalid_reason=f"Unsupported network: {network}",
210
+ payer=None,
211
+ )
212
+
213
+ # Parse the Cosmos payload
214
+ cosmos_payload = ExactDirectPayload.from_map(payload_data)
215
+
216
+ # Validate required fields
217
+ if not cosmos_payload.tx_hash:
218
+ return VerifyResponse(
219
+ is_valid=False,
220
+ invalid_reason="Missing transaction hash in payload",
221
+ payer=None,
222
+ )
223
+
224
+ if not cosmos_payload.from_address:
225
+ return VerifyResponse(
226
+ is_valid=False,
227
+ invalid_reason="Missing sender (from) in payload",
228
+ payer=None,
229
+ )
230
+
231
+ # Validate address format
232
+ if not is_valid_address(cosmos_payload.from_address, network_config.bech32_prefix):
233
+ return VerifyResponse(
234
+ is_valid=False,
235
+ invalid_reason=f"Invalid sender address format: {cosmos_payload.from_address}",
236
+ payer=cosmos_payload.from_address,
237
+ )
238
+
239
+ # Check for replay attack
240
+ if self._is_tx_used(cosmos_payload.tx_hash):
241
+ return VerifyResponse(
242
+ is_valid=False,
243
+ invalid_reason="Transaction has already been used",
244
+ payer=cosmos_payload.from_address,
245
+ )
246
+
247
+ # Query the transaction from the Cosmos REST API
248
+ try:
249
+ tx_result = await self._signer.query_transaction(
250
+ network=network,
251
+ tx_hash=cosmos_payload.tx_hash,
252
+ )
253
+ except Exception as e:
254
+ return VerifyResponse(
255
+ is_valid=False,
256
+ invalid_reason=f"Transaction not found: {e}",
257
+ payer=cosmos_payload.from_address,
258
+ )
259
+
260
+ # Verify transaction succeeded
261
+ if not tx_result.is_success():
262
+ return VerifyResponse(
263
+ is_valid=False,
264
+ invalid_reason=(
265
+ f"Transaction failed on-chain: code={tx_result.code}, "
266
+ f"log={tx_result.raw_log}"
267
+ ),
268
+ payer=cosmos_payload.from_address,
269
+ )
270
+
271
+ # Find MsgSend in the transaction
272
+ msg_send = tx_result.find_msg_send()
273
+ if msg_send is None:
274
+ return VerifyResponse(
275
+ is_valid=False,
276
+ invalid_reason="No bank send message found in transaction",
277
+ payer=cosmos_payload.from_address,
278
+ )
279
+
280
+ # Determine expected denom
281
+ expected_denom = USDC_DENOM
282
+ required_asset = req_data.get("asset", "")
283
+ if required_asset and required_asset != "USDC":
284
+ # If asset is specified and not USDC symbol, treat it as denom
285
+ expected_denom = required_asset
286
+
287
+ # Verify recipient
288
+ required_pay_to = req_data.get("payTo") or req_data.get("pay_to", "")
289
+ if msg_send.to_address != required_pay_to:
290
+ return VerifyResponse(
291
+ is_valid=False,
292
+ invalid_reason=(
293
+ f"Wrong recipient: expected {required_pay_to}, "
294
+ f"got {msg_send.to_address}"
295
+ ),
296
+ payer=cosmos_payload.from_address,
297
+ )
298
+
299
+ # Verify sender
300
+ if msg_send.from_address != cosmos_payload.from_address:
301
+ return VerifyResponse(
302
+ is_valid=False,
303
+ invalid_reason=(
304
+ f"Sender mismatch: expected {cosmos_payload.from_address}, "
305
+ f"got {msg_send.from_address}"
306
+ ),
307
+ payer=cosmos_payload.from_address,
308
+ )
309
+
310
+ # Verify amount by denom
311
+ tx_amount_str = msg_send.get_amount_by_denom(expected_denom)
312
+ if not tx_amount_str:
313
+ return VerifyResponse(
314
+ is_valid=False,
315
+ invalid_reason=(
316
+ f"Wrong denom: expected {expected_denom} not found in transaction"
317
+ ),
318
+ payer=cosmos_payload.from_address,
319
+ )
320
+
321
+ try:
322
+ tx_amount = int(tx_amount_str)
323
+ except (ValueError, TypeError):
324
+ return VerifyResponse(
325
+ is_valid=False,
326
+ invalid_reason=f"Invalid transaction amount: {tx_amount_str}",
327
+ payer=cosmos_payload.from_address,
328
+ )
329
+
330
+ required_amount_str = req_data.get("amount", "0")
331
+ try:
332
+ required_amount = int(required_amount_str)
333
+ except (ValueError, TypeError):
334
+ return VerifyResponse(
335
+ is_valid=False,
336
+ invalid_reason=f"Invalid required amount: {required_amount_str}",
337
+ payer=cosmos_payload.from_address,
338
+ )
339
+
340
+ if tx_amount < required_amount:
341
+ return VerifyResponse(
342
+ is_valid=False,
343
+ invalid_reason=(
344
+ f"Insufficient amount: expected {required_amount}, "
345
+ f"got {tx_amount}"
346
+ ),
347
+ payer=cosmos_payload.from_address,
348
+ )
349
+
350
+ # Mark transaction as used
351
+ self._mark_tx_used(cosmos_payload.tx_hash)
352
+
353
+ return VerifyResponse(
354
+ is_valid=True,
355
+ invalid_reason=None,
356
+ payer=cosmos_payload.from_address,
357
+ )
358
+
359
+ except Exception as e:
360
+ logger.error(f"Cosmos verification failed: {e}")
361
+ return VerifyResponse(
362
+ is_valid=False,
363
+ invalid_reason=f"Verification error: {str(e)}",
364
+ payer=None,
365
+ )
366
+
367
+ async def settle(
368
+ self,
369
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
370
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
371
+ ) -> SettleResponse:
372
+ """Settle a verified payment.
373
+
374
+ For exact-direct, the transfer was already executed by the client,
375
+ so settlement simply verifies the transaction and returns the tx hash.
376
+
377
+ Args:
378
+ payload: The verified payment payload.
379
+ requirements: The payment requirements.
380
+
381
+ Returns:
382
+ SettleResponse with the transaction hash and status.
383
+ """
384
+ try:
385
+ payload_data = self._extract_payload(payload)
386
+ req_data = self._extract_requirements(requirements)
387
+
388
+ network = req_data.get("network", "")
389
+ cosmos_payload = ExactDirectPayload.from_map(payload_data)
390
+
391
+ # Verify the transaction first
392
+ verify_result = await self.verify(payload, requirements)
393
+
394
+ if not verify_result.is_valid:
395
+ return SettleResponse(
396
+ success=False,
397
+ error_reason=verify_result.invalid_reason,
398
+ transaction=None,
399
+ network=network,
400
+ payer=verify_result.payer,
401
+ )
402
+
403
+ # For exact-direct, settlement is already complete
404
+ return SettleResponse(
405
+ success=True,
406
+ error_reason=None,
407
+ transaction=cosmos_payload.tx_hash,
408
+ network=network,
409
+ payer=verify_result.payer,
410
+ )
411
+
412
+ except Exception as e:
413
+ logger.error(f"Cosmos settlement failed: {e}")
414
+ return SettleResponse(
415
+ success=False,
416
+ error_reason=f"Settlement error: {str(e)}",
417
+ transaction=None,
418
+ network=None,
419
+ payer=None,
420
+ )
421
+
422
+ def _is_tx_used(self, tx_hash: str) -> bool:
423
+ """Check if a transaction has been used (replay protection).
424
+
425
+ Args:
426
+ tx_hash: The transaction hash to check.
427
+
428
+ Returns:
429
+ True if the transaction has already been used.
430
+ """
431
+ with self._used_txs_lock:
432
+ return tx_hash in self._used_txs
433
+
434
+ def _mark_tx_used(self, tx_hash: str) -> None:
435
+ """Mark a transaction as used.
436
+
437
+ Args:
438
+ tx_hash: The transaction hash to mark.
439
+ """
440
+ with self._used_txs_lock:
441
+ self._used_txs[tx_hash] = time.time()
442
+
443
+ def _cleanup_used_txs(self) -> None:
444
+ """Periodically clean up old used transaction entries.
445
+
446
+ Runs in a background daemon thread.
447
+ """
448
+ while True:
449
+ time.sleep(3600) # Clean up every hour
450
+ cutoff = time.time() - self._config.used_tx_cache_duration_seconds
451
+ with self._used_txs_lock:
452
+ expired = [
453
+ tx_hash
454
+ for tx_hash, used_at in self._used_txs.items()
455
+ if used_at < cutoff
456
+ ]
457
+ for tx_hash in expired:
458
+ del self._used_txs[tx_hash]
459
+
460
+ def _extract_payload(
461
+ self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
462
+ ) -> Dict[str, Any]:
463
+ """Extract payload data as a dict.
464
+
465
+ Handles both PaymentPayloadV2 models and plain dicts.
466
+
467
+ Args:
468
+ payload: Payment payload (model or dict).
469
+
470
+ Returns:
471
+ Dict containing the inner payload data.
472
+ """
473
+ if hasattr(payload, "model_dump"):
474
+ data = payload.model_dump(by_alias=True)
475
+ return data.get("payload", data)
476
+ elif isinstance(payload, dict):
477
+ return payload.get("payload", payload)
478
+ return dict(payload)
479
+
480
+ def _extract_requirements(
481
+ self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
482
+ ) -> Dict[str, Any]:
483
+ """Extract requirements data as a dict.
484
+
485
+ Args:
486
+ requirements: Payment requirements (model or dict).
487
+
488
+ Returns:
489
+ Dict containing requirement fields.
490
+ """
491
+ if hasattr(requirements, "model_dump"):
492
+ return requirements.model_dump(by_alias=True)
493
+ return dict(requirements)