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,424 @@
1
+ """Stacks Exact-Direct Scheme - Facilitator Implementation.
2
+
3
+ This module provides the facilitator-side implementation of the exact-direct
4
+ payment scheme for Stacks (Bitcoin L2) networks.
5
+
6
+ The facilitator:
7
+ 1. Verifies payment payloads by querying the transaction on-chain
8
+ 2. Validates that the transaction is a successful SIP-010 token transfer
9
+ matching the payment requirements (sender, recipient, amount, contract)
10
+ 3. For settle(), confirms the transfer has already occurred on-chain
11
+ (since exact-direct payments are pre-paid by the client)
12
+ 4. Maintains a txId cache for replay protection
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from typing import Any, Dict, List, Optional, Set, Union
19
+
20
+ from t402.types import (
21
+ PaymentRequirementsV2,
22
+ PaymentPayloadV2,
23
+ VerifyResponse,
24
+ SettleResponse,
25
+ Network,
26
+ )
27
+ from t402.schemes.stacks.constants import (
28
+ SCHEME_EXACT_DIRECT,
29
+ get_network_config,
30
+ is_stacks_network,
31
+ )
32
+ from t402.schemes.stacks.types import (
33
+ FacilitatorStacksSigner,
34
+ ExactDirectPayload,
35
+ TransactionResult,
36
+ is_valid_tx_id,
37
+ extract_token_transfer,
38
+ parse_contract_identifier,
39
+ )
40
+
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ class ExactDirectStacksFacilitatorScheme:
46
+ """Facilitator scheme for Stacks exact-direct payments.
47
+
48
+ Verifies on-chain SIP-010 token transfers by querying the transaction
49
+ via the Hiro API, and confirms the transfer matches the payment
50
+ requirements. Includes replay protection via txId caching.
51
+
52
+ Example:
53
+ ```python
54
+ facilitator = ExactDirectStacksFacilitatorScheme(
55
+ signer=my_stacks_facilitator_signer,
56
+ )
57
+
58
+ # Verify a payment
59
+ result = await facilitator.verify(payload, requirements)
60
+ if result.is_valid:
61
+ # Payment is confirmed on-chain
62
+ settlement = await facilitator.settle(payload, requirements)
63
+ ```
64
+ """
65
+
66
+ scheme = SCHEME_EXACT_DIRECT
67
+ caip_family = "stacks:*"
68
+
69
+ def __init__(
70
+ self,
71
+ signer: FacilitatorStacksSigner,
72
+ ):
73
+ """Initialize the facilitator.
74
+
75
+ Args:
76
+ signer: Stacks facilitator signer for querying transactions
77
+ """
78
+ self._signer = signer
79
+ self._used_tx_ids: Set[str] = set()
80
+
81
+ def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
82
+ """Get mechanism-specific extra data for supported kinds.
83
+
84
+ Returns asset metadata for the network's default token.
85
+
86
+ Args:
87
+ network: The network identifier
88
+
89
+ Returns:
90
+ Dict with asset metadata, or None if network is unsupported
91
+ """
92
+ try:
93
+ config = get_network_config(network)
94
+ except ValueError:
95
+ return None
96
+
97
+ return {
98
+ "contractAddress": config.default_token.contract_address,
99
+ "assetSymbol": config.default_token.symbol,
100
+ "assetDecimals": config.default_token.decimals,
101
+ "networkName": config.name,
102
+ }
103
+
104
+ def get_signers(self, network: Network) -> List[str]:
105
+ """Get signer addresses for this facilitator on a given network.
106
+
107
+ Args:
108
+ network: The network identifier
109
+
110
+ Returns:
111
+ List of facilitator Stacks addresses for the network
112
+ """
113
+ return self._signer.get_addresses(network)
114
+
115
+ async def verify(
116
+ self,
117
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
118
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
119
+ ) -> VerifyResponse:
120
+ """Verify a Stacks exact-direct payment payload.
121
+
122
+ Queries the transaction on-chain and validates:
123
+ 1. The transaction exists and was successful
124
+ 2. It is a SIP-010 token transfer call
125
+ 3. The sender, recipient, amount, and contract match the requirements
126
+ 4. The transaction has not been previously used (replay protection)
127
+
128
+ Args:
129
+ payload: Payment payload containing transaction proof
130
+ requirements: Payment requirements to verify against
131
+
132
+ Returns:
133
+ VerifyResponse indicating validity and payer address
134
+ """
135
+ try:
136
+ # Extract data
137
+ payload_data = self._extract_payload(payload)
138
+ req_data = self._extract_requirements(requirements)
139
+
140
+ # Parse the payload
141
+ exact_payload = ExactDirectPayload.from_dict(payload_data)
142
+
143
+ # Extract requirements
144
+ network = req_data.get("network", "")
145
+ required_amount = req_data.get("amount", "0")
146
+ pay_to = req_data.get("payTo", req_data.get("pay_to", ""))
147
+ asset = req_data.get("asset", "")
148
+
149
+ # Validate network
150
+ if not is_stacks_network(network):
151
+ return VerifyResponse(
152
+ is_valid=False,
153
+ invalid_reason=f"Unsupported network: {network}",
154
+ payer=exact_payload.from_address or None,
155
+ )
156
+
157
+ # Validate transaction ID
158
+ if not exact_payload.tx_id:
159
+ return VerifyResponse(
160
+ is_valid=False,
161
+ invalid_reason="Missing transaction ID in payload",
162
+ payer=exact_payload.from_address or None,
163
+ )
164
+
165
+ if not is_valid_tx_id(exact_payload.tx_id):
166
+ return VerifyResponse(
167
+ is_valid=False,
168
+ invalid_reason=f"Invalid transaction ID format: {exact_payload.tx_id}",
169
+ payer=exact_payload.from_address or None,
170
+ )
171
+
172
+ # Replay protection: check if txId was already used
173
+ if exact_payload.tx_id in self._used_tx_ids:
174
+ return VerifyResponse(
175
+ is_valid=False,
176
+ invalid_reason=f"Transaction ID already used: {exact_payload.tx_id}",
177
+ payer=exact_payload.from_address or None,
178
+ )
179
+
180
+ # Query the transaction on-chain
181
+ tx_data = await self._signer.query_transaction(exact_payload.tx_id)
182
+
183
+ if not tx_data:
184
+ return VerifyResponse(
185
+ is_valid=False,
186
+ invalid_reason="Transaction not found on-chain",
187
+ payer=exact_payload.from_address or None,
188
+ )
189
+
190
+ # Parse the transaction result
191
+ tx_result = self._parse_transaction_data(tx_data)
192
+
193
+ # Check success
194
+ if tx_result.tx_status != "success":
195
+ return VerifyResponse(
196
+ is_valid=False,
197
+ invalid_reason=f"Transaction not successful, status: {tx_result.tx_status}",
198
+ payer=tx_result.sender_address or None,
199
+ )
200
+
201
+ # Extract transfer details
202
+ transfer = extract_token_transfer(tx_result)
203
+ if transfer is None:
204
+ return VerifyResponse(
205
+ is_valid=False,
206
+ invalid_reason="Transaction is not a valid SIP-010 token transfer",
207
+ payer=tx_result.sender_address or None,
208
+ )
209
+
210
+ # Validate recipient matches payTo
211
+ if pay_to and transfer.to_address != pay_to:
212
+ return VerifyResponse(
213
+ is_valid=False,
214
+ invalid_reason=(
215
+ f"Transfer recipient {transfer.to_address} does not match "
216
+ f"required payTo {pay_to}"
217
+ ),
218
+ payer=transfer.from_address or None,
219
+ )
220
+
221
+ # Validate amount >= required
222
+ try:
223
+ transfer_amount = int(transfer.amount)
224
+ req_amount = int(required_amount)
225
+ except (ValueError, TypeError):
226
+ return VerifyResponse(
227
+ is_valid=False,
228
+ invalid_reason="Invalid amount format in transfer or requirements",
229
+ payer=transfer.from_address or None,
230
+ )
231
+
232
+ if transfer_amount < req_amount:
233
+ return VerifyResponse(
234
+ is_valid=False,
235
+ invalid_reason=(
236
+ f"Transfer amount {transfer_amount} is less than "
237
+ f"required amount {req_amount}"
238
+ ),
239
+ payer=transfer.from_address or None,
240
+ )
241
+
242
+ # Validate contract address if specified in requirements
243
+ expected_contract = self._resolve_expected_contract(asset, req_data)
244
+ if expected_contract and transfer.contract_address != expected_contract:
245
+ return VerifyResponse(
246
+ is_valid=False,
247
+ invalid_reason=(
248
+ f"Transfer contract {transfer.contract_address} does not match "
249
+ f"expected contract {expected_contract}"
250
+ ),
251
+ payer=transfer.from_address or None,
252
+ )
253
+
254
+ # All checks passed - mark txId as used for replay protection
255
+ self._used_tx_ids.add(exact_payload.tx_id)
256
+
257
+ return VerifyResponse(
258
+ is_valid=True,
259
+ invalid_reason=None,
260
+ payer=transfer.from_address,
261
+ )
262
+
263
+ except Exception as e:
264
+ logger.error(f"Stacks verification failed: {e}")
265
+ return VerifyResponse(
266
+ is_valid=False,
267
+ invalid_reason=f"Verification error: {str(e)}",
268
+ payer=None,
269
+ )
270
+
271
+ async def settle(
272
+ self,
273
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
274
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
275
+ ) -> SettleResponse:
276
+ """Settle a Stacks exact-direct payment.
277
+
278
+ For exact-direct payments, the transfer has already been executed
279
+ on-chain by the client. Settlement simply confirms the transfer
280
+ and returns the transaction ID as the settlement proof.
281
+
282
+ This method first verifies the payment, then returns the
283
+ transaction ID as the settlement proof.
284
+
285
+ Args:
286
+ payload: The verified payment payload
287
+ requirements: The payment requirements
288
+
289
+ Returns:
290
+ SettleResponse with the transaction ID and status
291
+ """
292
+ try:
293
+ # Extract data
294
+ payload_data = self._extract_payload(payload)
295
+ req_data = self._extract_requirements(requirements)
296
+
297
+ network = req_data.get("network", "")
298
+
299
+ # First verify the payment
300
+ verify_result = await self.verify(payload, requirements)
301
+
302
+ if not verify_result.is_valid:
303
+ return SettleResponse(
304
+ success=False,
305
+ error_reason=verify_result.invalid_reason,
306
+ transaction=None,
307
+ network=network,
308
+ payer=verify_result.payer,
309
+ )
310
+
311
+ # Payment already settled on-chain, return the transaction ID
312
+ exact_payload = ExactDirectPayload.from_dict(payload_data)
313
+
314
+ return SettleResponse(
315
+ success=True,
316
+ error_reason=None,
317
+ transaction=exact_payload.tx_id,
318
+ network=network,
319
+ payer=verify_result.payer,
320
+ )
321
+
322
+ except Exception as e:
323
+ logger.error(f"Stacks settlement failed: {e}")
324
+ return SettleResponse(
325
+ success=False,
326
+ error_reason=f"Settlement error: {str(e)}",
327
+ transaction=None,
328
+ network=req_data.get("network") if "req_data" in dir() else None,
329
+ payer=None,
330
+ )
331
+
332
+ def _extract_payload(
333
+ self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
334
+ ) -> Dict[str, Any]:
335
+ """Extract payload data as a dict.
336
+
337
+ Handles both PaymentPayloadV2 models and plain dicts.
338
+
339
+ Args:
340
+ payload: Payment payload (model or dict)
341
+
342
+ Returns:
343
+ Dict containing the inner payload data
344
+ """
345
+ if hasattr(payload, "model_dump"):
346
+ data = payload.model_dump(by_alias=True)
347
+ return data.get("payload", data)
348
+ elif isinstance(payload, dict):
349
+ return payload.get("payload", payload)
350
+ return dict(payload)
351
+
352
+ def _extract_requirements(
353
+ self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
354
+ ) -> Dict[str, Any]:
355
+ """Extract requirements data as a dict.
356
+
357
+ Args:
358
+ requirements: Payment requirements (model or dict)
359
+
360
+ Returns:
361
+ Dict containing requirement fields
362
+ """
363
+ if hasattr(requirements, "model_dump"):
364
+ return requirements.model_dump(by_alias=True)
365
+ return dict(requirements)
366
+
367
+ def _parse_transaction_data(self, data: Dict[str, Any]) -> TransactionResult:
368
+ """Parse raw transaction query data into a TransactionResult.
369
+
370
+ Args:
371
+ data: Raw dictionary from the Hiro API query
372
+
373
+ Returns:
374
+ TransactionResult instance
375
+ """
376
+ return TransactionResult(
377
+ tx_id=data.get("tx_id", ""),
378
+ tx_status=data.get("tx_status", ""),
379
+ sender_address=data.get("sender_address", ""),
380
+ contract_call=data.get("contract_call"),
381
+ block_height=int(data.get("block_height", 0)),
382
+ block_hash=data.get("block_hash", ""),
383
+ )
384
+
385
+ def _resolve_expected_contract(
386
+ self, asset: str, req_data: Dict[str, Any]
387
+ ) -> Optional[str]:
388
+ """Resolve the expected contract address from requirements.
389
+
390
+ Tries to determine the contract address from:
391
+ 1. The CAIP-19 asset identifier
392
+ 2. The extra.contractAddress field
393
+ 3. The network's default token
394
+
395
+ Args:
396
+ asset: CAIP-19 asset identifier string
397
+ req_data: Requirements dictionary
398
+
399
+ Returns:
400
+ Expected contract address, or None if it cannot be determined
401
+ """
402
+ # Try CAIP-19 identifier
403
+ if asset:
404
+ parsed = parse_contract_identifier(asset)
405
+ if parsed is not None:
406
+ return parsed
407
+
408
+ # Try extra.contractAddress
409
+ extra = req_data.get("extra", {})
410
+ if extra and "contractAddress" in extra:
411
+ contract_val = extra["contractAddress"]
412
+ if isinstance(contract_val, str) and contract_val:
413
+ return contract_val
414
+
415
+ # Try network default
416
+ network = req_data.get("network", "")
417
+ if network:
418
+ try:
419
+ config = get_network_config(network)
420
+ return config.default_token.contract_address
421
+ except ValueError:
422
+ pass
423
+
424
+ return None