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
t402/errors.py ADDED
@@ -0,0 +1,213 @@
1
+ """Standardized T402 error codes returned by the facilitator API.
2
+
3
+ Error codes follow the format T402-XYYY where X is the category (1-8)
4
+ and YYY is the specific error within that category.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ # Client Errors (T402-1xxx): Invalid input, malformed requests
13
+ ERR_INVALID_REQUEST = "T402-1001"
14
+ ERR_MISSING_PAYLOAD = "T402-1002"
15
+ ERR_MISSING_REQUIREMENTS = "T402-1003"
16
+ ERR_INVALID_PAYLOAD = "T402-1004"
17
+ ERR_INVALID_REQUIREMENTS = "T402-1005"
18
+ ERR_INVALID_SIGNATURE = "T402-1006"
19
+ ERR_INVALID_NETWORK = "T402-1007"
20
+ ERR_INVALID_SCHEME = "T402-1008"
21
+ ERR_INVALID_AMOUNT = "T402-1009"
22
+ ERR_INVALID_ADDRESS = "T402-1010"
23
+ ERR_EXPIRED_PAYMENT = "T402-1011"
24
+ ERR_INVALID_NONCE = "T402-1012"
25
+ ERR_INSUFFICIENT_AMOUNT = "T402-1013"
26
+ ERR_INVALID_IDEMPOTENCY_KEY = "T402-1014"
27
+ ERR_SIGNATURE_EXPIRED = "T402-1015"
28
+
29
+ # Server Errors (T402-2xxx): Internal failures, dependency issues
30
+ ERR_INTERNAL = "T402-2001"
31
+ ERR_DATABASE_UNAVAILABLE = "T402-2002"
32
+ ERR_CACHE_UNAVAILABLE = "T402-2003"
33
+ ERR_RPC_UNAVAILABLE = "T402-2004"
34
+ ERR_RATE_LIMITED = "T402-2005"
35
+ ERR_SERVICE_UNAVAILABLE = "T402-2006"
36
+
37
+ # Facilitator Errors (T402-3xxx): Verification and settlement failures
38
+ ERR_VERIFICATION_FAILED = "T402-3001"
39
+ ERR_SETTLEMENT_FAILED = "T402-3002"
40
+ ERR_INSUFFICIENT_BALANCE = "T402-3003"
41
+ ERR_ALLOWANCE_INSUFFICIENT = "T402-3004"
42
+ ERR_PAYMENT_MISMATCH = "T402-3005"
43
+ ERR_DUPLICATE_PAYMENT = "T402-3006"
44
+ ERR_SETTLEMENT_PENDING = "T402-3007"
45
+ ERR_SETTLEMENT_TIMEOUT = "T402-3008"
46
+ ERR_NONCE_REPLAY = "T402-3009"
47
+ ERR_IDEMPOTENCY_CONFLICT = "T402-3010"
48
+ ERR_IDEMPOTENCY_UNAVAILABLE = "T402-3011"
49
+ ERR_PREVIOUS_REQUEST_FAILED = "T402-3012"
50
+ ERR_REQUEST_IN_PROGRESS = "T402-3013"
51
+
52
+ # Chain-Specific Errors (T402-4xxx): Network and transaction issues
53
+ ERR_CHAIN_UNAVAILABLE = "T402-4001"
54
+ ERR_TRANSACTION_FAILED = "T402-4002"
55
+ ERR_TRANSACTION_REVERTED = "T402-4003"
56
+ ERR_GAS_ESTIMATION_FAILED = "T402-4004"
57
+ ERR_NONCE_CONFLICT = "T402-4005"
58
+ ERR_CHAIN_CONGESTED = "T402-4006"
59
+ ERR_CONTRACT_ERROR = "T402-4007"
60
+
61
+ # Bridge Errors (T402-5xxx): Cross-chain operation failures
62
+ ERR_BRIDGE_UNAVAILABLE = "T402-5001"
63
+ ERR_BRIDGE_QUOTE_FAILED = "T402-5002"
64
+ ERR_BRIDGE_TRANSFER_FAILED = "T402-5003"
65
+ ERR_BRIDGE_TIMEOUT = "T402-5004"
66
+ ERR_UNSUPPORTED_ROUTE = "T402-5005"
67
+
68
+ # Streaming Errors (T402-6xxx): Payment stream issues
69
+ ERR_STREAM_NOT_FOUND = "T402-6001"
70
+ ERR_STREAM_ALREADY_CLOSED = "T402-6002"
71
+ ERR_STREAM_ALREADY_PAUSED = "T402-6003"
72
+ ERR_STREAM_NOT_PAUSED = "T402-6004"
73
+ ERR_STREAM_AMOUNT_EXCEEDED = "T402-6005"
74
+ ERR_STREAM_EXPIRED = "T402-6006"
75
+ ERR_STREAM_INVALID_STATE = "T402-6007"
76
+ ERR_STREAM_RATE_LIMITED = "T402-6008"
77
+
78
+ # Intent Errors (T402-7xxx): Payment intent issues
79
+ ERR_INTENT_NOT_FOUND = "T402-7001"
80
+ ERR_INTENT_ALREADY_EXECUTED = "T402-7002"
81
+ ERR_INTENT_CANCELLED = "T402-7003"
82
+ ERR_INTENT_EXPIRED = "T402-7004"
83
+ ERR_NO_ROUTES_AVAILABLE = "T402-7005"
84
+ ERR_ROUTE_EXPIRED = "T402-7006"
85
+ ERR_ROUTE_NOT_SELECTED = "T402-7007"
86
+ ERR_INTENT_INVALID_STATE = "T402-7008"
87
+
88
+ # Discovery Errors (T402-8xxx): Resource marketplace issues
89
+ ERR_RESOURCE_NOT_FOUND = "T402-8001"
90
+ ERR_RESOURCE_ALREADY_EXISTS = "T402-8002"
91
+ ERR_INVALID_PARAMETERS = "T402-8003"
92
+ ERR_NOT_AUTHORIZED = "T402-8004"
93
+
94
+ # All error code constants for iteration/validation
95
+ ALL_ERROR_CODES: list[str] = [
96
+ ERR_INVALID_REQUEST, ERR_MISSING_PAYLOAD, ERR_MISSING_REQUIREMENTS,
97
+ ERR_INVALID_PAYLOAD, ERR_INVALID_REQUIREMENTS, ERR_INVALID_SIGNATURE,
98
+ ERR_INVALID_NETWORK, ERR_INVALID_SCHEME, ERR_INVALID_AMOUNT,
99
+ ERR_INVALID_ADDRESS, ERR_EXPIRED_PAYMENT, ERR_INVALID_NONCE,
100
+ ERR_INSUFFICIENT_AMOUNT, ERR_INVALID_IDEMPOTENCY_KEY, ERR_SIGNATURE_EXPIRED,
101
+ ERR_INTERNAL, ERR_DATABASE_UNAVAILABLE, ERR_CACHE_UNAVAILABLE,
102
+ ERR_RPC_UNAVAILABLE, ERR_RATE_LIMITED, ERR_SERVICE_UNAVAILABLE,
103
+ ERR_VERIFICATION_FAILED, ERR_SETTLEMENT_FAILED, ERR_INSUFFICIENT_BALANCE,
104
+ ERR_ALLOWANCE_INSUFFICIENT, ERR_PAYMENT_MISMATCH, ERR_DUPLICATE_PAYMENT,
105
+ ERR_SETTLEMENT_PENDING, ERR_SETTLEMENT_TIMEOUT, ERR_NONCE_REPLAY,
106
+ ERR_IDEMPOTENCY_CONFLICT, ERR_IDEMPOTENCY_UNAVAILABLE,
107
+ ERR_PREVIOUS_REQUEST_FAILED, ERR_REQUEST_IN_PROGRESS,
108
+ ERR_CHAIN_UNAVAILABLE, ERR_TRANSACTION_FAILED, ERR_TRANSACTION_REVERTED,
109
+ ERR_GAS_ESTIMATION_FAILED, ERR_NONCE_CONFLICT, ERR_CHAIN_CONGESTED,
110
+ ERR_CONTRACT_ERROR,
111
+ ERR_BRIDGE_UNAVAILABLE, ERR_BRIDGE_QUOTE_FAILED, ERR_BRIDGE_TRANSFER_FAILED,
112
+ ERR_BRIDGE_TIMEOUT, ERR_UNSUPPORTED_ROUTE,
113
+ ERR_STREAM_NOT_FOUND, ERR_STREAM_ALREADY_CLOSED, ERR_STREAM_ALREADY_PAUSED,
114
+ ERR_STREAM_NOT_PAUSED, ERR_STREAM_AMOUNT_EXCEEDED, ERR_STREAM_EXPIRED,
115
+ ERR_STREAM_INVALID_STATE, ERR_STREAM_RATE_LIMITED,
116
+ ERR_INTENT_NOT_FOUND, ERR_INTENT_ALREADY_EXECUTED, ERR_INTENT_CANCELLED,
117
+ ERR_INTENT_EXPIRED, ERR_NO_ROUTES_AVAILABLE, ERR_ROUTE_EXPIRED,
118
+ ERR_ROUTE_NOT_SELECTED, ERR_INTENT_INVALID_STATE,
119
+ ERR_RESOURCE_NOT_FOUND, ERR_RESOURCE_ALREADY_EXISTS,
120
+ ERR_INVALID_PARAMETERS, ERR_NOT_AUTHORIZED,
121
+ ]
122
+
123
+
124
+ @dataclass
125
+ class APIError(Exception):
126
+ """Structured error response from the facilitator API."""
127
+
128
+ code: str
129
+ message: str
130
+ details: str = ""
131
+ retry: bool = False
132
+
133
+ def __str__(self) -> str:
134
+ if self.details:
135
+ return f"[{self.code}] {self.message}: {self.details}"
136
+ return f"[{self.code}] {self.message}"
137
+
138
+ @property
139
+ def http_status(self) -> int:
140
+ """Returns the expected HTTP status code for this error code."""
141
+ return http_status_for_code(self.code)
142
+
143
+ @property
144
+ def is_client_error(self) -> bool:
145
+ return len(self.code) >= 6 and self.code[5] == "1"
146
+
147
+ @property
148
+ def is_server_error(self) -> bool:
149
+ return len(self.code) >= 6 and self.code[5] == "2"
150
+
151
+ @property
152
+ def is_facilitator_error(self) -> bool:
153
+ return len(self.code) >= 6 and self.code[5] == "3"
154
+
155
+ @property
156
+ def is_chain_error(self) -> bool:
157
+ return len(self.code) >= 6 and self.code[5] == "4"
158
+
159
+ @property
160
+ def is_bridge_error(self) -> bool:
161
+ return len(self.code) >= 6 and self.code[5] == "5"
162
+
163
+ @property
164
+ def is_retryable(self) -> bool:
165
+ return self.retry
166
+
167
+ @classmethod
168
+ def from_dict(cls, data: dict) -> "APIError":
169
+ """Create an APIError from a dictionary (e.g., from JSON response)."""
170
+ return cls(
171
+ code=data.get("code", ""),
172
+ message=data.get("message", ""),
173
+ details=data.get("details", ""),
174
+ retry=data.get("retry", False),
175
+ )
176
+
177
+
178
+ def http_status_for_code(code: str) -> int:
179
+ """Returns the expected HTTP status code for a given T402 error code."""
180
+ if len(code) < 6:
181
+ return 500
182
+ category = code[5]
183
+ if category == "1":
184
+ return 400
185
+ if category == "2":
186
+ if code == ERR_RATE_LIMITED:
187
+ return 429
188
+ return 500
189
+ if category == "3":
190
+ if code in (ERR_VERIFICATION_FAILED, ERR_PAYMENT_MISMATCH):
191
+ return 422
192
+ return 500
193
+ if category == "4":
194
+ return 502
195
+ if category == "5":
196
+ return 502
197
+ if category == "6":
198
+ if code == ERR_STREAM_NOT_FOUND:
199
+ return 404
200
+ return 400
201
+ if category == "7":
202
+ if code == ERR_INTENT_NOT_FOUND:
203
+ return 404
204
+ return 400
205
+ if category == "8":
206
+ if code == ERR_RESOURCE_NOT_FOUND:
207
+ return 404
208
+ if code == ERR_RESOURCE_ALREADY_EXISTS:
209
+ return 409
210
+ if code == ERR_NOT_AUTHORIZED:
211
+ return 403
212
+ return 400
213
+ return 500
t402/facilitator.py CHANGED
@@ -10,6 +10,10 @@ from t402.types import (
10
10
  SettleResponse,
11
11
  ListDiscoveryResourcesRequest,
12
12
  ListDiscoveryResourcesResponse,
13
+ DiscoveryItem,
14
+ RegisterResourceRequest,
15
+ RegisterResourceResponse,
16
+ UpdateResourceRequest,
13
17
  )
14
18
 
15
19
 
@@ -133,3 +137,124 @@ class FacilitatorClient:
133
137
 
134
138
  data = response.json()
135
139
  return ListDiscoveryResourcesResponse(**data)
140
+
141
+ async def get_resource(self, resource_id: str) -> DiscoveryItem:
142
+ """Get details of a specific discoverable resource.
143
+
144
+ Args:
145
+ resource_id: The unique ID of the resource
146
+
147
+ Returns:
148
+ DiscoveryItem with resource details
149
+ """
150
+ headers = {"Content-Type": "application/json"}
151
+
152
+ if self.config.get("create_headers"):
153
+ custom_headers = await self.config["create_headers"]()
154
+ headers.update(custom_headers.get("discovery", {}))
155
+
156
+ async with httpx.AsyncClient() as client:
157
+ response = await client.get(
158
+ f"{self.config['url']}/discovery/resources/{resource_id}",
159
+ headers=headers,
160
+ follow_redirects=True,
161
+ )
162
+
163
+ if response.status_code != 200:
164
+ raise ValueError(
165
+ f"Failed to get resource: {response.status_code} {response.text}"
166
+ )
167
+
168
+ data = response.json()
169
+ return DiscoveryItem(**data)
170
+
171
+ async def register_resource(
172
+ self, request: RegisterResourceRequest
173
+ ) -> RegisterResourceResponse:
174
+ """Register a new resource in the Bazaar.
175
+
176
+ Args:
177
+ request: Resource registration data
178
+
179
+ Returns:
180
+ RegisterResourceResponse with the new resource ID and metadata
181
+ """
182
+ headers = {"Content-Type": "application/json"}
183
+
184
+ if self.config.get("create_headers"):
185
+ custom_headers = await self.config["create_headers"]()
186
+ headers.update(custom_headers.get("discovery", {}))
187
+
188
+ async with httpx.AsyncClient() as client:
189
+ response = await client.post(
190
+ f"{self.config['url']}/discovery/register",
191
+ json=request.model_dump(by_alias=True, exclude_none=True),
192
+ headers=headers,
193
+ follow_redirects=True,
194
+ )
195
+
196
+ if response.status_code != 201:
197
+ raise ValueError(
198
+ f"Failed to register resource: {response.status_code} {response.text}"
199
+ )
200
+
201
+ data = response.json()
202
+ return RegisterResourceResponse(**data)
203
+
204
+ async def update_resource(
205
+ self, resource_id: str, request: UpdateResourceRequest
206
+ ) -> DiscoveryItem:
207
+ """Update an existing resource in the Bazaar.
208
+
209
+ Args:
210
+ resource_id: The unique ID of the resource to update
211
+ request: Resource update data
212
+
213
+ Returns:
214
+ DiscoveryItem with updated resource details
215
+ """
216
+ headers = {"Content-Type": "application/json"}
217
+
218
+ if self.config.get("create_headers"):
219
+ custom_headers = await self.config["create_headers"]()
220
+ headers.update(custom_headers.get("discovery", {}))
221
+
222
+ async with httpx.AsyncClient() as client:
223
+ response = await client.put(
224
+ f"{self.config['url']}/discovery/resources/{resource_id}",
225
+ json=request.model_dump(by_alias=True, exclude_none=True),
226
+ headers=headers,
227
+ follow_redirects=True,
228
+ )
229
+
230
+ if response.status_code != 200:
231
+ raise ValueError(
232
+ f"Failed to update resource: {response.status_code} {response.text}"
233
+ )
234
+
235
+ data = response.json()
236
+ return DiscoveryItem(**data)
237
+
238
+ async def delete_resource(self, resource_id: str) -> None:
239
+ """Delete a resource from the Bazaar.
240
+
241
+ Args:
242
+ resource_id: The unique ID of the resource to delete
243
+ """
244
+ headers = {"Content-Type": "application/json"}
245
+
246
+ if self.config.get("create_headers"):
247
+ custom_headers = await self.config["create_headers"]()
248
+ headers.update(custom_headers.get("discovery", {}))
249
+
250
+ async with httpx.AsyncClient() as client:
251
+ response = await client.delete(
252
+ f"{self.config['url']}/discovery/resources/{resource_id}",
253
+ headers=headers,
254
+ follow_redirects=True,
255
+ )
256
+
257
+ if response.status_code != 204:
258
+ raise ValueError(
259
+ f"Failed to delete resource: {response.status_code} {response.text}"
260
+ )
t402/mcp/constants.py CHANGED
@@ -51,8 +51,8 @@ DEFAULT_RPC_URLS: dict[SupportedNetwork, str] = {
51
51
  "optimism": "https://mainnet.optimism.io",
52
52
  "polygon": "https://polygon-rpc.com",
53
53
  "avalanche": "https://api.avax.network/ext/bc/C/rpc",
54
- "ink": "https://rpc-qnd.ink.xyz",
55
- "berachain": "https://artio.rpc.berachain.com",
54
+ "ink": "https://rpc-gel.inkonchain.com",
55
+ "berachain": "https://rpc.berachain.com",
56
56
  "unichain": "https://mainnet.unichain.org",
57
57
  }
58
58
 
@@ -64,9 +64,6 @@ USDC_ADDRESSES: dict[SupportedNetwork, str] = {
64
64
  "optimism": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
65
65
  "polygon": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
66
66
  "avalanche": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
67
- "ink": "0x0200C29006150606B650577BBE7B6248F58470c1",
68
- "berachain": "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
69
- "unichain": "0x588ce4F028D8e7B53B687865d6A67b3A54C75518",
70
67
  }
71
68
 
72
69
  # USDT contract addresses by network
@@ -84,7 +81,7 @@ USDT0_ADDRESSES: dict[SupportedNetwork, str] = {
84
81
  "arbitrum": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
85
82
  "ink": "0x0200C29006150606B650577BBE7B6248F58470c1",
86
83
  "berachain": "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
87
- "unichain": "0x588ce4F028D8e7B53B687865d6A67b3A54C75518",
84
+ "unichain": "0x9151434b16b9763660705744891fA906F660EcC5",
88
85
  }
89
86
 
90
87
  # Networks that support USDT0 bridging via LayerZero