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.
- t402/__init__.py +2 -1
- t402/a2a/__init__.py +73 -0
- t402/a2a/helpers.py +158 -0
- t402/a2a/types.py +145 -0
- t402/bridge/client.py +13 -5
- t402/bridge/constants.py +4 -2
- t402/bridge/router.py +1 -1
- t402/bridge/scan.py +3 -1
- t402/chains.py +268 -1
- t402/cli.py +31 -9
- t402/common.py +2 -0
- t402/cosmos_paywall_template.py +2 -0
- t402/django/__init__.py +42 -0
- t402/django/middleware.py +596 -0
- t402/encoding.py +9 -3
- t402/erc4337/accounts.py +56 -51
- t402/erc4337/bundlers.py +105 -99
- t402/erc4337/paymasters.py +100 -109
- t402/erc4337/types.py +39 -26
- t402/errors.py +213 -0
- t402/evm_paywall_template.py +1 -1
- t402/facilitator.py +125 -0
- t402/fastapi/middleware.py +1 -3
- t402/mcp/constants.py +3 -6
- t402/mcp/server.py +501 -84
- t402/mcp/web3_utils.py +493 -0
- t402/multisig/__init__.py +120 -0
- t402/multisig/constants.py +54 -0
- t402/multisig/safe.py +441 -0
- t402/multisig/signature.py +228 -0
- t402/multisig/transaction.py +238 -0
- t402/multisig/types.py +108 -0
- t402/multisig/utils.py +77 -0
- t402/near_paywall_template.py +2 -0
- t402/networks.py +34 -1
- t402/paywall.py +1 -3
- t402/schemes/__init__.py +143 -0
- t402/schemes/aptos/__init__.py +70 -0
- t402/schemes/aptos/constants.py +349 -0
- t402/schemes/aptos/exact_direct/__init__.py +44 -0
- t402/schemes/aptos/exact_direct/client.py +202 -0
- t402/schemes/aptos/exact_direct/facilitator.py +426 -0
- t402/schemes/aptos/exact_direct/server.py +272 -0
- t402/schemes/aptos/types.py +237 -0
- t402/schemes/cosmos/__init__.py +114 -0
- t402/schemes/cosmos/constants.py +211 -0
- t402/schemes/cosmos/exact_direct/__init__.py +21 -0
- t402/schemes/cosmos/exact_direct/client.py +198 -0
- t402/schemes/cosmos/exact_direct/facilitator.py +493 -0
- t402/schemes/cosmos/exact_direct/server.py +315 -0
- t402/schemes/cosmos/types.py +501 -0
- t402/schemes/evm/__init__.py +46 -1
- t402/schemes/evm/exact/__init__.py +11 -0
- t402/schemes/evm/exact/client.py +3 -1
- t402/schemes/evm/exact/facilitator.py +894 -0
- t402/schemes/evm/exact/server.py +1 -1
- t402/schemes/evm/exact_legacy/__init__.py +38 -0
- t402/schemes/evm/exact_legacy/client.py +291 -0
- t402/schemes/evm/exact_legacy/facilitator.py +777 -0
- t402/schemes/evm/exact_legacy/server.py +231 -0
- t402/schemes/evm/upto/__init__.py +12 -0
- t402/schemes/evm/upto/client.py +6 -2
- t402/schemes/evm/upto/facilitator.py +625 -0
- t402/schemes/evm/upto/server.py +243 -0
- t402/schemes/evm/upto/types.py +3 -1
- t402/schemes/interfaces.py +6 -2
- t402/schemes/near/__init__.py +137 -0
- t402/schemes/near/constants.py +189 -0
- t402/schemes/near/exact_direct/__init__.py +21 -0
- t402/schemes/near/exact_direct/client.py +204 -0
- t402/schemes/near/exact_direct/facilitator.py +455 -0
- t402/schemes/near/exact_direct/server.py +303 -0
- t402/schemes/near/types.py +419 -0
- t402/schemes/near/upto/__init__.py +54 -0
- t402/schemes/near/upto/types.py +272 -0
- t402/schemes/polkadot/__init__.py +72 -0
- t402/schemes/polkadot/constants.py +155 -0
- t402/schemes/polkadot/exact_direct/__init__.py +43 -0
- t402/schemes/polkadot/exact_direct/client.py +235 -0
- t402/schemes/polkadot/exact_direct/facilitator.py +428 -0
- t402/schemes/polkadot/exact_direct/server.py +292 -0
- t402/schemes/polkadot/types.py +385 -0
- t402/schemes/registry.py +6 -2
- t402/schemes/stacks/__init__.py +68 -0
- t402/schemes/stacks/constants.py +122 -0
- t402/schemes/stacks/exact_direct/__init__.py +43 -0
- t402/schemes/stacks/exact_direct/client.py +222 -0
- t402/schemes/stacks/exact_direct/facilitator.py +424 -0
- t402/schemes/stacks/exact_direct/server.py +292 -0
- t402/schemes/stacks/types.py +380 -0
- t402/schemes/svm/__init__.py +44 -0
- t402/schemes/svm/exact/__init__.py +35 -0
- t402/schemes/svm/exact/client.py +23 -0
- t402/schemes/svm/exact/facilitator.py +24 -0
- t402/schemes/svm/exact/server.py +20 -0
- t402/schemes/svm/upto/__init__.py +23 -0
- t402/schemes/svm/upto/types.py +193 -0
- t402/schemes/tezos/__init__.py +84 -0
- t402/schemes/tezos/constants.py +372 -0
- t402/schemes/tezos/exact_direct/__init__.py +22 -0
- t402/schemes/tezos/exact_direct/client.py +226 -0
- t402/schemes/tezos/exact_direct/facilitator.py +491 -0
- t402/schemes/tezos/exact_direct/server.py +277 -0
- t402/schemes/tezos/types.py +220 -0
- t402/schemes/ton/__init__.py +24 -2
- t402/schemes/ton/exact/__init__.py +7 -0
- t402/schemes/ton/exact/facilitator.py +730 -0
- t402/schemes/ton/exact/server.py +1 -1
- t402/schemes/ton/upto/__init__.py +31 -0
- t402/schemes/ton/upto/types.py +215 -0
- t402/schemes/tron/__init__.py +28 -2
- t402/schemes/tron/exact/__init__.py +9 -0
- t402/schemes/tron/exact/facilitator.py +673 -0
- t402/schemes/tron/exact/server.py +1 -1
- t402/schemes/tron/upto/__init__.py +30 -0
- t402/schemes/tron/upto/types.py +213 -0
- t402/stacks_paywall_template.py +2 -0
- t402/starlette/__init__.py +38 -0
- t402/starlette/middleware.py +522 -0
- t402/svm.py +45 -11
- t402/svm_paywall_template.py +1 -1
- t402/ton.py +6 -2
- t402/ton_paywall_template.py +1 -192
- t402/tron.py +2 -0
- t402/tron_paywall_template.py +2 -0
- t402/types.py +103 -3
- t402/wdk/chains.py +1 -1
- t402/wdk/errors.py +15 -5
- t402/wdk/signer.py +11 -2
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/METADATA +42 -1
- t402-1.10.0.dist-info/RECORD +156 -0
- t402-1.9.0.dist-info/RECORD +0 -72
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/WHEEL +0 -0
- {t402-1.9.0.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
|
+
)
|
t402/schemes/evm/__init__.py
CHANGED
|
@@ -4,20 +4,55 @@ This package provides payment scheme implementations for EVM-compatible
|
|
|
4
4
|
blockchains (Ethereum, Base, Avalanche, etc.).
|
|
5
5
|
|
|
6
6
|
Supported schemes:
|
|
7
|
-
- exact: EIP-3009 TransferWithAuthorization
|
|
7
|
+
- exact: EIP-3009 TransferWithAuthorization (recommended)
|
|
8
|
+
- exact-legacy: approve + transferFrom (DEPRECATED - see deprecation notice below)
|
|
8
9
|
- upto: EIP-2612 Permit (usage-based billing)
|
|
10
|
+
|
|
11
|
+
.. deprecated:: 2.3.0
|
|
12
|
+
The **exact-legacy** scheme is deprecated and will be removed in v3.0.0.
|
|
13
|
+
|
|
14
|
+
The exact-legacy scheme uses the traditional approve + transferFrom pattern,
|
|
15
|
+
which has several drawbacks compared to the "exact" scheme with USDT0:
|
|
16
|
+
|
|
17
|
+
1. **Two transactions required**: Users must approve then transfer
|
|
18
|
+
2. **Gas costs**: Users pay gas for both transactions
|
|
19
|
+
3. **Limited availability**: Legacy USDT only on specific chains
|
|
20
|
+
|
|
21
|
+
**Migration Guide:**
|
|
22
|
+
1. Replace `ExactLegacyEvmClientScheme` with `ExactEvmClientScheme`
|
|
23
|
+
2. Replace `ExactLegacyEvmServerScheme` with `ExactEvmServerScheme`
|
|
24
|
+
3. Use USDT0 token addresses instead of legacy USDT
|
|
25
|
+
4. See https://docs.t402.io/advanced/migration-v1-to-v2 for details
|
|
26
|
+
|
|
27
|
+
**USDT0 Advantages:**
|
|
28
|
+
- Single signature (no approve transaction)
|
|
29
|
+
- Gasless via EIP-3009
|
|
30
|
+
- Available on 19+ chains via LayerZero
|
|
31
|
+
- Cross-chain bridging support
|
|
9
32
|
"""
|
|
10
33
|
|
|
11
34
|
from t402.schemes.evm.exact import (
|
|
12
35
|
ExactEvmClientScheme,
|
|
13
36
|
ExactEvmServerScheme,
|
|
37
|
+
ExactEvmFacilitatorScheme,
|
|
38
|
+
FacilitatorEvmSigner,
|
|
39
|
+
EvmVerifyResult,
|
|
40
|
+
EvmTransactionConfirmation,
|
|
14
41
|
EvmSigner,
|
|
15
42
|
create_nonce,
|
|
16
43
|
SCHEME_EXACT,
|
|
17
44
|
)
|
|
18
45
|
|
|
46
|
+
from t402.schemes.evm.exact_legacy import (
|
|
47
|
+
ExactLegacyEvmClientScheme,
|
|
48
|
+
ExactLegacyEvmServerScheme,
|
|
49
|
+
SCHEME_EXACT_LEGACY,
|
|
50
|
+
)
|
|
51
|
+
|
|
19
52
|
from t402.schemes.evm.upto import (
|
|
20
53
|
UptoEvmClientScheme,
|
|
54
|
+
UptoEvmServerScheme,
|
|
55
|
+
UptoEvmFacilitatorScheme,
|
|
21
56
|
create_payment_nonce,
|
|
22
57
|
SCHEME_UPTO,
|
|
23
58
|
PermitSignature,
|
|
@@ -31,11 +66,21 @@ __all__ = [
|
|
|
31
66
|
# Exact scheme
|
|
32
67
|
"ExactEvmClientScheme",
|
|
33
68
|
"ExactEvmServerScheme",
|
|
69
|
+
"ExactEvmFacilitatorScheme",
|
|
70
|
+
"FacilitatorEvmSigner",
|
|
71
|
+
"EvmVerifyResult",
|
|
72
|
+
"EvmTransactionConfirmation",
|
|
34
73
|
"EvmSigner",
|
|
35
74
|
"create_nonce",
|
|
36
75
|
"SCHEME_EXACT",
|
|
76
|
+
# Exact-Legacy scheme
|
|
77
|
+
"ExactLegacyEvmClientScheme",
|
|
78
|
+
"ExactLegacyEvmServerScheme",
|
|
79
|
+
"SCHEME_EXACT_LEGACY",
|
|
37
80
|
# Upto scheme
|
|
38
81
|
"UptoEvmClientScheme",
|
|
82
|
+
"UptoEvmServerScheme",
|
|
83
|
+
"UptoEvmFacilitatorScheme",
|
|
39
84
|
"create_payment_nonce",
|
|
40
85
|
"SCHEME_UPTO",
|
|
41
86
|
"PermitSignature",
|
|
@@ -16,6 +16,12 @@ from t402.schemes.evm.exact.client import (
|
|
|
16
16
|
from t402.schemes.evm.exact.server import (
|
|
17
17
|
ExactEvmServerScheme,
|
|
18
18
|
)
|
|
19
|
+
from t402.schemes.evm.exact.facilitator import (
|
|
20
|
+
ExactEvmFacilitatorScheme,
|
|
21
|
+
FacilitatorEvmSigner,
|
|
22
|
+
EvmVerifyResult,
|
|
23
|
+
EvmTransactionConfirmation,
|
|
24
|
+
)
|
|
19
25
|
|
|
20
26
|
__all__ = [
|
|
21
27
|
# Client
|
|
@@ -24,6 +30,11 @@ __all__ = [
|
|
|
24
30
|
"create_nonce",
|
|
25
31
|
# Server
|
|
26
32
|
"ExactEvmServerScheme",
|
|
33
|
+
# Facilitator
|
|
34
|
+
"ExactEvmFacilitatorScheme",
|
|
35
|
+
"FacilitatorEvmSigner",
|
|
36
|
+
"EvmVerifyResult",
|
|
37
|
+
"EvmTransactionConfirmation",
|
|
27
38
|
# Constants
|
|
28
39
|
"SCHEME_EXACT",
|
|
29
40
|
]
|
t402/schemes/evm/exact/client.py
CHANGED
|
@@ -123,7 +123,9 @@ class ExactEvmClientScheme:
|
|
|
123
123
|
asset = req.get("asset", "")
|
|
124
124
|
|
|
125
125
|
# Get timeout
|
|
126
|
-
max_timeout = req.get("maxTimeoutSeconds") or req.get(
|
|
126
|
+
max_timeout = req.get("maxTimeoutSeconds") or req.get(
|
|
127
|
+
"max_timeout_seconds", 300
|
|
128
|
+
)
|
|
127
129
|
|
|
128
130
|
# Get EIP-712 domain info from extra
|
|
129
131
|
extra = req.get("extra", {})
|