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,501 @@
1
+ """Cosmos/Noble blockchain types for the T402 protocol.
2
+
3
+ This module defines the data types used by the Cosmos exact-direct payment scheme,
4
+ including transaction structures, payload types, and signer protocols.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
13
+ from pydantic.alias_generators import to_camel
14
+
15
+ from t402.schemes.cosmos.constants import MSG_TYPE_SEND
16
+
17
+
18
+ # =============================================================================
19
+ # Signer Protocols
20
+ # =============================================================================
21
+
22
+
23
+ @runtime_checkable
24
+ class ClientCosmosSigner(Protocol):
25
+ """Protocol for Cosmos client-side signing operations.
26
+
27
+ Implementations should handle key management and transaction signing
28
+ for the Cosmos blockchain.
29
+
30
+ Example:
31
+ ```python
32
+ class MyCosmosSigner:
33
+ def address(self) -> str:
34
+ return "noble1abc..."
35
+
36
+ async def send_tokens(
37
+ self, network: str, to: str, amount: str, denom: str
38
+ ) -> str:
39
+ # Build, sign, and send the transaction
40
+ return "tx_hash_here"
41
+ ```
42
+ """
43
+
44
+ def address(self) -> str:
45
+ """Get the signer's Cosmos address.
46
+
47
+ Returns:
48
+ The bech32-encoded address (e.g., "noble1abc...").
49
+ """
50
+ ...
51
+
52
+ async def send_tokens(
53
+ self,
54
+ network: str,
55
+ to: str,
56
+ amount: str,
57
+ denom: str,
58
+ ) -> str:
59
+ """Send tokens and return the transaction hash.
60
+
61
+ This is a pre-signed direct transfer (exact-direct scheme).
62
+
63
+ Args:
64
+ network: The CAIP-2 network identifier (e.g., "cosmos:noble-1").
65
+ to: The recipient's bech32 address.
66
+ amount: The amount in atomic units (e.g., "1000000" for 1 USDC).
67
+ denom: The token denomination (e.g., "uusdc").
68
+
69
+ Returns:
70
+ Transaction hash string on success.
71
+
72
+ Raises:
73
+ Exception: If the transaction fails.
74
+ """
75
+ ...
76
+
77
+
78
+ @runtime_checkable
79
+ class FacilitatorCosmosSigner(Protocol):
80
+ """Protocol for Cosmos facilitator-side operations.
81
+
82
+ Implementations should handle transaction querying and balance lookups
83
+ for the Cosmos blockchain.
84
+
85
+ Example:
86
+ ```python
87
+ class MyCosmosRPC:
88
+ def get_addresses(self, network: str) -> List[str]:
89
+ return ["noble1facilitator..."]
90
+
91
+ async def query_transaction(
92
+ self, network: str, tx_hash: str
93
+ ) -> TransactionResult:
94
+ # Query the Cosmos REST API for the transaction
95
+ return TransactionResult(...)
96
+
97
+ async def get_balance(
98
+ self, network: str, address: str, denom: str
99
+ ) -> str:
100
+ return "1000000"
101
+ ```
102
+ """
103
+
104
+ def get_addresses(self, network: str) -> List[str]:
105
+ """Get the facilitator's Cosmos addresses for a network.
106
+
107
+ Args:
108
+ network: The CAIP-2 network identifier.
109
+
110
+ Returns:
111
+ List of bech32-encoded addresses.
112
+ """
113
+ ...
114
+
115
+ async def query_transaction(
116
+ self,
117
+ network: str,
118
+ tx_hash: str,
119
+ ) -> TransactionResult:
120
+ """Query a transaction by hash from the Cosmos REST API.
121
+
122
+ Args:
123
+ network: The CAIP-2 network identifier.
124
+ tx_hash: The transaction hash to query.
125
+
126
+ Returns:
127
+ TransactionResult with the transaction details.
128
+
129
+ Raises:
130
+ Exception: If the transaction is not found or the query fails.
131
+ """
132
+ ...
133
+
134
+ async def get_balance(
135
+ self,
136
+ network: str,
137
+ address: str,
138
+ denom: str,
139
+ ) -> str:
140
+ """Get the token balance for an account.
141
+
142
+ Args:
143
+ network: The CAIP-2 network identifier.
144
+ address: The bech32-encoded address.
145
+ denom: The token denomination (e.g., "uusdc").
146
+
147
+ Returns:
148
+ Balance in atomic units as a string.
149
+
150
+ Raises:
151
+ Exception: If the balance query fails.
152
+ """
153
+ ...
154
+
155
+
156
+ # =============================================================================
157
+ # Transaction Types
158
+ # =============================================================================
159
+
160
+
161
+ class Coin:
162
+ """Represents a Cosmos SDK Coin.
163
+
164
+ Attributes:
165
+ denom: The token denomination.
166
+ amount: The amount in atomic units.
167
+ """
168
+
169
+ def __init__(self, denom: str, amount: str) -> None:
170
+ self.denom = denom
171
+ self.amount = amount
172
+
173
+ def to_dict(self) -> Dict[str, str]:
174
+ """Convert to a dict.
175
+
176
+ Returns:
177
+ Dict with denom and amount fields.
178
+ """
179
+ return {"denom": self.denom, "amount": self.amount}
180
+
181
+ @classmethod
182
+ def from_dict(cls, data: Dict[str, Any]) -> Coin:
183
+ """Create a Coin from a dict.
184
+
185
+ Args:
186
+ data: Dict with denom and amount fields.
187
+
188
+ Returns:
189
+ Coin instance.
190
+ """
191
+ return cls(
192
+ denom=data.get("denom", ""),
193
+ amount=data.get("amount", "0"),
194
+ )
195
+
196
+
197
+ class MsgSend:
198
+ """Represents a Cosmos SDK MsgSend message.
199
+
200
+ Attributes:
201
+ type: The message type URL (should be /cosmos.bank.v1beta1.MsgSend).
202
+ from_address: The sender's bech32 address.
203
+ to_address: The recipient's bech32 address.
204
+ amount: List of Coin objects.
205
+ """
206
+
207
+ def __init__(
208
+ self,
209
+ type: str,
210
+ from_address: str,
211
+ to_address: str,
212
+ amount: List[Coin],
213
+ ) -> None:
214
+ self.type = type
215
+ self.from_address = from_address
216
+ self.to_address = to_address
217
+ self.amount = amount
218
+
219
+ def get_amount_by_denom(self, denom: str) -> str:
220
+ """Get the amount for a specific denomination.
221
+
222
+ Args:
223
+ denom: The token denomination to look for.
224
+
225
+ Returns:
226
+ Amount string if found, empty string otherwise.
227
+ """
228
+ for coin in self.amount:
229
+ if coin.denom == denom:
230
+ return coin.amount
231
+ return ""
232
+
233
+ @classmethod
234
+ def from_dict(cls, data: Dict[str, Any]) -> Optional[MsgSend]:
235
+ """Parse a message dict into a MsgSend.
236
+
237
+ Returns None if the message is not a MsgSend type.
238
+
239
+ Args:
240
+ data: Dict representing a Cosmos message.
241
+
242
+ Returns:
243
+ MsgSend instance if it is a bank send message, None otherwise.
244
+ """
245
+ msg_type = data.get("@type", "")
246
+ if msg_type != MSG_TYPE_SEND:
247
+ return None
248
+
249
+ coins = []
250
+ for coin_data in data.get("amount", []):
251
+ coins.append(Coin.from_dict(coin_data))
252
+
253
+ return cls(
254
+ type=msg_type,
255
+ from_address=data.get("from_address", ""),
256
+ to_address=data.get("to_address", ""),
257
+ amount=coins,
258
+ )
259
+
260
+
261
+ class TxEvent:
262
+ """Represents an event in a transaction log.
263
+
264
+ Attributes:
265
+ type: The event type.
266
+ attributes: List of key-value attribute dicts.
267
+ """
268
+
269
+ def __init__(self, type: str, attributes: List[Dict[str, str]]) -> None:
270
+ self.type = type
271
+ self.attributes = attributes
272
+
273
+ @classmethod
274
+ def from_dict(cls, data: Dict[str, Any]) -> TxEvent:
275
+ """Create a TxEvent from a dict.
276
+
277
+ Args:
278
+ data: Dict with type and attributes fields.
279
+
280
+ Returns:
281
+ TxEvent instance.
282
+ """
283
+ return cls(
284
+ type=data.get("type", ""),
285
+ attributes=data.get("attributes", []),
286
+ )
287
+
288
+
289
+ class TxLog:
290
+ """Represents a transaction log.
291
+
292
+ Attributes:
293
+ msg_index: The message index.
294
+ log: The log string.
295
+ events: List of TxEvent objects.
296
+ """
297
+
298
+ def __init__(self, msg_index: int, log: str, events: List[TxEvent]) -> None:
299
+ self.msg_index = msg_index
300
+ self.log = log
301
+ self.events = events
302
+
303
+ @classmethod
304
+ def from_dict(cls, data: Dict[str, Any]) -> TxLog:
305
+ """Create a TxLog from a dict.
306
+
307
+ Args:
308
+ data: Dict with msg_index, log, and events fields.
309
+
310
+ Returns:
311
+ TxLog instance.
312
+ """
313
+ events = [TxEvent.from_dict(e) for e in data.get("events", [])]
314
+ return cls(
315
+ msg_index=data.get("msg_index", 0),
316
+ log=data.get("log", ""),
317
+ events=events,
318
+ )
319
+
320
+
321
+ class TransactionResult:
322
+ """Represents the result of a Cosmos transaction query.
323
+
324
+ Attributes:
325
+ tx_hash: The transaction hash.
326
+ height: The block height.
327
+ code: The response code (0 = success).
328
+ raw_log: The raw log string.
329
+ logs: List of TxLog objects.
330
+ gas_wanted: The gas requested.
331
+ gas_used: The gas consumed.
332
+ messages: List of raw message dicts from the transaction body.
333
+ timestamp: The block timestamp.
334
+ """
335
+
336
+ def __init__(
337
+ self,
338
+ tx_hash: str = "",
339
+ height: str = "",
340
+ code: int = 0,
341
+ raw_log: str = "",
342
+ logs: Optional[List[TxLog]] = None,
343
+ gas_wanted: str = "",
344
+ gas_used: str = "",
345
+ messages: Optional[List[Dict[str, Any]]] = None,
346
+ timestamp: str = "",
347
+ ) -> None:
348
+ self.tx_hash = tx_hash
349
+ self.height = height
350
+ self.code = code
351
+ self.raw_log = raw_log
352
+ self.logs = logs or []
353
+ self.gas_wanted = gas_wanted
354
+ self.gas_used = gas_used
355
+ self.messages = messages or []
356
+ self.timestamp = timestamp
357
+
358
+ def is_success(self) -> bool:
359
+ """Check if the transaction succeeded.
360
+
361
+ Returns:
362
+ True if the response code is 0.
363
+ """
364
+ return self.code == 0
365
+
366
+ def find_msg_send(self) -> Optional[MsgSend]:
367
+ """Find the first MsgSend message in the transaction.
368
+
369
+ Returns:
370
+ MsgSend if found, None otherwise.
371
+ """
372
+ for msg_data in self.messages:
373
+ msg = MsgSend.from_dict(msg_data)
374
+ if msg is not None:
375
+ return msg
376
+ return None
377
+
378
+ @classmethod
379
+ def from_dict(cls, data: Dict[str, Any]) -> TransactionResult:
380
+ """Create a TransactionResult from a REST API response dict.
381
+
382
+ Handles both the direct format and the nested tx_response format.
383
+
384
+ Args:
385
+ data: Dict from the Cosmos REST API.
386
+
387
+ Returns:
388
+ TransactionResult instance.
389
+ """
390
+ # Handle nested REST API response format
391
+ tx_response = data.get("tx_response", data)
392
+ tx_wrapper = data.get("tx", {})
393
+
394
+ # Parse logs
395
+ logs = []
396
+ for log_data in tx_response.get("logs", []):
397
+ logs.append(TxLog.from_dict(log_data))
398
+
399
+ # Parse messages from tx body
400
+ messages: List[Dict[str, Any]] = []
401
+ body = tx_wrapper.get("body", {})
402
+ raw_messages = body.get("messages", [])
403
+ for raw_msg in raw_messages:
404
+ if isinstance(raw_msg, dict):
405
+ messages.append(raw_msg)
406
+ elif isinstance(raw_msg, (str, bytes)):
407
+ try:
408
+ messages.append(json.loads(raw_msg))
409
+ except (json.JSONDecodeError, TypeError):
410
+ continue
411
+
412
+ return cls(
413
+ tx_hash=tx_response.get("txhash", tx_response.get("tx_hash", "")),
414
+ height=tx_response.get("height", ""),
415
+ code=tx_response.get("code", 0),
416
+ raw_log=tx_response.get("raw_log", ""),
417
+ logs=logs,
418
+ gas_wanted=tx_response.get("gas_wanted", ""),
419
+ gas_used=tx_response.get("gas_used", ""),
420
+ messages=messages,
421
+ timestamp=tx_response.get("timestamp", ""),
422
+ )
423
+
424
+
425
+ # =============================================================================
426
+ # Payload Types
427
+ # =============================================================================
428
+
429
+
430
+ class ExactDirectPayload(BaseModel):
431
+ """Payload for the exact-direct scheme on Cosmos.
432
+
433
+ Contains the transaction hash as proof of on-chain payment,
434
+ along with transfer details for verification.
435
+
436
+ Attributes:
437
+ tx_hash: The on-chain transaction hash.
438
+ from_address: The sender's bech32 address.
439
+ to_address: The recipient's bech32 address.
440
+ amount: The transfer amount in atomic units.
441
+ denom: The token denomination (optional, defaults to "uusdc").
442
+ """
443
+
444
+ tx_hash: str = Field(alias="txHash")
445
+ from_address: str = Field(alias="from")
446
+ to_address: str = Field(alias="to")
447
+ amount: str
448
+ denom: str = ""
449
+
450
+ model_config = ConfigDict(
451
+ alias_generator=to_camel,
452
+ populate_by_name=True,
453
+ from_attributes=True,
454
+ )
455
+
456
+ @field_validator("amount")
457
+ @classmethod
458
+ def validate_amount(cls, v: str) -> str:
459
+ """Validate that amount is a valid integer string."""
460
+ try:
461
+ int(v)
462
+ except ValueError:
463
+ raise ValueError("amount must be an integer encoded as a string")
464
+ return v
465
+
466
+ def to_map(self) -> Dict[str, Any]:
467
+ """Convert the payload to a plain dict for inclusion in PaymentPayload.
468
+
469
+ Returns:
470
+ Dict with txHash, from, to, amount, and optionally denom fields.
471
+ """
472
+ m: Dict[str, Any] = {
473
+ "txHash": self.tx_hash,
474
+ "from": self.from_address,
475
+ "to": self.to_address,
476
+ "amount": self.amount,
477
+ }
478
+ if self.denom:
479
+ m["denom"] = self.denom
480
+ return m
481
+
482
+ @classmethod
483
+ def from_map(cls, data: Dict[str, Any]) -> ExactDirectPayload:
484
+ """Create an ExactDirectPayload from a plain dict.
485
+
486
+ Args:
487
+ data: Dict with txHash, from, to, amount, and optionally denom fields.
488
+
489
+ Returns:
490
+ ExactDirectPayload instance.
491
+
492
+ Raises:
493
+ ValueError: If required fields are missing.
494
+ """
495
+ return cls(
496
+ tx_hash=data.get("txHash", ""),
497
+ from_address=data.get("from", ""),
498
+ to_address=data.get("to", ""),
499
+ amount=data.get("amount", "0"),
500
+ denom=data.get("denom", ""),
501
+ )
@@ -22,7 +22,7 @@ Supported schemes:
22
22
  1. Replace `ExactLegacyEvmClientScheme` with `ExactEvmClientScheme`
23
23
  2. Replace `ExactLegacyEvmServerScheme` with `ExactEvmServerScheme`
24
24
  3. Use USDT0 token addresses instead of legacy USDT
25
- 4. See https://docs.t402.io/migration/exact-legacy for details
25
+ 4. See https://docs.t402.io/advanced/migration-v1-to-v2 for details
26
26
 
27
27
  **USDT0 Advantages:**
28
28
  - Single signature (no approve transaction)
@@ -10,7 +10,7 @@ for EVM networks using the approve + transferFrom pattern.
10
10
  **Migration Guide:**
11
11
  - Replace: `SCHEME_EXACT_LEGACY` with `SCHEME_EXACT`
12
12
  - Replace: legacy USDT tokens with USDT0 tokens
13
- - See https://docs.t402.io/migration/exact-legacy for full migration guide
13
+ - See https://docs.t402.io/advanced/migration-v1-to-v2 for full migration guide
14
14
 
15
15
  **Why Migrate:**
16
16
  1. Gasless transfers: USDT0 supports EIP-3009, eliminating gas costs for users
@@ -4,6 +4,7 @@ This package provides payment scheme implementations for the NEAR blockchain.
4
4
 
5
5
  Supported schemes:
6
6
  - exact-direct: Client executes NEP-141 ft_transfer, tx hash used as proof.
7
+ - upto: Escrow-based pattern for usage-based billing (ft_transfer to facilitator).
7
8
 
8
9
  Usage:
9
10
  ```python
@@ -24,6 +25,13 @@ Usage:
24
25
  SCHEME_EXACT_DIRECT,
25
26
  NEAR_MAINNET,
26
27
  NEAR_TESTNET,
28
+ # Upto types
29
+ UptoNearAuthorization,
30
+ UptoNearPayload,
31
+ UptoNearExtra,
32
+ UptoNearSettlement,
33
+ is_upto_near_payload,
34
+ upto_payload_from_dict,
27
35
  )
28
36
  ```
29
37
  """
