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.
Files changed (100) hide show
  1. t402/__init__.py +2 -1
  2. t402/bridge/client.py +13 -5
  3. t402/bridge/constants.py +3 -1
  4. t402/bridge/router.py +1 -1
  5. t402/bridge/scan.py +3 -1
  6. t402/chains.py +268 -1
  7. t402/cli.py +31 -9
  8. t402/common.py +2 -0
  9. t402/cosmos_paywall_template.py +2 -0
  10. t402/encoding.py +9 -3
  11. t402/erc4337/accounts.py +56 -51
  12. t402/erc4337/bundlers.py +105 -99
  13. t402/erc4337/paymasters.py +100 -109
  14. t402/erc4337/types.py +39 -26
  15. t402/evm_paywall_template.py +1 -1
  16. t402/fastapi/middleware.py +1 -3
  17. t402/mcp/server.py +79 -46
  18. t402/near_paywall_template.py +2 -0
  19. t402/networks.py +34 -1
  20. t402/paywall.py +1 -3
  21. t402/schemes/__init__.py +124 -0
  22. t402/schemes/aptos/__init__.py +70 -0
  23. t402/schemes/aptos/constants.py +349 -0
  24. t402/schemes/aptos/exact_direct/__init__.py +44 -0
  25. t402/schemes/aptos/exact_direct/client.py +202 -0
  26. t402/schemes/aptos/exact_direct/facilitator.py +426 -0
  27. t402/schemes/aptos/exact_direct/server.py +272 -0
  28. t402/schemes/aptos/types.py +237 -0
  29. t402/schemes/evm/__init__.py +46 -1
  30. t402/schemes/evm/exact/__init__.py +11 -0
  31. t402/schemes/evm/exact/client.py +3 -1
  32. t402/schemes/evm/exact/facilitator.py +894 -0
  33. t402/schemes/evm/exact/server.py +1 -1
  34. t402/schemes/evm/exact_legacy/__init__.py +38 -0
  35. t402/schemes/evm/exact_legacy/client.py +291 -0
  36. t402/schemes/evm/exact_legacy/facilitator.py +777 -0
  37. t402/schemes/evm/exact_legacy/server.py +231 -0
  38. t402/schemes/evm/upto/__init__.py +12 -0
  39. t402/schemes/evm/upto/client.py +6 -2
  40. t402/schemes/evm/upto/facilitator.py +625 -0
  41. t402/schemes/evm/upto/server.py +243 -0
  42. t402/schemes/evm/upto/types.py +3 -1
  43. t402/schemes/interfaces.py +6 -2
  44. t402/schemes/near/__init__.py +112 -0
  45. t402/schemes/near/constants.py +189 -0
  46. t402/schemes/near/exact_direct/__init__.py +21 -0
  47. t402/schemes/near/exact_direct/client.py +204 -0
  48. t402/schemes/near/exact_direct/facilitator.py +455 -0
  49. t402/schemes/near/exact_direct/server.py +303 -0
  50. t402/schemes/near/types.py +419 -0
  51. t402/schemes/polkadot/__init__.py +72 -0
  52. t402/schemes/polkadot/constants.py +155 -0
  53. t402/schemes/polkadot/exact_direct/__init__.py +43 -0
  54. t402/schemes/polkadot/exact_direct/client.py +235 -0
  55. t402/schemes/polkadot/exact_direct/facilitator.py +428 -0
  56. t402/schemes/polkadot/exact_direct/server.py +292 -0
  57. t402/schemes/polkadot/types.py +385 -0
  58. t402/schemes/registry.py +6 -2
  59. t402/schemes/stacks/__init__.py +68 -0
  60. t402/schemes/stacks/constants.py +122 -0
  61. t402/schemes/stacks/exact_direct/__init__.py +43 -0
  62. t402/schemes/stacks/exact_direct/client.py +222 -0
  63. t402/schemes/stacks/exact_direct/facilitator.py +424 -0
  64. t402/schemes/stacks/exact_direct/server.py +292 -0
  65. t402/schemes/stacks/types.py +380 -0
  66. t402/schemes/svm/__init__.py +29 -0
  67. t402/schemes/svm/exact/__init__.py +35 -0
  68. t402/schemes/svm/exact/client.py +23 -0
  69. t402/schemes/svm/exact/facilitator.py +24 -0
  70. t402/schemes/svm/exact/server.py +20 -0
  71. t402/schemes/tezos/__init__.py +84 -0
  72. t402/schemes/tezos/constants.py +372 -0
  73. t402/schemes/tezos/exact_direct/__init__.py +22 -0
  74. t402/schemes/tezos/exact_direct/client.py +226 -0
  75. t402/schemes/tezos/exact_direct/facilitator.py +491 -0
  76. t402/schemes/tezos/exact_direct/server.py +277 -0
  77. t402/schemes/tezos/types.py +220 -0
  78. t402/schemes/ton/__init__.py +9 -2
  79. t402/schemes/ton/exact/__init__.py +7 -0
  80. t402/schemes/ton/exact/facilitator.py +730 -0
  81. t402/schemes/ton/exact/server.py +1 -1
  82. t402/schemes/tron/__init__.py +11 -2
  83. t402/schemes/tron/exact/__init__.py +9 -0
  84. t402/schemes/tron/exact/facilitator.py +673 -0
  85. t402/schemes/tron/exact/server.py +1 -1
  86. t402/stacks_paywall_template.py +2 -0
  87. t402/svm.py +45 -11
  88. t402/svm_paywall_template.py +1 -1
  89. t402/ton.py +5 -1
  90. t402/ton_paywall_template.py +1 -192
  91. t402/tron.py +2 -0
  92. t402/tron_paywall_template.py +2 -0
  93. t402/types.py +3 -1
  94. t402/wdk/errors.py +15 -5
  95. t402/wdk/signer.py +11 -2
  96. {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/METADATA +42 -1
  97. t402-1.9.1.dist-info/RECORD +125 -0
  98. t402-1.9.0.dist-info/RECORD +0 -72
  99. {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/WHEEL +0 -0
  100. {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
+ }