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.
Files changed (134) hide show
  1. t402/__init__.py +2 -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/client.py +13 -5
  6. t402/bridge/constants.py +4 -2
  7. t402/bridge/router.py +1 -1
  8. t402/bridge/scan.py +3 -1
  9. t402/chains.py +268 -1
  10. t402/cli.py +31 -9
  11. t402/common.py +2 -0
  12. t402/cosmos_paywall_template.py +2 -0
  13. t402/django/__init__.py +42 -0
  14. t402/django/middleware.py +596 -0
  15. t402/encoding.py +9 -3
  16. t402/erc4337/accounts.py +56 -51
  17. t402/erc4337/bundlers.py +105 -99
  18. t402/erc4337/paymasters.py +100 -109
  19. t402/erc4337/types.py +39 -26
  20. t402/errors.py +213 -0
  21. t402/evm_paywall_template.py +1 -1
  22. t402/facilitator.py +125 -0
  23. t402/fastapi/middleware.py +1 -3
  24. t402/mcp/constants.py +3 -6
  25. t402/mcp/server.py +501 -84
  26. t402/mcp/web3_utils.py +493 -0
  27. t402/multisig/__init__.py +120 -0
  28. t402/multisig/constants.py +54 -0
  29. t402/multisig/safe.py +441 -0
  30. t402/multisig/signature.py +228 -0
  31. t402/multisig/transaction.py +238 -0
  32. t402/multisig/types.py +108 -0
  33. t402/multisig/utils.py +77 -0
  34. t402/near_paywall_template.py +2 -0
  35. t402/networks.py +34 -1
  36. t402/paywall.py +1 -3
  37. t402/schemes/__init__.py +143 -0
  38. t402/schemes/aptos/__init__.py +70 -0
  39. t402/schemes/aptos/constants.py +349 -0
  40. t402/schemes/aptos/exact_direct/__init__.py +44 -0
  41. t402/schemes/aptos/exact_direct/client.py +202 -0
  42. t402/schemes/aptos/exact_direct/facilitator.py +426 -0
  43. t402/schemes/aptos/exact_direct/server.py +272 -0
  44. t402/schemes/aptos/types.py +237 -0
  45. t402/schemes/cosmos/__init__.py +114 -0
  46. t402/schemes/cosmos/constants.py +211 -0
  47. t402/schemes/cosmos/exact_direct/__init__.py +21 -0
  48. t402/schemes/cosmos/exact_direct/client.py +198 -0
  49. t402/schemes/cosmos/exact_direct/facilitator.py +493 -0
  50. t402/schemes/cosmos/exact_direct/server.py +315 -0
  51. t402/schemes/cosmos/types.py +501 -0
  52. t402/schemes/evm/__init__.py +46 -1
  53. t402/schemes/evm/exact/__init__.py +11 -0
  54. t402/schemes/evm/exact/client.py +3 -1
  55. t402/schemes/evm/exact/facilitator.py +894 -0
  56. t402/schemes/evm/exact/server.py +1 -1
  57. t402/schemes/evm/exact_legacy/__init__.py +38 -0
  58. t402/schemes/evm/exact_legacy/client.py +291 -0
  59. t402/schemes/evm/exact_legacy/facilitator.py +777 -0
  60. t402/schemes/evm/exact_legacy/server.py +231 -0
  61. t402/schemes/evm/upto/__init__.py +12 -0
  62. t402/schemes/evm/upto/client.py +6 -2
  63. t402/schemes/evm/upto/facilitator.py +625 -0
  64. t402/schemes/evm/upto/server.py +243 -0
  65. t402/schemes/evm/upto/types.py +3 -1
  66. t402/schemes/interfaces.py +6 -2
  67. t402/schemes/near/__init__.py +137 -0
  68. t402/schemes/near/constants.py +189 -0
  69. t402/schemes/near/exact_direct/__init__.py +21 -0
  70. t402/schemes/near/exact_direct/client.py +204 -0
  71. t402/schemes/near/exact_direct/facilitator.py +455 -0
  72. t402/schemes/near/exact_direct/server.py +303 -0
  73. t402/schemes/near/types.py +419 -0
  74. t402/schemes/near/upto/__init__.py +54 -0
  75. t402/schemes/near/upto/types.py +272 -0
  76. t402/schemes/polkadot/__init__.py +72 -0
  77. t402/schemes/polkadot/constants.py +155 -0
  78. t402/schemes/polkadot/exact_direct/__init__.py +43 -0
  79. t402/schemes/polkadot/exact_direct/client.py +235 -0
  80. t402/schemes/polkadot/exact_direct/facilitator.py +428 -0
  81. t402/schemes/polkadot/exact_direct/server.py +292 -0
  82. t402/schemes/polkadot/types.py +385 -0
  83. t402/schemes/registry.py +6 -2
  84. t402/schemes/stacks/__init__.py +68 -0
  85. t402/schemes/stacks/constants.py +122 -0
  86. t402/schemes/stacks/exact_direct/__init__.py +43 -0
  87. t402/schemes/stacks/exact_direct/client.py +222 -0
  88. t402/schemes/stacks/exact_direct/facilitator.py +424 -0
  89. t402/schemes/stacks/exact_direct/server.py +292 -0
  90. t402/schemes/stacks/types.py +380 -0
  91. t402/schemes/svm/__init__.py +44 -0
  92. t402/schemes/svm/exact/__init__.py +35 -0
  93. t402/schemes/svm/exact/client.py +23 -0
  94. t402/schemes/svm/exact/facilitator.py +24 -0
  95. t402/schemes/svm/exact/server.py +20 -0
  96. t402/schemes/svm/upto/__init__.py +23 -0
  97. t402/schemes/svm/upto/types.py +193 -0
  98. t402/schemes/tezos/__init__.py +84 -0
  99. t402/schemes/tezos/constants.py +372 -0
  100. t402/schemes/tezos/exact_direct/__init__.py +22 -0
  101. t402/schemes/tezos/exact_direct/client.py +226 -0
  102. t402/schemes/tezos/exact_direct/facilitator.py +491 -0
  103. t402/schemes/tezos/exact_direct/server.py +277 -0
  104. t402/schemes/tezos/types.py +220 -0
  105. t402/schemes/ton/__init__.py +24 -2
  106. t402/schemes/ton/exact/__init__.py +7 -0
  107. t402/schemes/ton/exact/facilitator.py +730 -0
  108. t402/schemes/ton/exact/server.py +1 -1
  109. t402/schemes/ton/upto/__init__.py +31 -0
  110. t402/schemes/ton/upto/types.py +215 -0
  111. t402/schemes/tron/__init__.py +28 -2
  112. t402/schemes/tron/exact/__init__.py +9 -0
  113. t402/schemes/tron/exact/facilitator.py +673 -0
  114. t402/schemes/tron/exact/server.py +1 -1
  115. t402/schemes/tron/upto/__init__.py +30 -0
  116. t402/schemes/tron/upto/types.py +213 -0
  117. t402/stacks_paywall_template.py +2 -0
  118. t402/starlette/__init__.py +38 -0
  119. t402/starlette/middleware.py +522 -0
  120. t402/svm.py +45 -11
  121. t402/svm_paywall_template.py +1 -1
  122. t402/ton.py +6 -2
  123. t402/ton_paywall_template.py +1 -192
  124. t402/tron.py +2 -0
  125. t402/tron_paywall_template.py +2 -0
  126. t402/types.py +103 -3
  127. t402/wdk/chains.py +1 -1
  128. t402/wdk/errors.py +15 -5
  129. t402/wdk/signer.py +11 -2
  130. {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/METADATA +42 -1
  131. t402-1.10.0.dist-info/RECORD +156 -0
  132. t402-1.9.0.dist-info/RECORD +0 -72
  133. {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/WHEEL +0 -0
  134. {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,315 @@
1
+ """Cosmos/Noble Exact-Direct Scheme - Server Implementation.
2
+
3
+ This module provides the server-side implementation of the exact-direct payment
4
+ scheme for Cosmos/Noble networks.
5
+
6
+ The server:
7
+ 1. Parses user-friendly prices into atomic token amounts (6 decimals for USDC).
8
+ 2. Enhances payment requirements with chain-specific metadata (chainId, bech32Prefix, denom).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from decimal import Decimal, ROUND_DOWN
15
+ from typing import Any, Dict, List, Optional, Union
16
+
17
+ from t402.types import PaymentRequirementsV2, Network
18
+ from t402.schemes.interfaces import AssetAmount, SupportedKindDict
19
+ from t402.schemes.cosmos.constants import (
20
+ SCHEME_EXACT_DIRECT,
21
+ CAIP_FAMILY,
22
+ TokenInfo,
23
+ get_network_config,
24
+ get_token_by_denom,
25
+ get_token_info,
26
+ is_valid_network,
27
+ )
28
+
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class ExactDirectCosmosServerConfig:
34
+ """Configuration for the ExactDirectCosmosServerScheme.
35
+
36
+ Attributes:
37
+ preferred_token: Preferred token symbol (e.g., "USDC").
38
+ Defaults to the network's default token if not set.
39
+ """
40
+
41
+ def __init__(self, preferred_token: Optional[str] = None) -> None:
42
+ self.preferred_token = preferred_token
43
+
44
+
45
+ class ExactDirectCosmosServerScheme:
46
+ """Server scheme for Cosmos/Noble exact-direct payments.
47
+
48
+ Handles parsing user-friendly prices to atomic token amounts and enhancing
49
+ payment requirements with Cosmos-specific metadata.
50
+
51
+ Example:
52
+ ```python
53
+ scheme = ExactDirectCosmosServerScheme()
54
+
55
+ # Parse a USD price to USDC atomic units
56
+ asset_amount = await scheme.parse_price("$1.50", "cosmos:noble-1")
57
+ # Returns: {"amount": "1500000", "asset": "uusdc", "extra": {...}}
58
+
59
+ # Enhance requirements with chain metadata
60
+ enhanced = await scheme.enhance_requirements(
61
+ requirements, supported_kind, extensions
62
+ )
63
+ ```
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ config: Optional[ExactDirectCosmosServerConfig] = None,
69
+ ) -> None:
70
+ """Initialize the server scheme.
71
+
72
+ Args:
73
+ config: Optional configuration. If not provided, defaults are used.
74
+ """
75
+ self._config = config or ExactDirectCosmosServerConfig()
76
+
77
+ @property
78
+ def scheme(self) -> str:
79
+ """The scheme identifier."""
80
+ return SCHEME_EXACT_DIRECT
81
+
82
+ @property
83
+ def caip_family(self) -> str:
84
+ """The CAIP-2 family pattern for Cosmos networks."""
85
+ return CAIP_FAMILY
86
+
87
+ async def parse_price(
88
+ self,
89
+ price: Union[str, int, float, Dict[str, Any]],
90
+ network: Network,
91
+ ) -> AssetAmount:
92
+ """Parse a user-friendly price to atomic amount and asset.
93
+
94
+ Supports:
95
+ - String with $ prefix: "$1.50" -> 1500000 (6 decimals)
96
+ - String without prefix: "1.50" -> 1500000
97
+ - Integer/float: 1.50 -> 1500000
98
+ - Dict (already parsed): {"amount": "1500000", "asset": "uusdc"}
99
+
100
+ Args:
101
+ price: User-friendly price.
102
+ network: Network identifier (CAIP-2 format, e.g., "cosmos:noble-1").
103
+
104
+ Returns:
105
+ AssetAmount dict with amount (atomic units string), asset (denom),
106
+ and extra metadata (symbol, decimals).
107
+
108
+ Raises:
109
+ ValueError: If price format is invalid or network is unsupported.
110
+ """
111
+ if not is_valid_network(network):
112
+ raise ValueError(f"Unsupported network: {network}")
113
+
114
+ # Handle dict (already in AssetAmount format)
115
+ if isinstance(price, dict):
116
+ if "amount" in price:
117
+ token = self._get_default_token(network)
118
+ asset = price.get("asset", token.denom)
119
+ extra = price.get("extra", {})
120
+ return {
121
+ "amount": str(price["amount"]),
122
+ "asset": asset,
123
+ "extra": extra,
124
+ }
125
+
126
+ # Parse money to decimal
127
+ decimal_amount = self._parse_money_to_decimal(price)
128
+
129
+ # Convert to atomic units using the default token
130
+ return self._default_money_conversion(decimal_amount, network)
131
+
132
+ async def enhance_requirements(
133
+ self,
134
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
135
+ supported_kind: SupportedKindDict,
136
+ facilitator_extensions: List[str],
137
+ ) -> Union[PaymentRequirementsV2, Dict[str, Any]]:
138
+ """Enhance payment requirements with Cosmos-specific metadata.
139
+
140
+ Adds token denom as asset and includes chainId, bech32Prefix, denom,
141
+ and symbol/decimals metadata from the facilitator's supported kinds response.
142
+
143
+ Args:
144
+ requirements: Base payment requirements with amount set.
145
+ supported_kind: The matched SupportedKind from facilitator.
146
+ facilitator_extensions: Extensions supported by the facilitator.
147
+
148
+ Returns:
149
+ Enhanced requirements with asset and extra metadata.
150
+ """
151
+ # Convert to dict for modification
152
+ if hasattr(requirements, "model_dump"):
153
+ req = requirements.model_dump(by_alias=True)
154
+ else:
155
+ req = dict(requirements)
156
+
157
+ network = req.get("network", "")
158
+
159
+ if not is_valid_network(network):
160
+ raise ValueError(f"Unsupported network: {network}")
161
+
162
+ config = get_network_config(network)
163
+ if config is None:
164
+ raise ValueError(f"Unsupported network: {network}")
165
+
166
+ # If asset is not set, use the default token denom for the network
167
+ if not req.get("asset"):
168
+ token = self._get_default_token(network)
169
+ req["asset"] = token.denom
170
+
171
+ # If amount contains a decimal point, convert to atomic units
172
+ amount = req.get("amount", "")
173
+ if amount and "." in amount:
174
+ token = get_token_by_denom(network, req["asset"])
175
+ decimals = token.decimals if token else 6
176
+ req["amount"] = self._to_atomic_units(amount, decimals)
177
+
178
+ # Initialize extra map if needed
179
+ if "extra" not in req or req["extra"] is None:
180
+ req["extra"] = {}
181
+
182
+ # Add Cosmos-specific extra data
183
+ if "chainId" not in req["extra"]:
184
+ req["extra"]["chainId"] = config.chain_id
185
+ if "bech32Prefix" not in req["extra"]:
186
+ req["extra"]["bech32Prefix"] = config.bech32_prefix
187
+ if "denom" not in req["extra"]:
188
+ req["extra"]["denom"] = req["asset"]
189
+
190
+ # Add facilitator-provided extra fields (asset metadata)
191
+ if supported_kind.get("extra"):
192
+ sk_extra = supported_kind["extra"]
193
+ if "assetSymbol" in sk_extra:
194
+ req["extra"]["assetSymbol"] = sk_extra["assetSymbol"]
195
+ if "assetDecimals" in sk_extra:
196
+ req["extra"]["assetDecimals"] = sk_extra["assetDecimals"]
197
+
198
+ # Copy extension keys from supportedKind
199
+ if supported_kind.get("extra"):
200
+ for key in facilitator_extensions:
201
+ if key in supported_kind["extra"]:
202
+ req["extra"][key] = supported_kind["extra"][key]
203
+
204
+ return req
205
+
206
+ def _get_default_token(self, network: str) -> TokenInfo:
207
+ """Get the default token for a network.
208
+
209
+ Priority: configured preferred_token > network default.
210
+
211
+ Args:
212
+ network: The CAIP-2 network identifier.
213
+
214
+ Returns:
215
+ TokenInfo for the default token.
216
+ """
217
+ # If a preferred token is configured, try to use it
218
+ if self._config.preferred_token:
219
+ token = get_token_info(network, self._config.preferred_token)
220
+ if token:
221
+ return token
222
+
223
+ # Fall back to network default
224
+ config = get_network_config(network)
225
+ if config:
226
+ return config.default_token
227
+
228
+ # Final fallback (should not happen for valid networks)
229
+ from t402.schemes.cosmos.constants import USDC_TOKEN
230
+ return USDC_TOKEN
231
+
232
+ def _parse_money_to_decimal(self, price: Union[str, int, float]) -> Decimal:
233
+ """Convert a money value to a Decimal amount.
234
+
235
+ Handles formats like "$1.50", "1.50", 1.50, etc.
236
+
237
+ Args:
238
+ price: The price value to parse.
239
+
240
+ Returns:
241
+ Decimal amount.
242
+
243
+ Raises:
244
+ ValueError: If the price format is invalid.
245
+ """
246
+ if isinstance(price, str):
247
+ clean_price = price.strip()
248
+ if clean_price.startswith("$"):
249
+ clean_price = clean_price[1:]
250
+ clean_price = clean_price.strip()
251
+
252
+ # Use the first space-separated part as the amount
253
+ parts = clean_price.split()
254
+ if parts:
255
+ try:
256
+ return Decimal(parts[0])
257
+ except Exception:
258
+ raise ValueError(f"Failed to parse price string: {price!r}")
259
+ raise ValueError("Empty price string after cleanup")
260
+ elif isinstance(price, (int, float)):
261
+ return Decimal(str(price))
262
+ else:
263
+ raise ValueError(f"Invalid price format: {price!r}")
264
+
265
+ def _default_money_conversion(self, amount: Decimal, network: str) -> AssetAmount:
266
+ """Convert a decimal amount to the default token's atomic units.
267
+
268
+ Args:
269
+ amount: Decimal amount in human-readable units.
270
+ network: The CAIP-2 network identifier.
271
+
272
+ Returns:
273
+ AssetAmount with atomic units, denom, and metadata.
274
+ """
275
+ token = self._get_default_token(network)
276
+ atomic_amount = self._to_atomic_units(str(amount), token.decimals)
277
+
278
+ return {
279
+ "amount": atomic_amount,
280
+ "asset": token.denom,
281
+ "extra": {
282
+ "symbol": token.symbol,
283
+ "decimals": token.decimals,
284
+ },
285
+ }
286
+
287
+ def _to_atomic_units(self, amount: str, decimals: int) -> str:
288
+ """Convert a decimal string amount to atomic units string.
289
+
290
+ For example, with decimals=6: "1.50" -> "1500000".
291
+
292
+ Args:
293
+ amount: The decimal amount string.
294
+ decimals: The number of decimal places for the token.
295
+
296
+ Returns:
297
+ Atomic units as a string.
298
+
299
+ Raises:
300
+ ValueError: If the amount is invalid or negative.
301
+ """
302
+ amount = amount.strip()
303
+ parsed = Decimal(amount)
304
+
305
+ if parsed < 0:
306
+ raise ValueError("Amount must be non-negative")
307
+
308
+ # Convert to atomic units
309
+ multiplier = Decimal(10) ** decimals
310
+ atomic = parsed * multiplier
311
+
312
+ # Truncate to integer (no rounding up)
313
+ atomic_int = int(atomic.to_integral_value(rounding=ROUND_DOWN))
314
+
315
+ return str(atomic_int)