@@ -43,6 +51,14 @@ from t402.schemes.near.types import (
43
51
  FtTransferArgs,
44
52
  is_valid_account_id,
45
53
  )
54
+ from t402.schemes.near.upto import (
55
+ UptoNearAuthorization,
56
+ UptoNearPayload,
57
+ UptoNearExtra,
58
+ UptoNearSettlement,
59
+ is_upto_near_payload,
60
+ upto_payload_from_dict,
61
+ )
46
62
  from t402.schemes.near.constants import (
47
63
  SCHEME_EXACT_DIRECT,
48
64
  NEAR_MAINNET,
@@ -82,6 +98,15 @@ __all__ = [
82
98
  # Payload types
83
99
  "ExactDirectPayload",
84
100
  "FtTransferArgs",
101
+ # Upto types
102
+ "UptoNearAuthorization",
103
+ "UptoNearPayload",
104
+ "UptoNearExtra",
105
+ "UptoNearSettlement",
106
+ # Upto type guards
107
+ "is_upto_near_payload",
108
+ # Upto factory functions
109
+ "upto_payload_from_dict",
85
110
  # Validation
86
111
  "is_valid_account_id",
87
112
  "is_valid_network",
@@ -0,0 +1,54 @@
1
+ """NEAR Up-To Payment Scheme Types.
2
+
3
+ This package provides the upto payment scheme types for NEAR networks
4
+ using an escrow pattern with NEP-141 ft_transfer.
5
+
6
+ The upto scheme allows clients to authorize a maximum amount that can be
7
+ settled later based on actual usage. Since NEAR NEP-141 tokens don't support
8
+ native approve/transferFrom, this uses an escrow pattern:
9
+
10
+ 1. Client ft_transfers maxAmount to the facilitator
11
+ 2. Facilitator verifies the transfer
12
+ 3. Facilitator forwards settleAmount to payTo and refunds the rest
13
+
14
+ Usage:
15
+ ```python
16
+ from t402.schemes.near.upto import (
17
+ UptoNearAuthorization,
18
+ UptoNearPayload,
19
+ UptoNearExtra,
20
+ UptoNearSettlement,
21
+ is_upto_near_payload,
22
+ upto_payload_from_dict,
23
+ )
24
+
25
+ # Check if data is an upto NEAR payload
26
+ if is_upto_near_payload(data):
27
+ payload = upto_payload_from_dict(data)
28
+ print(payload.tx_hash)
29
+ print(payload.authorization.max_amount)
30
+ ```
31
+ """
32
+
33
+ from t402.schemes.near.upto.types import (
34
+ UptoNearAuthorization,
35
+ UptoNearPayload,
36
+ UptoNearExtra,
37
+ UptoNearSettlement,
38
+ UptoNearUsageDetails,
39
+ is_upto_near_payload,
40
+ upto_payload_from_dict,
41
+ )
42
+
43
+ __all__ = [
44
+ # Models
45
+ "UptoNearAuthorization",
46
+ "UptoNearPayload",
47
+ "UptoNearExtra",
48
+ "UptoNearSettlement",
49
+ "UptoNearUsageDetails",
50
+ # Type guards
51
+ "is_upto_near_payload",
52
+ # Factory functions
53
+ "upto_payload_from_dict",
54
+ ]