t402 1.9.0__py3-none-any.whl → 1.9.1__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/bridge/client.py +13 -5
- t402/bridge/constants.py +3 -1
- 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/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/evm_paywall_template.py +1 -1
- t402/fastapi/middleware.py +1 -3
- t402/mcp/server.py +79 -46
- t402/near_paywall_template.py +2 -0
- t402/networks.py +34 -1
- t402/paywall.py +1 -3
- t402/schemes/__init__.py +124 -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/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 +112 -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/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 +29 -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/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 +9 -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/tron/__init__.py +11 -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/stacks_paywall_template.py +2 -0
- t402/svm.py +45 -11
- t402/svm_paywall_template.py +1 -1
- t402/ton.py +5 -1
- t402/ton_paywall_template.py +1 -192
- t402/tron.py +2 -0
- t402/tron_paywall_template.py +2 -0
- t402/types.py +3 -1
- t402/wdk/errors.py +15 -5
- t402/wdk/signer.py +11 -2
- {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/METADATA +42 -1
- t402-1.9.1.dist-info/RECORD +125 -0
- t402-1.9.0.dist-info/RECORD +0 -72
- {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/WHEEL +0 -0
- {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
"""TON Exact Scheme - Facilitator Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the facilitator-side implementation of the exact payment
|
|
4
|
+
scheme for TON network using Jetton transfers.
|
|
5
|
+
|
|
6
|
+
The facilitator:
|
|
7
|
+
1. Verifies signed BOC messages by checking authorization metadata, balances,
|
|
8
|
+
seqno, and message structure
|
|
9
|
+
2. Settles payments by broadcasting the signed BOC to the TON network
|
|
10
|
+
3. Waits for transaction confirmation via seqno monitoring
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
import logging
|
|
17
|
+
from typing import Any, Dict, List, Optional, Protocol, Union, runtime_checkable
|
|
18
|
+
|
|
19
|
+
from t402.types import (
|
|
20
|
+
PaymentRequirementsV2,
|
|
21
|
+
PaymentPayloadV2,
|
|
22
|
+
VerifyResponse,
|
|
23
|
+
SettleResponse,
|
|
24
|
+
Network,
|
|
25
|
+
)
|
|
26
|
+
from t402.ton import (
|
|
27
|
+
SCHEME_EXACT,
|
|
28
|
+
MIN_VALIDITY_BUFFER,
|
|
29
|
+
validate_boc,
|
|
30
|
+
addresses_equal,
|
|
31
|
+
is_valid_network,
|
|
32
|
+
get_network_config,
|
|
33
|
+
TonVerifyMessageResult,
|
|
34
|
+
TonTransactionConfirmation,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@runtime_checkable
|
|
42
|
+
class FacilitatorTonSigner(Protocol):
|
|
43
|
+
"""Protocol for TON facilitator signer operations.
|
|
44
|
+
|
|
45
|
+
Implementations should provide address retrieval, message verification,
|
|
46
|
+
balance checking, BOC broadcasting, and transaction confirmation capabilities.
|
|
47
|
+
|
|
48
|
+
Example implementation:
|
|
49
|
+
```python
|
|
50
|
+
class MyTonFacilitatorSigner:
|
|
51
|
+
def __init__(self, client, addresses):
|
|
52
|
+
self._client = client
|
|
53
|
+
self._addresses = addresses
|
|
54
|
+
|
|
55
|
+
def get_addresses(self, network: str) -> List[str]:
|
|
56
|
+
return self._addresses.get(network, [])
|
|
57
|
+
|
|
58
|
+
async def get_jetton_balance(
|
|
59
|
+
self,
|
|
60
|
+
owner_address: str,
|
|
61
|
+
jetton_master_address: str,
|
|
62
|
+
network: str,
|
|
63
|
+
) -> str:
|
|
64
|
+
return await self._client.get_jetton_balance(
|
|
65
|
+
owner_address, jetton_master_address
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
async def verify_message(
|
|
69
|
+
self,
|
|
70
|
+
signed_boc: str,
|
|
71
|
+
expected_from: str,
|
|
72
|
+
expected_transfer: dict,
|
|
73
|
+
network: str,
|
|
74
|
+
) -> TonVerifyMessageResult:
|
|
75
|
+
# Verify BOC structure and transfer parameters
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
async def send_external_message(
|
|
79
|
+
self, signed_boc: str, network: str
|
|
80
|
+
) -> str:
|
|
81
|
+
return await self._client.send_boc(signed_boc)
|
|
82
|
+
|
|
83
|
+
async def wait_for_transaction(
|
|
84
|
+
self,
|
|
85
|
+
address: str,
|
|
86
|
+
seqno: int,
|
|
87
|
+
timeout_ms: int,
|
|
88
|
+
network: str,
|
|
89
|
+
) -> TonTransactionConfirmation:
|
|
90
|
+
# Poll for seqno increase
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
async def get_seqno(self, address: str, network: str) -> int:
|
|
94
|
+
return await self._client.get_seqno(address)
|
|
95
|
+
|
|
96
|
+
async def is_deployed(self, address: str, network: str) -> bool:
|
|
97
|
+
return await self._client.is_deployed(address)
|
|
98
|
+
```
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def get_addresses(self, network: str) -> List[str]:
|
|
102
|
+
"""Return all facilitator addresses for the given network."""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
async def get_jetton_balance(
|
|
106
|
+
self,
|
|
107
|
+
owner_address: str,
|
|
108
|
+
jetton_master_address: str,
|
|
109
|
+
network: str,
|
|
110
|
+
) -> str:
|
|
111
|
+
"""Get the Jetton balance for an owner.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
owner_address: Owner's TON address
|
|
115
|
+
jetton_master_address: Jetton master contract address
|
|
116
|
+
network: Network identifier
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Balance in smallest units as string
|
|
120
|
+
"""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
async def verify_message(
|
|
124
|
+
self,
|
|
125
|
+
signed_boc: str,
|
|
126
|
+
expected_from: str,
|
|
127
|
+
expected_transfer: Dict[str, str],
|
|
128
|
+
network: str,
|
|
129
|
+
) -> TonVerifyMessageResult:
|
|
130
|
+
"""Verify a signed BOC message structure.
|
|
131
|
+
|
|
132
|
+
Checks that the BOC contains a valid Jetton transfer message
|
|
133
|
+
with the expected parameters.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
signed_boc: Base64-encoded signed BOC
|
|
137
|
+
expected_from: Expected sender address
|
|
138
|
+
expected_transfer: Dict with jetton_amount, destination, jetton_master
|
|
139
|
+
network: Network identifier
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
TonVerifyMessageResult indicating validity
|
|
143
|
+
"""
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
async def send_external_message(
|
|
147
|
+
self,
|
|
148
|
+
signed_boc: str,
|
|
149
|
+
network: str,
|
|
150
|
+
) -> str:
|
|
151
|
+
"""Broadcast a signed external message to the TON network.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
signed_boc: Base64-encoded signed BOC
|
|
155
|
+
network: Network identifier
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Transaction hash or message hash
|
|
159
|
+
"""
|
|
160
|
+
...
|
|
161
|
+
|
|
162
|
+
async def wait_for_transaction(
|
|
163
|
+
self,
|
|
164
|
+
address: str,
|
|
165
|
+
seqno: int,
|
|
166
|
+
timeout_ms: int,
|
|
167
|
+
network: str,
|
|
168
|
+
) -> TonTransactionConfirmation:
|
|
169
|
+
"""Wait for a transaction to be confirmed by monitoring seqno.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
address: Wallet address to monitor
|
|
173
|
+
seqno: Expected new seqno (current + 1)
|
|
174
|
+
timeout_ms: Maximum wait time in milliseconds
|
|
175
|
+
network: Network identifier
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
TonTransactionConfirmation with success status and hash
|
|
179
|
+
"""
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
async def get_seqno(self, address: str, network: str) -> int:
|
|
183
|
+
"""Get the current wallet sequence number.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
address: Wallet address
|
|
187
|
+
network: Network identifier
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Current seqno as integer
|
|
191
|
+
"""
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
async def is_deployed(self, address: str, network: str) -> bool:
|
|
195
|
+
"""Check if a wallet contract is deployed on-chain.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
address: Wallet address
|
|
199
|
+
network: Network identifier
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
True if the wallet is deployed
|
|
203
|
+
"""
|
|
204
|
+
...
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ExactTonFacilitatorScheme:
|
|
208
|
+
"""Facilitator scheme for TON exact payments using Jetton transfers.
|
|
209
|
+
|
|
210
|
+
Verifies signed BOC messages containing Jetton transfer operations and
|
|
211
|
+
settles payments by broadcasting them to the TON network.
|
|
212
|
+
|
|
213
|
+
The verification process checks:
|
|
214
|
+
1. Scheme and network validity
|
|
215
|
+
2. BOC format (valid base64)
|
|
216
|
+
3. Message structure via signer verification
|
|
217
|
+
4. Authorization expiry (with 30-second buffer)
|
|
218
|
+
5. Jetton balance sufficiency
|
|
219
|
+
6. Amount >= required amount
|
|
220
|
+
7. Recipient matches payTo
|
|
221
|
+
8. Jetton master matches required asset
|
|
222
|
+
9. Seqno for replay protection
|
|
223
|
+
10. Wallet deployment status
|
|
224
|
+
|
|
225
|
+
Example:
|
|
226
|
+
```python
|
|
227
|
+
facilitator = ExactTonFacilitatorScheme(signer=my_ton_signer)
|
|
228
|
+
|
|
229
|
+
# Verify a payment
|
|
230
|
+
result = await facilitator.verify(payload, requirements)
|
|
231
|
+
if result.is_valid:
|
|
232
|
+
# Settle the payment
|
|
233
|
+
settlement = await facilitator.settle(payload, requirements)
|
|
234
|
+
```
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
scheme = SCHEME_EXACT
|
|
238
|
+
caip_family = "ton:*"
|
|
239
|
+
|
|
240
|
+
def __init__(self, signer: FacilitatorTonSigner):
|
|
241
|
+
"""Initialize the TON facilitator scheme.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
signer: TON facilitator signer for message verification,
|
|
245
|
+
balance checking, and transaction broadcasting.
|
|
246
|
+
"""
|
|
247
|
+
self._signer = signer
|
|
248
|
+
|
|
249
|
+
def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
|
|
250
|
+
"""Get mechanism-specific extra data for supported kinds.
|
|
251
|
+
|
|
252
|
+
Returns asset metadata (default asset address, symbol, decimals)
|
|
253
|
+
for the specified TON network.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
network: The network identifier (e.g., "ton:mainnet")
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Dict with asset metadata if network is supported, else None
|
|
260
|
+
"""
|
|
261
|
+
config = get_network_config(network)
|
|
262
|
+
if not config:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
default_asset = config["default_asset"]
|
|
266
|
+
return {
|
|
267
|
+
"defaultAsset": default_asset["master_address"],
|
|
268
|
+
"symbol": default_asset["symbol"],
|
|
269
|
+
"decimals": default_asset["decimals"],
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
def get_signers(self, network: Network) -> List[str]:
|
|
273
|
+
"""Get signer addresses for this facilitator on the given network.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
network: The network identifier
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
List of facilitator wallet addresses
|
|
280
|
+
"""
|
|
281
|
+
return self._signer.get_addresses(network)
|
|
282
|
+
|
|
283
|
+
async def verify(
|
|
284
|
+
self,
|
|
285
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
286
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
287
|
+
) -> VerifyResponse:
|
|
288
|
+
"""Verify a TON Jetton transfer payment payload.
|
|
289
|
+
|
|
290
|
+
Performs comprehensive validation of the signed BOC message including
|
|
291
|
+
authorization metadata, balance checks, and replay protection.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
payload: The payment payload containing signed BOC and authorization
|
|
295
|
+
requirements: The payment requirements to verify against
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
VerifyResponse indicating validity and payer address
|
|
299
|
+
"""
|
|
300
|
+
try:
|
|
301
|
+
# Extract data from payload and requirements
|
|
302
|
+
payload_data = self._extract_payload(payload)
|
|
303
|
+
req_data = self._extract_requirements(requirements)
|
|
304
|
+
|
|
305
|
+
network = req_data.get("network", "")
|
|
306
|
+
scheme = req_data.get("scheme", "")
|
|
307
|
+
|
|
308
|
+
# Step 1: Validate scheme
|
|
309
|
+
if scheme != SCHEME_EXACT:
|
|
310
|
+
return VerifyResponse(
|
|
311
|
+
is_valid=False,
|
|
312
|
+
invalid_reason="unsupported_scheme",
|
|
313
|
+
payer=None,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Step 2: Validate network
|
|
317
|
+
if not is_valid_network(network):
|
|
318
|
+
return VerifyResponse(
|
|
319
|
+
is_valid=False,
|
|
320
|
+
invalid_reason="unsupported_network",
|
|
321
|
+
payer=None,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Step 3: Parse TON payload
|
|
325
|
+
ton_payload = self._parse_ton_payload(payload_data)
|
|
326
|
+
if ton_payload is None:
|
|
327
|
+
return VerifyResponse(
|
|
328
|
+
is_valid=False,
|
|
329
|
+
invalid_reason="invalid_payload",
|
|
330
|
+
payer=None,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
authorization = ton_payload["authorization"]
|
|
334
|
+
signed_boc = ton_payload["signed_boc"]
|
|
335
|
+
payer = authorization["from"]
|
|
336
|
+
|
|
337
|
+
# Step 4: Validate BOC format
|
|
338
|
+
if not validate_boc(signed_boc):
|
|
339
|
+
return VerifyResponse(
|
|
340
|
+
is_valid=False,
|
|
341
|
+
invalid_reason="invalid_boc_format",
|
|
342
|
+
payer=payer,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Step 5: Verify message structure via signer
|
|
346
|
+
pay_to = req_data.get("payTo", "")
|
|
347
|
+
asset = req_data.get("asset", "")
|
|
348
|
+
|
|
349
|
+
expected_transfer = {
|
|
350
|
+
"jetton_amount": authorization["jetton_amount"],
|
|
351
|
+
"destination": pay_to,
|
|
352
|
+
"jetton_master": asset,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
verify_result = await self._signer.verify_message(
|
|
356
|
+
signed_boc=signed_boc,
|
|
357
|
+
expected_from=payer,
|
|
358
|
+
expected_transfer=expected_transfer,
|
|
359
|
+
network=network,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if not verify_result.valid:
|
|
363
|
+
reason = verify_result.reason or "unknown"
|
|
364
|
+
return VerifyResponse(
|
|
365
|
+
is_valid=False,
|
|
366
|
+
invalid_reason=f"message_verification_failed: {reason}",
|
|
367
|
+
payer=payer,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Step 6: Check authorization expiry (with buffer)
|
|
371
|
+
now = int(time.time())
|
|
372
|
+
valid_until = authorization["valid_until"]
|
|
373
|
+
if valid_until < now + MIN_VALIDITY_BUFFER:
|
|
374
|
+
return VerifyResponse(
|
|
375
|
+
is_valid=False,
|
|
376
|
+
invalid_reason="authorization_expired",
|
|
377
|
+
payer=payer,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Step 7: Verify Jetton balance
|
|
381
|
+
try:
|
|
382
|
+
balance_str = await self._signer.get_jetton_balance(
|
|
383
|
+
owner_address=payer,
|
|
384
|
+
jetton_master_address=asset,
|
|
385
|
+
network=network,
|
|
386
|
+
)
|
|
387
|
+
balance = int(balance_str)
|
|
388
|
+
except (ValueError, TypeError) as e:
|
|
389
|
+
logger.error(f"Balance check failed: {e}")
|
|
390
|
+
return VerifyResponse(
|
|
391
|
+
is_valid=False,
|
|
392
|
+
invalid_reason="balance_check_failed",
|
|
393
|
+
payer=payer,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
required_amount_str = req_data.get("amount", "0")
|
|
397
|
+
try:
|
|
398
|
+
required_amount = int(required_amount_str)
|
|
399
|
+
except (ValueError, TypeError):
|
|
400
|
+
return VerifyResponse(
|
|
401
|
+
is_valid=False,
|
|
402
|
+
invalid_reason="invalid_required_amount",
|
|
403
|
+
payer=payer,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if balance < required_amount:
|
|
407
|
+
return VerifyResponse(
|
|
408
|
+
is_valid=False,
|
|
409
|
+
invalid_reason="insufficient_jetton_balance",
|
|
410
|
+
payer=payer,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Step 8: Verify amount sufficiency
|
|
414
|
+
try:
|
|
415
|
+
payload_amount = int(authorization["jetton_amount"])
|
|
416
|
+
except (ValueError, TypeError):
|
|
417
|
+
return VerifyResponse(
|
|
418
|
+
is_valid=False,
|
|
419
|
+
invalid_reason="invalid_payload_amount",
|
|
420
|
+
payer=payer,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if payload_amount < required_amount:
|
|
424
|
+
return VerifyResponse(
|
|
425
|
+
is_valid=False,
|
|
426
|
+
invalid_reason="insufficient_amount",
|
|
427
|
+
payer=payer,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Step 9: Verify recipient matching
|
|
431
|
+
auth_to = authorization.get("to", "")
|
|
432
|
+
if not addresses_equal(auth_to, pay_to):
|
|
433
|
+
return VerifyResponse(
|
|
434
|
+
is_valid=False,
|
|
435
|
+
invalid_reason="recipient_mismatch",
|
|
436
|
+
payer=payer,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Step 10: Verify Jetton master matching
|
|
440
|
+
auth_jetton_master = authorization.get("jetton_master", "")
|
|
441
|
+
if not addresses_equal(auth_jetton_master, asset):
|
|
442
|
+
return VerifyResponse(
|
|
443
|
+
is_valid=False,
|
|
444
|
+
invalid_reason="asset_mismatch",
|
|
445
|
+
payer=payer,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Step 11: Verify seqno (replay protection)
|
|
449
|
+
try:
|
|
450
|
+
current_seqno = await self._signer.get_seqno(payer, network)
|
|
451
|
+
except Exception as e:
|
|
452
|
+
logger.error(f"Seqno check failed: {e}")
|
|
453
|
+
return VerifyResponse(
|
|
454
|
+
is_valid=False,
|
|
455
|
+
invalid_reason="seqno_check_failed",
|
|
456
|
+
payer=payer,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
auth_seqno = authorization.get("seqno", -1)
|
|
460
|
+
if auth_seqno < current_seqno:
|
|
461
|
+
return VerifyResponse(
|
|
462
|
+
is_valid=False,
|
|
463
|
+
invalid_reason="seqno_already_used",
|
|
464
|
+
payer=payer,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if auth_seqno > current_seqno:
|
|
468
|
+
return VerifyResponse(
|
|
469
|
+
is_valid=False,
|
|
470
|
+
invalid_reason="seqno_too_high",
|
|
471
|
+
payer=payer,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Step 12: Verify wallet is deployed
|
|
475
|
+
try:
|
|
476
|
+
deployed = await self._signer.is_deployed(payer, network)
|
|
477
|
+
except Exception as e:
|
|
478
|
+
logger.error(f"Deployment check failed: {e}")
|
|
479
|
+
return VerifyResponse(
|
|
480
|
+
is_valid=False,
|
|
481
|
+
invalid_reason="deployment_check_failed",
|
|
482
|
+
payer=payer,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if not deployed:
|
|
486
|
+
return VerifyResponse(
|
|
487
|
+
is_valid=False,
|
|
488
|
+
invalid_reason="wallet_not_deployed",
|
|
489
|
+
payer=payer,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# All checks passed
|
|
493
|
+
return VerifyResponse(
|
|
494
|
+
is_valid=True,
|
|
495
|
+
invalid_reason=None,
|
|
496
|
+
payer=payer,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.error(f"TON verification failed: {e}")
|
|
501
|
+
return VerifyResponse(
|
|
502
|
+
is_valid=False,
|
|
503
|
+
invalid_reason=f"verification_error: {str(e)}",
|
|
504
|
+
payer=None,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
async def settle(
|
|
508
|
+
self,
|
|
509
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
510
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
511
|
+
) -> SettleResponse:
|
|
512
|
+
"""Settle a TON Jetton transfer payment on-chain.
|
|
513
|
+
|
|
514
|
+
Verifies the payment first, then broadcasts the signed BOC to the TON
|
|
515
|
+
network and waits for transaction confirmation via seqno monitoring.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
payload: The verified payment payload with signed BOC
|
|
519
|
+
requirements: The payment requirements
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
SettleResponse with transaction hash and status
|
|
523
|
+
"""
|
|
524
|
+
req_data = self._extract_requirements(requirements)
|
|
525
|
+
network = req_data.get("network", "")
|
|
526
|
+
|
|
527
|
+
# Step 1: Verify the payment first
|
|
528
|
+
verify_result = await self.verify(payload, requirements)
|
|
529
|
+
|
|
530
|
+
if not verify_result.is_valid:
|
|
531
|
+
return SettleResponse(
|
|
532
|
+
success=False,
|
|
533
|
+
error_reason=verify_result.invalid_reason,
|
|
534
|
+
transaction=None,
|
|
535
|
+
network=network,
|
|
536
|
+
payer=verify_result.payer,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Step 2: Extract payload data for broadcasting
|
|
540
|
+
try:
|
|
541
|
+
payload_data = self._extract_payload(payload)
|
|
542
|
+
ton_payload = self._parse_ton_payload(payload_data)
|
|
543
|
+
|
|
544
|
+
if ton_payload is None:
|
|
545
|
+
return SettleResponse(
|
|
546
|
+
success=False,
|
|
547
|
+
error_reason="invalid_payload",
|
|
548
|
+
transaction=None,
|
|
549
|
+
network=network,
|
|
550
|
+
payer=verify_result.payer,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
authorization = ton_payload["authorization"]
|
|
554
|
+
signed_boc = ton_payload["signed_boc"]
|
|
555
|
+
payer = authorization["from"]
|
|
556
|
+
auth_seqno = authorization.get("seqno", 0)
|
|
557
|
+
|
|
558
|
+
except Exception as e:
|
|
559
|
+
logger.error(f"Payload extraction failed: {e}")
|
|
560
|
+
return SettleResponse(
|
|
561
|
+
success=False,
|
|
562
|
+
error_reason=f"invalid_payload: {str(e)}",
|
|
563
|
+
transaction=None,
|
|
564
|
+
network=network,
|
|
565
|
+
payer=verify_result.payer,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Step 3: Broadcast the signed BOC
|
|
569
|
+
try:
|
|
570
|
+
tx_hash = await self._signer.send_external_message(
|
|
571
|
+
signed_boc=signed_boc,
|
|
572
|
+
network=network,
|
|
573
|
+
)
|
|
574
|
+
except Exception as e:
|
|
575
|
+
logger.error(f"Transaction broadcast failed: {e}")
|
|
576
|
+
return SettleResponse(
|
|
577
|
+
success=False,
|
|
578
|
+
error_reason=f"transaction_failed: {str(e)}",
|
|
579
|
+
transaction=None,
|
|
580
|
+
network=network,
|
|
581
|
+
payer=payer,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# Step 4: Wait for transaction confirmation
|
|
585
|
+
try:
|
|
586
|
+
confirmation = await self._signer.wait_for_transaction(
|
|
587
|
+
address=payer,
|
|
588
|
+
seqno=auth_seqno + 1, # Wait for next seqno
|
|
589
|
+
timeout_ms=60000, # 60 seconds
|
|
590
|
+
network=network,
|
|
591
|
+
)
|
|
592
|
+
except Exception as e:
|
|
593
|
+
logger.error(f"Transaction confirmation failed: {e}")
|
|
594
|
+
return SettleResponse(
|
|
595
|
+
success=False,
|
|
596
|
+
error_reason=f"transaction_confirmation_failed: {str(e)}",
|
|
597
|
+
transaction=tx_hash,
|
|
598
|
+
network=network,
|
|
599
|
+
payer=payer,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
if not confirmation.success:
|
|
603
|
+
return SettleResponse(
|
|
604
|
+
success=False,
|
|
605
|
+
error_reason=confirmation.error or "confirmation_failed",
|
|
606
|
+
transaction=tx_hash,
|
|
607
|
+
network=network,
|
|
608
|
+
payer=payer,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
# Use the confirmed transaction hash if available
|
|
612
|
+
final_tx_hash = confirmation.hash if confirmation.hash else tx_hash
|
|
613
|
+
|
|
614
|
+
return SettleResponse(
|
|
615
|
+
success=True,
|
|
616
|
+
error_reason=None,
|
|
617
|
+
transaction=final_tx_hash,
|
|
618
|
+
network=network,
|
|
619
|
+
payer=payer,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
def _extract_payload(
|
|
623
|
+
self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
|
|
624
|
+
) -> Dict[str, Any]:
|
|
625
|
+
"""Extract payload data as a dict.
|
|
626
|
+
|
|
627
|
+
Handles both PaymentPayloadV2 models (where the inner payload is
|
|
628
|
+
in the 'payload' field) and plain dicts.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
payload: Payment payload (model or dict)
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Dict containing signed BOC and authorization data
|
|
635
|
+
"""
|
|
636
|
+
if hasattr(payload, "model_dump"):
|
|
637
|
+
data = payload.model_dump(by_alias=True)
|
|
638
|
+
return data.get("payload", data)
|
|
639
|
+
elif isinstance(payload, dict):
|
|
640
|
+
return payload.get("payload", payload)
|
|
641
|
+
return dict(payload)
|
|
642
|
+
|
|
643
|
+
def _extract_requirements(
|
|
644
|
+
self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
|
|
645
|
+
) -> Dict[str, Any]:
|
|
646
|
+
"""Extract requirements data as a dict.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
requirements: Payment requirements (model or dict)
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
Dict containing requirement fields
|
|
653
|
+
"""
|
|
654
|
+
if hasattr(requirements, "model_dump"):
|
|
655
|
+
return requirements.model_dump(by_alias=True)
|
|
656
|
+
return dict(requirements)
|
|
657
|
+
|
|
658
|
+
def _parse_ton_payload(
|
|
659
|
+
self, payload_data: Dict[str, Any]
|
|
660
|
+
) -> Optional[Dict[str, Any]]:
|
|
661
|
+
"""Parse and validate TON-specific payload fields.
|
|
662
|
+
|
|
663
|
+
Extracts signedBoc and authorization from the payload data,
|
|
664
|
+
normalizing field names for internal use.
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
payload_data: Raw payload dict
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
Normalized dict with signed_boc and authorization fields,
|
|
671
|
+
or None if required fields are missing.
|
|
672
|
+
"""
|
|
673
|
+
signed_boc = payload_data.get("signedBoc") or payload_data.get("signed_boc")
|
|
674
|
+
if not signed_boc:
|
|
675
|
+
return None
|
|
676
|
+
|
|
677
|
+
auth_data = payload_data.get("authorization")
|
|
678
|
+
if not auth_data:
|
|
679
|
+
return None
|
|
680
|
+
|
|
681
|
+
# Normalize authorization fields (handle both camelCase and snake_case)
|
|
682
|
+
from_addr = (
|
|
683
|
+
auth_data.get("from")
|
|
684
|
+
or auth_data.get("from_")
|
|
685
|
+
or ""
|
|
686
|
+
)
|
|
687
|
+
to_addr = auth_data.get("to", "")
|
|
688
|
+
jetton_master = (
|
|
689
|
+
auth_data.get("jettonMaster")
|
|
690
|
+
or auth_data.get("jetton_master")
|
|
691
|
+
or ""
|
|
692
|
+
)
|
|
693
|
+
jetton_amount = (
|
|
694
|
+
auth_data.get("jettonAmount")
|
|
695
|
+
or auth_data.get("jetton_amount")
|
|
696
|
+
or "0"
|
|
697
|
+
)
|
|
698
|
+
ton_amount = (
|
|
699
|
+
auth_data.get("tonAmount")
|
|
700
|
+
or auth_data.get("ton_amount")
|
|
701
|
+
or "0"
|
|
702
|
+
)
|
|
703
|
+
valid_until = (
|
|
704
|
+
auth_data.get("validUntil")
|
|
705
|
+
or auth_data.get("valid_until")
|
|
706
|
+
or 0
|
|
707
|
+
)
|
|
708
|
+
seqno = auth_data.get("seqno", 0)
|
|
709
|
+
query_id = (
|
|
710
|
+
auth_data.get("queryId")
|
|
711
|
+
or auth_data.get("query_id")
|
|
712
|
+
or ""
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
if not from_addr:
|
|
716
|
+
return None
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
"signed_boc": signed_boc,
|
|
720
|
+
"authorization": {
|
|
721
|
+
"from": from_addr,
|
|
722
|
+
"to": to_addr,
|
|
723
|
+
"jetton_master": jetton_master,
|
|
724
|
+
"jetton_amount": str(jetton_amount),
|
|
725
|
+
"ton_amount": str(ton_amount),
|
|
726
|
+
"valid_until": int(valid_until),
|
|
727
|
+
"seqno": int(seqno),
|
|
728
|
+
"query_id": str(query_id),
|
|
729
|
+
},
|
|
730
|
+
}
|