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,372 @@
|
|
|
1
|
+
"""Tezos Blockchain Constants for T402 Protocol.
|
|
2
|
+
|
|
3
|
+
This module defines constants, token information, and network configurations
|
|
4
|
+
for the Tezos blockchain mechanism in the T402 protocol.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Scheme identifier
|
|
13
|
+
SCHEME_EXACT_DIRECT = "exact-direct"
|
|
14
|
+
|
|
15
|
+
# CAIP-2 network identifiers (derived from genesis block hash prefix)
|
|
16
|
+
TEZOS_MAINNET = "tezos:NetXdQprcVkpaWU"
|
|
17
|
+
TEZOS_GHOSTNET = "tezos:NetXnHfVqm9iesp"
|
|
18
|
+
|
|
19
|
+
# RPC endpoints
|
|
20
|
+
TEZOS_MAINNET_RPC = "https://mainnet.api.tez.ie"
|
|
21
|
+
TEZOS_GHOSTNET_RPC = "https://ghostnet.tezos.marigold.dev"
|
|
22
|
+
|
|
23
|
+
# Indexer API endpoints (TzKT)
|
|
24
|
+
TEZOS_MAINNET_INDEXER = "https://api.tzkt.io"
|
|
25
|
+
TEZOS_GHOSTNET_INDEXER = "https://api.ghostnet.tzkt.io"
|
|
26
|
+
|
|
27
|
+
# FA2 token standard (TZIP-12)
|
|
28
|
+
FA2_TRANSFER_ENTRYPOINT = "transfer"
|
|
29
|
+
|
|
30
|
+
# USDt on Tezos Mainnet
|
|
31
|
+
USDT_MAINNET_CONTRACT = "KT1XnTn74bUtxHfDtBmm2bGZAQfhPbvKWR8o"
|
|
32
|
+
USDT_MAINNET_TOKEN_ID = 0
|
|
33
|
+
USDT_DECIMALS = 6
|
|
34
|
+
|
|
35
|
+
# Address length for Tezos addresses (tz1/tz2/tz3/KT1)
|
|
36
|
+
TEZOS_ADDRESS_LENGTH = 36
|
|
37
|
+
|
|
38
|
+
# Operation hash length (starts with 'o')
|
|
39
|
+
TEZOS_OP_HASH_LENGTH = 51
|
|
40
|
+
|
|
41
|
+
# Valid address prefixes
|
|
42
|
+
VALID_ADDRESS_PREFIXES = ("tz1", "tz2", "tz3", "KT1")
|
|
43
|
+
|
|
44
|
+
# Base58 character set (no 0, O, I, l)
|
|
45
|
+
BASE58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TokenInfo:
|
|
49
|
+
"""Information about a Tezos FA2 token.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
contract_address: The FA2 contract address (KT1...)
|
|
53
|
+
token_id: The token ID within the FA2 contract
|
|
54
|
+
symbol: Token symbol (e.g., "USDt")
|
|
55
|
+
name: Full token name (e.g., "Tether USD")
|
|
56
|
+
decimals: Number of decimal places
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
contract_address: str,
|
|
62
|
+
token_id: int,
|
|
63
|
+
symbol: str,
|
|
64
|
+
name: str,
|
|
65
|
+
decimals: int,
|
|
66
|
+
):
|
|
67
|
+
self.contract_address = contract_address
|
|
68
|
+
self.token_id = token_id
|
|
69
|
+
self.symbol = symbol
|
|
70
|
+
self.name = name
|
|
71
|
+
self.decimals = decimals
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class NetworkConfig:
|
|
75
|
+
"""Configuration for a Tezos network.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
name: Human-readable network name
|
|
79
|
+
rpc_url: Tezos RPC endpoint URL
|
|
80
|
+
indexer_url: TzKT indexer API URL
|
|
81
|
+
default_token: Default token for price conversion (may be None)
|
|
82
|
+
is_testnet: Whether this is a testnet
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
name: str,
|
|
88
|
+
rpc_url: str,
|
|
89
|
+
indexer_url: str,
|
|
90
|
+
default_token: Optional[TokenInfo] = None,
|
|
91
|
+
is_testnet: bool = False,
|
|
92
|
+
):
|
|
93
|
+
self.name = name
|
|
94
|
+
self.rpc_url = rpc_url
|
|
95
|
+
self.indexer_url = indexer_url
|
|
96
|
+
self.default_token = default_token
|
|
97
|
+
self.is_testnet = is_testnet
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Token definitions
|
|
101
|
+
USDT_MAINNET = TokenInfo(
|
|
102
|
+
contract_address=USDT_MAINNET_CONTRACT,
|
|
103
|
+
token_id=USDT_MAINNET_TOKEN_ID,
|
|
104
|
+
symbol="USDt",
|
|
105
|
+
name="Tether USD",
|
|
106
|
+
decimals=USDT_DECIMALS,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Network configurations
|
|
110
|
+
NETWORK_CONFIGS: Dict[str, NetworkConfig] = {
|
|
111
|
+
TEZOS_MAINNET: NetworkConfig(
|
|
112
|
+
name="Tezos Mainnet",
|
|
113
|
+
rpc_url=TEZOS_MAINNET_RPC,
|
|
114
|
+
indexer_url=TEZOS_MAINNET_INDEXER,
|
|
115
|
+
default_token=USDT_MAINNET,
|
|
116
|
+
is_testnet=False,
|
|
117
|
+
),
|
|
118
|
+
TEZOS_GHOSTNET: NetworkConfig(
|
|
119
|
+
name="Tezos Ghostnet",
|
|
120
|
+
rpc_url=TEZOS_GHOSTNET_RPC,
|
|
121
|
+
indexer_url=TEZOS_GHOSTNET_INDEXER,
|
|
122
|
+
default_token=None, # No USDT on testnet
|
|
123
|
+
is_testnet=True,
|
|
124
|
+
),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Token registry indexed by network and symbol
|
|
128
|
+
TOKEN_REGISTRY: Dict[str, Dict[str, TokenInfo]] = {
|
|
129
|
+
TEZOS_MAINNET: {
|
|
130
|
+
"USDt": USDT_MAINNET,
|
|
131
|
+
},
|
|
132
|
+
TEZOS_GHOSTNET: {},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_network_config(network: str) -> Optional[NetworkConfig]:
|
|
137
|
+
"""Get the configuration for a Tezos network.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
network: CAIP-2 network identifier (e.g., "tezos:NetXdQprcVkpaWU")
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
NetworkConfig if found, None otherwise
|
|
144
|
+
"""
|
|
145
|
+
return NETWORK_CONFIGS.get(network)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_token_info(network: str, symbol: str) -> Optional[TokenInfo]:
|
|
149
|
+
"""Get token information by network and symbol.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
network: CAIP-2 network identifier
|
|
153
|
+
symbol: Token symbol (e.g., "USDt")
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
TokenInfo if found, None otherwise
|
|
157
|
+
"""
|
|
158
|
+
tokens = TOKEN_REGISTRY.get(network, {})
|
|
159
|
+
return tokens.get(symbol)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_token_by_contract(
|
|
163
|
+
network: str, contract_address: str, token_id: int
|
|
164
|
+
) -> Optional[TokenInfo]:
|
|
165
|
+
"""Get token information by contract address and token ID.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
network: CAIP-2 network identifier
|
|
169
|
+
contract_address: FA2 contract address
|
|
170
|
+
token_id: Token ID within the contract
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
TokenInfo if found, None otherwise
|
|
174
|
+
"""
|
|
175
|
+
tokens = TOKEN_REGISTRY.get(network, {})
|
|
176
|
+
for token in tokens.values():
|
|
177
|
+
if token.contract_address == contract_address and token.token_id == token_id:
|
|
178
|
+
return token
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def is_tezos_network(network: str) -> bool:
|
|
183
|
+
"""Check if a network identifier belongs to the Tezos namespace.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
network: Network identifier string
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if the network starts with "tezos:"
|
|
190
|
+
"""
|
|
191
|
+
return network.startswith("tezos:")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def is_valid_address(address: str) -> bool:
|
|
195
|
+
"""Validate a Tezos address format.
|
|
196
|
+
|
|
197
|
+
Valid addresses:
|
|
198
|
+
- Implicit accounts: tz1, tz2, tz3 (36 characters)
|
|
199
|
+
- Contract accounts: KT1 (36 characters)
|
|
200
|
+
|
|
201
|
+
All characters must be valid Base58 characters.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
address: The address to validate
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
True if the address format is valid
|
|
208
|
+
"""
|
|
209
|
+
if not address:
|
|
210
|
+
return False
|
|
211
|
+
if not address.startswith(VALID_ADDRESS_PREFIXES):
|
|
212
|
+
return False
|
|
213
|
+
if len(address) != TEZOS_ADDRESS_LENGTH:
|
|
214
|
+
return False
|
|
215
|
+
# Check all characters are valid base58
|
|
216
|
+
for char in address:
|
|
217
|
+
if char not in BASE58_CHARS:
|
|
218
|
+
return False
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def is_valid_operation_hash(op_hash: str) -> bool:
|
|
223
|
+
"""Validate a Tezos operation hash format.
|
|
224
|
+
|
|
225
|
+
Operation hashes start with 'o' and are 51 characters of Base58.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
op_hash: The operation hash to validate
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
True if the operation hash format is valid
|
|
232
|
+
"""
|
|
233
|
+
if not op_hash:
|
|
234
|
+
return False
|
|
235
|
+
if not op_hash.startswith("o"):
|
|
236
|
+
return False
|
|
237
|
+
if len(op_hash) != TEZOS_OP_HASH_LENGTH:
|
|
238
|
+
return False
|
|
239
|
+
# Check all characters are valid base58
|
|
240
|
+
for char in op_hash:
|
|
241
|
+
if char not in BASE58_CHARS:
|
|
242
|
+
return False
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def create_asset_identifier(network: str, contract_address: str, token_id: int) -> str:
|
|
247
|
+
"""Create a CAIP-19 asset identifier for a Tezos FA2 token.
|
|
248
|
+
|
|
249
|
+
Format: tezos:{chainRef}/fa2:{contractAddress}/{tokenId}
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
network: CAIP-2 network identifier (e.g., "tezos:NetXdQprcVkpaWU")
|
|
253
|
+
contract_address: FA2 contract address (e.g., "KT1XnTn74bUtxHfDtBmm2bGZAQfhPbvKWR8o")
|
|
254
|
+
token_id: Token ID within the FA2 contract
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
CAIP-19 asset identifier string
|
|
258
|
+
"""
|
|
259
|
+
return f"{network}/fa2:{contract_address}/{token_id}"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def parse_asset_identifier(asset: str) -> Dict[str, Any]:
|
|
263
|
+
"""Parse a CAIP-19 asset identifier for Tezos FA2 tokens.
|
|
264
|
+
|
|
265
|
+
Supports two formats:
|
|
266
|
+
- CAIP-19: tezos:{chainRef}/fa2:{contractAddress}/{tokenId}
|
|
267
|
+
- Simple: {contractAddress}/{tokenId} or {contractAddress} (tokenId defaults to 0)
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
asset: The asset identifier string
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Dict with "contract_address" (str) and "token_id" (int)
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ValueError: If the asset format is unrecognized
|
|
277
|
+
"""
|
|
278
|
+
if not asset:
|
|
279
|
+
raise ValueError("Asset identifier is empty")
|
|
280
|
+
|
|
281
|
+
# Try CAIP-19 format: tezos:{chainRef}/fa2:{contract}/{tokenId}
|
|
282
|
+
if asset.startswith("tezos:"):
|
|
283
|
+
parts = asset.split("/")
|
|
284
|
+
if len(parts) == 3 and parts[1].startswith("fa2:"):
|
|
285
|
+
contract_address = parts[1][4:] # Remove "fa2:" prefix
|
|
286
|
+
try:
|
|
287
|
+
token_id = int(parts[2])
|
|
288
|
+
except ValueError:
|
|
289
|
+
raise ValueError(f"Invalid token ID in asset: {asset}")
|
|
290
|
+
if not contract_address.startswith("KT1") or len(contract_address) != 36:
|
|
291
|
+
raise ValueError(f"Invalid contract address in asset: {asset}")
|
|
292
|
+
return {"contract_address": contract_address, "token_id": token_id}
|
|
293
|
+
|
|
294
|
+
# Try simple format: KT1.../tokenId or KT1...
|
|
295
|
+
if asset.startswith("KT1"):
|
|
296
|
+
parts = asset.split("/")
|
|
297
|
+
contract_address = parts[0]
|
|
298
|
+
if len(contract_address) != 36:
|
|
299
|
+
raise ValueError(f"Invalid contract address in asset: {asset}")
|
|
300
|
+
token_id = 0
|
|
301
|
+
if len(parts) == 2:
|
|
302
|
+
try:
|
|
303
|
+
token_id = int(parts[1])
|
|
304
|
+
except ValueError:
|
|
305
|
+
raise ValueError(f"Invalid token ID in asset: {asset}")
|
|
306
|
+
elif len(parts) > 2:
|
|
307
|
+
raise ValueError(f"Unrecognized asset format: {asset}")
|
|
308
|
+
return {"contract_address": contract_address, "token_id": token_id}
|
|
309
|
+
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"Unrecognized asset format: {asset} "
|
|
312
|
+
f"(expected tezos:{{chainRef}}/fa2:{{contract}}/{{tokenId}} or KT1...)"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def decimal_to_atomic(amount: float, decimals: int) -> str:
|
|
317
|
+
"""Convert a decimal amount to atomic units string.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
amount: Decimal amount (e.g., 1.50)
|
|
321
|
+
decimals: Number of decimal places for the token
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Atomic amount as string (e.g., "1500000" for 6 decimals)
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
multiplier = 10**decimals
|
|
328
|
+
atomic = int(round(amount * multiplier))
|
|
329
|
+
return str(atomic)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def parse_decimal_to_atomic(amount: str, decimals: int) -> str:
|
|
333
|
+
"""Convert a decimal string amount to atomic units.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
amount: Decimal string (e.g., "1.50")
|
|
337
|
+
decimals: Number of decimal places for the token
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Atomic amount string (e.g., "1500000" for 6 decimals)
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
ValueError: If the amount format is invalid
|
|
344
|
+
"""
|
|
345
|
+
parts = amount.split(".")
|
|
346
|
+
|
|
347
|
+
integer_part = parts[0]
|
|
348
|
+
fractional_part = ""
|
|
349
|
+
|
|
350
|
+
if len(parts) == 2:
|
|
351
|
+
fractional_part = parts[1]
|
|
352
|
+
elif len(parts) > 2:
|
|
353
|
+
raise ValueError(f"Invalid amount format: {amount}")
|
|
354
|
+
|
|
355
|
+
# Pad or truncate fractional part to match decimals
|
|
356
|
+
if len(fractional_part) > decimals:
|
|
357
|
+
fractional_part = fractional_part[:decimals]
|
|
358
|
+
else:
|
|
359
|
+
fractional_part = fractional_part + "0" * (decimals - len(fractional_part))
|
|
360
|
+
|
|
361
|
+
# Combine and parse as integer
|
|
362
|
+
combined = integer_part + fractional_part
|
|
363
|
+
|
|
364
|
+
# Remove leading zeros but keep at least one digit
|
|
365
|
+
combined = combined.lstrip("0") or "0"
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
result = int(combined)
|
|
369
|
+
except ValueError:
|
|
370
|
+
raise ValueError(f"Failed to parse amount: {amount}")
|
|
371
|
+
|
|
372
|
+
return str(result)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Tezos Exact-Direct Payment Scheme.
|
|
2
|
+
|
|
3
|
+
This package provides the exact-direct payment scheme implementation for Tezos.
|
|
4
|
+
In this scheme, the client executes the FA2 transfer directly on-chain and
|
|
5
|
+
provides the operation hash as proof of payment. The facilitator then verifies
|
|
6
|
+
the operation on-chain.
|
|
7
|
+
|
|
8
|
+
Components:
|
|
9
|
+
- Client: Executes FA2 transfer and returns opHash
|
|
10
|
+
- Server: Parses prices and enhances requirements with CAIP-19 asset info
|
|
11
|
+
- Facilitator: Verifies operation status and transfer details on-chain
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from t402.schemes.tezos.exact_direct.client import ExactDirectTezosClient
|
|
15
|
+
from t402.schemes.tezos.exact_direct.server import ExactDirectTezosServer
|
|
16
|
+
from t402.schemes.tezos.exact_direct.facilitator import ExactDirectTezosFacilitator
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ExactDirectTezosClient",
|
|
20
|
+
"ExactDirectTezosServer",
|
|
21
|
+
"ExactDirectTezosFacilitator",
|
|
22
|
+
]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Tezos Exact-Direct Scheme - Client Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the client-side implementation of the exact-direct payment
|
|
4
|
+
scheme for Tezos using FA2 token transfers.
|
|
5
|
+
|
|
6
|
+
In the exact-direct scheme, the client directly executes the FA2 transfer on-chain
|
|
7
|
+
and provides the operation hash as proof of payment. This differs from off-chain
|
|
8
|
+
authorization schemes where the facilitator executes the transfer.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, Dict, Union
|
|
15
|
+
|
|
16
|
+
from t402.types import (
|
|
17
|
+
PaymentRequirementsV2,
|
|
18
|
+
T402_VERSION_V1,
|
|
19
|
+
T402_VERSION_V2,
|
|
20
|
+
)
|
|
21
|
+
from t402.schemes.tezos.constants import (
|
|
22
|
+
SCHEME_EXACT_DIRECT,
|
|
23
|
+
is_tezos_network,
|
|
24
|
+
is_valid_address,
|
|
25
|
+
parse_asset_identifier,
|
|
26
|
+
)
|
|
27
|
+
from t402.schemes.tezos.types import (
|
|
28
|
+
ClientTezosSigner,
|
|
29
|
+
ExactDirectPayload,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExactDirectTezosClient:
|
|
37
|
+
"""Client scheme for Tezos exact-direct payments using FA2 transfers.
|
|
38
|
+
|
|
39
|
+
This scheme executes FA2 token transfers directly on-chain and provides
|
|
40
|
+
the operation hash as proof of payment. The facilitator verifies the
|
|
41
|
+
operation status and transfer details.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
```python
|
|
45
|
+
from t402.schemes.tezos import ExactDirectTezosClient
|
|
46
|
+
|
|
47
|
+
class MyTezosSigner:
|
|
48
|
+
def address(self) -> str:
|
|
49
|
+
return "tz1..."
|
|
50
|
+
|
|
51
|
+
async def transfer_fa2(
|
|
52
|
+
self, contract, token_id, to, amount, network
|
|
53
|
+
) -> str:
|
|
54
|
+
# Execute FA2 transfer
|
|
55
|
+
return "oo7bHf..." # operation hash
|
|
56
|
+
|
|
57
|
+
signer = MyTezosSigner()
|
|
58
|
+
client = ExactDirectTezosClient(signer=signer)
|
|
59
|
+
|
|
60
|
+
payload = await client.create_payment_payload(
|
|
61
|
+
t402_version=2,
|
|
62
|
+
requirements={
|
|
63
|
+
"scheme": "exact-direct",
|
|
64
|
+
"network": "tezos:NetXdQprcVkpaWU",
|
|
65
|
+
"asset": "tezos:NetXdQprcVkpaWU/fa2:KT1XnTn74bUtxHfDtBmm2bGZAQfhPbvKWR8o/0",
|
|
66
|
+
"amount": "1000000",
|
|
67
|
+
"payTo": "tz1...",
|
|
68
|
+
"maxTimeoutSeconds": 300,
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
scheme = SCHEME_EXACT_DIRECT
|
|
75
|
+
caip_family = "tezos:*"
|
|
76
|
+
|
|
77
|
+
def __init__(self, signer: ClientTezosSigner):
|
|
78
|
+
"""Initialize the Tezos exact-direct client.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
signer: A Tezos signer implementing the ClientTezosSigner protocol.
|
|
82
|
+
Must provide address() and transfer_fa2() methods.
|
|
83
|
+
"""
|
|
84
|
+
self._signer = signer
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def address(self) -> str:
|
|
88
|
+
"""Get the signer's Tezos address."""
|
|
89
|
+
return self._signer.address()
|
|
90
|
+
|
|
91
|
+
async def create_payment_payload(
|
|
92
|
+
self,
|
|
93
|
+
t402_version: int,
|
|
94
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
95
|
+
) -> Dict[str, Any]:
|
|
96
|
+
"""Execute FA2 transfer and create payment payload with operation hash.
|
|
97
|
+
|
|
98
|
+
This method:
|
|
99
|
+
1. Validates the payment requirements
|
|
100
|
+
2. Parses the CAIP-19 asset identifier
|
|
101
|
+
3. Executes the FA2 transfer on-chain via the signer
|
|
102
|
+
4. Returns a payload containing the operation hash as proof
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
t402_version: Protocol version (1 or 2)
|
|
106
|
+
requirements: Payment requirements specifying amount, network, asset, payTo
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Payment payload dict containing the operation hash and transfer metadata
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValueError: If requirements are invalid (wrong scheme, network, address, etc.)
|
|
113
|
+
Exception: If the FA2 transfer fails
|
|
114
|
+
"""
|
|
115
|
+
# Convert to dict for easier access
|
|
116
|
+
if hasattr(requirements, "model_dump"):
|
|
117
|
+
req = requirements.model_dump(by_alias=True)
|
|
118
|
+
else:
|
|
119
|
+
req = dict(requirements)
|
|
120
|
+
|
|
121
|
+
# Validate requirements
|
|
122
|
+
self._validate_requirements(req)
|
|
123
|
+
|
|
124
|
+
# Extract fields
|
|
125
|
+
network = req.get("network", "")
|
|
126
|
+
asset = req.get("asset", "")
|
|
127
|
+
amount = req.get("amount", "0")
|
|
128
|
+
pay_to = req.get("payTo") or req.get("pay_to", "")
|
|
129
|
+
|
|
130
|
+
# Parse asset to get contract address and token ID
|
|
131
|
+
asset_info = parse_asset_identifier(asset)
|
|
132
|
+
contract_address = asset_info["contract_address"]
|
|
133
|
+
token_id = asset_info["token_id"]
|
|
134
|
+
|
|
135
|
+
# Parse amount as integer
|
|
136
|
+
amount_int = int(amount)
|
|
137
|
+
|
|
138
|
+
# Execute FA2 transfer on-chain
|
|
139
|
+
logger.debug(
|
|
140
|
+
"Executing FA2 transfer: contract=%s, token_id=%d, to=%s, amount=%d",
|
|
141
|
+
contract_address,
|
|
142
|
+
token_id,
|
|
143
|
+
pay_to,
|
|
144
|
+
amount_int,
|
|
145
|
+
)
|
|
146
|
+
op_hash = await self._signer.transfer_fa2(
|
|
147
|
+
contract=contract_address,
|
|
148
|
+
token_id=token_id,
|
|
149
|
+
to=pay_to,
|
|
150
|
+
amount=amount_int,
|
|
151
|
+
network=network,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Build the payload
|
|
155
|
+
payload_data = ExactDirectPayload(
|
|
156
|
+
op_hash=op_hash,
|
|
157
|
+
from_=self._signer.address(),
|
|
158
|
+
to=pay_to,
|
|
159
|
+
amount=amount,
|
|
160
|
+
contract_address=contract_address,
|
|
161
|
+
token_id=token_id,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if t402_version == T402_VERSION_V1:
|
|
165
|
+
return {
|
|
166
|
+
"t402Version": T402_VERSION_V1,
|
|
167
|
+
"scheme": self.scheme,
|
|
168
|
+
"network": network,
|
|
169
|
+
"payload": payload_data.to_map(),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# V2 format
|
|
173
|
+
return {
|
|
174
|
+
"t402Version": T402_VERSION_V2,
|
|
175
|
+
"payload": payload_data.to_map(),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def _validate_requirements(self, req: Dict[str, Any]) -> None:
|
|
179
|
+
"""Validate payment requirements for the exact-direct scheme.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
req: Requirements dict
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
ValueError: If any validation check fails
|
|
186
|
+
"""
|
|
187
|
+
# Check scheme
|
|
188
|
+
scheme = req.get("scheme", "")
|
|
189
|
+
if scheme and scheme != SCHEME_EXACT_DIRECT:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Invalid scheme: expected {SCHEME_EXACT_DIRECT}, got {scheme}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Check network is Tezos
|
|
195
|
+
network = req.get("network", "")
|
|
196
|
+
if not is_tezos_network(network):
|
|
197
|
+
raise ValueError(f"Invalid network: {network} (expected tezos:*)")
|
|
198
|
+
|
|
199
|
+
# Check payTo address
|
|
200
|
+
pay_to = req.get("payTo") or req.get("pay_to", "")
|
|
201
|
+
if not pay_to:
|
|
202
|
+
raise ValueError("PayTo address is required")
|
|
203
|
+
if not is_valid_address(pay_to):
|
|
204
|
+
raise ValueError(f"Invalid payTo address: {pay_to}")
|
|
205
|
+
|
|
206
|
+
# Check amount
|
|
207
|
+
amount = req.get("amount", "")
|
|
208
|
+
if not amount:
|
|
209
|
+
raise ValueError("Amount is required")
|
|
210
|
+
try:
|
|
211
|
+
amount_int = int(amount)
|
|
212
|
+
if amount_int <= 0:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"Invalid amount: {amount} (must be a positive integer)"
|
|
215
|
+
)
|
|
216
|
+
except ValueError:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"Invalid amount: {amount} (must be a positive integer string)"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Check asset
|
|
222
|
+
asset = req.get("asset", "")
|
|
223
|
+
if not asset:
|
|
224
|
+
raise ValueError("Asset is required")
|
|
225
|
+
# This will raise ValueError if invalid
|
|
226
|
+
parse_asset_identifier(asset)
|