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,673 @@
1
+ """TRON Exact Scheme - Facilitator Implementation.
2
+
3
+ This module provides the facilitator-side implementation of the exact payment
4
+ scheme for TRON network using TRC-20 token transfers.
5
+
6
+ The facilitator:
7
+ 1. Verifies signed TRC-20 transactions against payment requirements
8
+ 2. Validates sender/recipient addresses, amount, and expiration
9
+ 3. Checks on-chain balance and account activation status
10
+ 4. Settles payments by broadcasting the signed transaction to the TRON network
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import time
17
+ from typing import Any, Dict, List, Optional, Protocol, Union
18
+
19
+ from t402.types import (
20
+ PaymentRequirementsV2,
21
+ PaymentPayloadV2,
22
+ VerifyResponse,
23
+ SettleResponse,
24
+ Network,
25
+ )
26
+ from t402.tron import (
27
+ SCHEME_EXACT,
28
+ MIN_VALIDITY_BUFFER,
29
+ TronPaymentPayload,
30
+ TronVerifyResult,
31
+ TronTransactionConfirmation,
32
+ validate_tron_address,
33
+ addresses_equal,
34
+ is_valid_network,
35
+ get_network_config,
36
+ )
37
+
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ # Default timeout for transaction confirmation (milliseconds)
42
+ DEFAULT_CONFIRMATION_TIMEOUT = 60000
43
+
44
+
45
+ class FacilitatorTronSigner(Protocol):
46
+ """Protocol for TRON facilitator signing and verification operations.
47
+
48
+ Implementations must provide address retrieval, transaction verification,
49
+ broadcasting, and balance-checking capabilities.
50
+
51
+ Example implementation:
52
+ ```python
53
+ class MyTronFacilitatorSigner:
54
+ def __init__(self, client, private_key):
55
+ self._client = client
56
+ self._private_key = private_key
57
+ self._address = derive_address(private_key)
58
+
59
+ def get_addresses(self, network: str) -> List[str]:
60
+ return [self._address]
61
+
62
+ async def verify_transaction(
63
+ self,
64
+ signed_transaction: str,
65
+ expected_from: str,
66
+ expected_to: str,
67
+ expected_contract: str,
68
+ expected_amount: str,
69
+ network: str,
70
+ ) -> TronVerifyResult:
71
+ # Decode and verify the signed transaction
72
+ ...
73
+
74
+ async def broadcast_transaction(
75
+ self, signed_transaction: str, network: str
76
+ ) -> str:
77
+ # Submit to TRON network, return tx ID
78
+ ...
79
+
80
+ async def wait_for_transaction(
81
+ self, tx_id: str, network: str, timeout: int
82
+ ) -> TronTransactionConfirmation:
83
+ # Poll for confirmation
84
+ ...
85
+
86
+ async def get_balance(
87
+ self, owner_address: str, contract_address: str, network: str
88
+ ) -> str:
89
+ # Query TRC-20 balance
90
+ ...
91
+
92
+ async def is_activated(self, address: str, network: str) -> bool:
93
+ # Check if account is activated on TRON
94
+ ...
95
+ ```
96
+ """
97
+
98
+ def get_addresses(self, network: str) -> List[str]:
99
+ """Get facilitator addresses for the given network.
100
+
101
+ Args:
102
+ network: Network identifier (e.g., "tron:mainnet")
103
+
104
+ Returns:
105
+ List of T-prefix base58check addresses
106
+ """
107
+ ...
108
+
109
+ async def verify_transaction(
110
+ self,
111
+ signed_transaction: str,
112
+ expected_from: str,
113
+ expected_to: str,
114
+ expected_contract: str,
115
+ expected_amount: str,
116
+ network: str,
117
+ ) -> TronVerifyResult:
118
+ """Verify a signed TRC-20 transfer transaction.
119
+
120
+ Decodes the signed transaction and validates that:
121
+ - The signature is valid (ECDSA secp256k1 recovery)
122
+ - The recovered signer matches expected_from
123
+ - The transfer recipient matches expected_to
124
+ - The contract address matches expected_contract
125
+ - The transfer amount matches expected_amount
126
+
127
+ Args:
128
+ signed_transaction: Hex-encoded signed transaction
129
+ expected_from: Expected sender address
130
+ expected_to: Expected recipient address
131
+ expected_contract: Expected TRC-20 contract address
132
+ expected_amount: Expected transfer amount in atomic units
133
+ network: Network identifier
134
+
135
+ Returns:
136
+ TronVerifyResult with validity status and optional reason
137
+ """
138
+ ...
139
+
140
+ async def broadcast_transaction(
141
+ self, signed_transaction: str, network: str
142
+ ) -> str:
143
+ """Broadcast a signed transaction to the TRON network.
144
+
145
+ Args:
146
+ signed_transaction: Hex-encoded signed transaction
147
+ network: Network identifier
148
+
149
+ Returns:
150
+ Transaction ID (hex string)
151
+
152
+ Raises:
153
+ Exception: If broadcast fails
154
+ """
155
+ ...
156
+
157
+ async def wait_for_transaction(
158
+ self, tx_id: str, network: str, timeout: int
159
+ ) -> TronTransactionConfirmation:
160
+ """Wait for a transaction to be confirmed on-chain.
161
+
162
+ Args:
163
+ tx_id: Transaction ID to wait for
164
+ network: Network identifier
165
+ timeout: Maximum wait time in milliseconds
166
+
167
+ Returns:
168
+ TronTransactionConfirmation with success status
169
+ """
170
+ ...
171
+
172
+ async def get_balance(
173
+ self, owner_address: str, contract_address: str, network: str
174
+ ) -> str:
175
+ """Get TRC-20 token balance for an address.
176
+
177
+ Args:
178
+ owner_address: Account address to check
179
+ contract_address: TRC-20 contract address
180
+ network: Network identifier
181
+
182
+ Returns:
183
+ Balance in atomic units as string
184
+ """
185
+ ...
186
+
187
+ async def is_activated(self, address: str, network: str) -> bool:
188
+ """Check if a TRON account is activated (has on-chain state).
189
+
190
+ New TRON accounts require activation before they can receive
191
+ TRC-20 tokens. This method verifies the account exists on-chain.
192
+
193
+ Args:
194
+ address: TRON address to check
195
+ network: Network identifier
196
+
197
+ Returns:
198
+ True if account is activated, False otherwise
199
+ """
200
+ ...
201
+
202
+
203
+ class ExactTronFacilitatorScheme:
204
+ """Facilitator scheme for TRON exact payments using TRC-20 transfers.
205
+
206
+ Verifies signed TRC-20 transfer transactions and settles payments
207
+ by broadcasting them to the TRON network.
208
+
209
+ The verification process:
210
+ 1. Validates scheme and network compatibility
211
+ 2. Parses the payment payload (signed transaction + authorization)
212
+ 3. Validates TRON addresses (from, to, contract)
213
+ 4. Verifies the transaction signature via the signer
214
+ 5. Checks authorization expiration (with buffer)
215
+ 6. Verifies TRC-20 balance sufficiency
216
+ 7. Validates amount >= required amount
217
+ 8. Confirms recipient and asset matching
218
+ 9. Verifies account activation status
219
+
220
+ Example:
221
+ ```python
222
+ facilitator = ExactTronFacilitatorScheme(
223
+ signer=my_tron_signer,
224
+ config=ExactTronFacilitatorConfig(can_sponsor_gas=True),
225
+ )
226
+
227
+ # Verify a payment
228
+ result = await facilitator.verify(payload, requirements)
229
+ if result.is_valid:
230
+ # Settle the payment
231
+ settlement = await facilitator.settle(payload, requirements)
232
+ ```
233
+ """
234
+
235
+ scheme = SCHEME_EXACT
236
+ caip_family = "tron:*"
237
+
238
+ def __init__(
239
+ self,
240
+ signer: FacilitatorTronSigner,
241
+ config: Optional[ExactTronFacilitatorConfig] = None,
242
+ ):
243
+ """Initialize the TRON facilitator scheme.
244
+
245
+ Args:
246
+ signer: TRON signer for transaction verification and broadcasting
247
+ config: Optional configuration for the facilitator
248
+ """
249
+ self._signer = signer
250
+ self._config = config
251
+
252
+ def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
253
+ """Get mechanism-specific extra data for supported kinds.
254
+
255
+ Returns the default asset info and gas sponsor address (if configured)
256
+ for the /supported endpoint.
257
+
258
+ Args:
259
+ network: The network identifier
260
+
261
+ Returns:
262
+ Dict with defaultAsset, symbol, decimals, and optionally gasSponsor
263
+ """
264
+ network_config = get_network_config(str(network))
265
+ if not network_config:
266
+ return None
267
+
268
+ default_asset = network_config["default_asset"]
269
+ result: Dict[str, Any] = {
270
+ "defaultAsset": default_asset["contract_address"],
271
+ "symbol": default_asset["symbol"],
272
+ "decimals": default_asset["decimals"],
273
+ }
274
+
275
+ if self._config and self._config.can_sponsor_gas:
276
+ addresses = self._signer.get_addresses(str(network))
277
+ if addresses:
278
+ result["gasSponsor"] = addresses[0]
279
+
280
+ return result
281
+
282
+ def get_signers(self, network: Network) -> List[str]:
283
+ """Get signer addresses for this facilitator.
284
+
285
+ Args:
286
+ network: The network identifier
287
+
288
+ Returns:
289
+ List of facilitator TRON addresses
290
+ """
291
+ return self._signer.get_addresses(str(network))
292
+
293
+ async def verify(
294
+ self,
295
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
296
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
297
+ ) -> VerifyResponse:
298
+ """Verify a TRON exact payment payload against requirements.
299
+
300
+ Performs comprehensive validation including:
301
+ - Scheme and network validation
302
+ - Payload structure parsing
303
+ - Address validation (from, to, contract)
304
+ - ECDSA signature verification via signer
305
+ - Expiration check with MIN_VALIDITY_BUFFER
306
+ - Balance sufficiency check
307
+ - Amount sufficiency check
308
+ - Recipient and asset matching
309
+ - Account activation check
310
+
311
+ Args:
312
+ payload: The payment payload containing signed transaction
313
+ requirements: The payment requirements to verify against
314
+
315
+ Returns:
316
+ VerifyResponse indicating validity, reason if invalid, and payer
317
+ """
318
+ try:
319
+ # Extract data from payload and requirements
320
+ payload_data = self._extract_payload(payload)
321
+ req_data = self._extract_requirements(requirements)
322
+
323
+ network = req_data.get("network", "")
324
+ payer = ""
325
+
326
+ # Step 1: Validate scheme
327
+ req_scheme = req_data.get("scheme", "")
328
+ if req_scheme and req_scheme != SCHEME_EXACT:
329
+ return VerifyResponse(
330
+ is_valid=False,
331
+ invalid_reason="unsupported_scheme",
332
+ payer=None,
333
+ )
334
+
335
+ # Step 2: Validate network is supported
336
+ if not is_valid_network(network):
337
+ return VerifyResponse(
338
+ is_valid=False,
339
+ invalid_reason="unsupported_network",
340
+ payer=None,
341
+ )
342
+
343
+ # Step 3: Parse the TRON payment payload
344
+ tron_payload = self._parse_tron_payload(payload_data)
345
+ if tron_payload is None:
346
+ return VerifyResponse(
347
+ is_valid=False,
348
+ invalid_reason="invalid_payload",
349
+ payer=None,
350
+ )
351
+
352
+ authorization = tron_payload.authorization
353
+ payer = authorization.from_
354
+
355
+ # Step 4: Validate addresses
356
+ if not validate_tron_address(authorization.from_):
357
+ return VerifyResponse(
358
+ is_valid=False,
359
+ invalid_reason="invalid_sender_address",
360
+ payer=payer,
361
+ )
362
+
363
+ if not validate_tron_address(authorization.to):
364
+ return VerifyResponse(
365
+ is_valid=False,
366
+ invalid_reason="invalid_recipient_address",
367
+ payer=payer,
368
+ )
369
+
370
+ if not validate_tron_address(authorization.contract_address):
371
+ return VerifyResponse(
372
+ is_valid=False,
373
+ invalid_reason="invalid_contract_address",
374
+ payer=payer,
375
+ )
376
+
377
+ # Step 5: Verify transaction signature via signer
378
+ verify_result = await self._signer.verify_transaction(
379
+ signed_transaction=tron_payload.signed_transaction,
380
+ expected_from=authorization.from_,
381
+ expected_to=req_data.get("payTo", ""),
382
+ expected_contract=req_data.get("asset", ""),
383
+ expected_amount=authorization.amount,
384
+ network=network,
385
+ )
386
+
387
+ if not verify_result.valid:
388
+ reason = verify_result.reason or "unknown"
389
+ return VerifyResponse(
390
+ is_valid=False,
391
+ invalid_reason=f"transaction_verification_failed: {reason}",
392
+ payer=payer,
393
+ )
394
+
395
+ # Step 6: Check authorization expiry (with buffer)
396
+ now_ms = int(time.time() * 1000)
397
+ expiration_with_buffer = authorization.expiration - (
398
+ MIN_VALIDITY_BUFFER * 1000
399
+ )
400
+ if now_ms >= expiration_with_buffer:
401
+ return VerifyResponse(
402
+ is_valid=False,
403
+ invalid_reason="authorization_expired",
404
+ payer=payer,
405
+ )
406
+
407
+ # Step 7: Verify TRC-20 balance
408
+ balance_str = await self._signer.get_balance(
409
+ owner_address=authorization.from_,
410
+ contract_address=req_data.get("asset", ""),
411
+ network=network,
412
+ )
413
+
414
+ required_amount_str = req_data.get("amount", "0")
415
+ try:
416
+ required_amount = int(required_amount_str)
417
+ except (ValueError, TypeError):
418
+ return VerifyResponse(
419
+ is_valid=False,
420
+ invalid_reason="invalid_required_amount",
421
+ payer=payer,
422
+ )
423
+
424
+ try:
425
+ balance = int(balance_str)
426
+ except (ValueError, TypeError):
427
+ return VerifyResponse(
428
+ is_valid=False,
429
+ invalid_reason="invalid_balance_format",
430
+ payer=payer,
431
+ )
432
+
433
+ if balance < required_amount:
434
+ return VerifyResponse(
435
+ is_valid=False,
436
+ invalid_reason="insufficient_balance",
437
+ payer=payer,
438
+ )
439
+
440
+ # Step 8: Verify amount sufficiency
441
+ try:
442
+ payload_amount = int(authorization.amount)
443
+ except (ValueError, TypeError):
444
+ return VerifyResponse(
445
+ is_valid=False,
446
+ invalid_reason="invalid_payload_amount",
447
+ payer=payer,
448
+ )
449
+
450
+ if payload_amount < required_amount:
451
+ return VerifyResponse(
452
+ is_valid=False,
453
+ invalid_reason="insufficient_amount",
454
+ payer=payer,
455
+ )
456
+
457
+ # Step 9: Verify recipient matching
458
+ pay_to = req_data.get("payTo", "")
459
+ if not addresses_equal(authorization.to, pay_to):
460
+ return VerifyResponse(
461
+ is_valid=False,
462
+ invalid_reason="recipient_mismatch",
463
+ payer=payer,
464
+ )
465
+
466
+ # Step 10: Verify contract address matching
467
+ asset = req_data.get("asset", "")
468
+ if not addresses_equal(authorization.contract_address, asset):
469
+ return VerifyResponse(
470
+ is_valid=False,
471
+ invalid_reason="asset_mismatch",
472
+ payer=payer,
473
+ )
474
+
475
+ # Step 11: Verify account is activated
476
+ is_active = await self._signer.is_activated(
477
+ authorization.from_, network
478
+ )
479
+ if not is_active:
480
+ return VerifyResponse(
481
+ is_valid=False,
482
+ invalid_reason="account_not_activated",
483
+ payer=payer,
484
+ )
485
+
486
+ # All checks passed
487
+ return VerifyResponse(
488
+ is_valid=True,
489
+ invalid_reason=None,
490
+ payer=payer,
491
+ )
492
+
493
+ except Exception as e:
494
+ logger.error(f"TRON verification failed: {e}")
495
+ return VerifyResponse(
496
+ is_valid=False,
497
+ invalid_reason=f"verification_error: {str(e)}",
498
+ payer=None,
499
+ )
500
+
501
+ async def settle(
502
+ self,
503
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
504
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
505
+ ) -> SettleResponse:
506
+ """Settle a TRON exact payment by broadcasting the signed transaction.
507
+
508
+ Performs verification first, then broadcasts the signed transaction
509
+ to the TRON network and waits for confirmation.
510
+
511
+ Args:
512
+ payload: The verified payment payload with signed transaction
513
+ requirements: The payment requirements
514
+
515
+ Returns:
516
+ SettleResponse with transaction ID, network, and status
517
+ """
518
+ req_data = self._extract_requirements(requirements)
519
+ network = req_data.get("network", "")
520
+
521
+ # Step 1: Verify the payment first
522
+ verify_result = await self.verify(payload, requirements)
523
+
524
+ if not verify_result.is_valid:
525
+ return SettleResponse(
526
+ success=False,
527
+ error_reason=verify_result.invalid_reason,
528
+ transaction=None,
529
+ network=network,
530
+ payer=verify_result.payer,
531
+ )
532
+
533
+ try:
534
+ # Step 2: Parse payload for broadcast
535
+ payload_data = self._extract_payload(payload)
536
+ tron_payload = self._parse_tron_payload(payload_data)
537
+
538
+ if tron_payload is None:
539
+ return SettleResponse(
540
+ success=False,
541
+ error_reason="invalid_payload",
542
+ transaction=None,
543
+ network=network,
544
+ payer=verify_result.payer,
545
+ )
546
+
547
+ # Step 3: Broadcast the signed transaction
548
+ tx_id = await self._signer.broadcast_transaction(
549
+ signed_transaction=tron_payload.signed_transaction,
550
+ network=network,
551
+ )
552
+
553
+ # Step 4: Wait for confirmation
554
+ confirmation = await self._signer.wait_for_transaction(
555
+ tx_id=tx_id,
556
+ network=network,
557
+ timeout=DEFAULT_CONFIRMATION_TIMEOUT,
558
+ )
559
+
560
+ if not confirmation.success:
561
+ return SettleResponse(
562
+ success=False,
563
+ error_reason=confirmation.error or "confirmation_failed",
564
+ transaction=tx_id,
565
+ network=network,
566
+ payer=verify_result.payer,
567
+ )
568
+
569
+ # Use the confirmed tx_id if available (may differ from broadcast)
570
+ final_tx_id = confirmation.tx_id or tx_id
571
+
572
+ return SettleResponse(
573
+ success=True,
574
+ error_reason=None,
575
+ transaction=final_tx_id,
576
+ network=network,
577
+ payer=verify_result.payer,
578
+ )
579
+
580
+ except Exception as e:
581
+ logger.error(f"TRON settlement failed: {e}")
582
+ return SettleResponse(
583
+ success=False,
584
+ error_reason=f"settlement_error: {str(e)}",
585
+ transaction=None,
586
+ network=network,
587
+ payer=verify_result.payer,
588
+ )
589
+
590
+ def _extract_payload(
591
+ self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
592
+ ) -> Dict[str, Any]:
593
+ """Extract payload data as a dict.
594
+
595
+ Handles PaymentPayloadV2 models (with nested 'payload' field)
596
+ and plain dicts.
597
+
598
+ Args:
599
+ payload: Payment payload (model or dict)
600
+
601
+ Returns:
602
+ Dict containing signedTransaction and authorization data
603
+ """
604
+ if hasattr(payload, "model_dump"):
605
+ data = payload.model_dump(by_alias=True)
606
+ return data.get("payload", data)
607
+ elif isinstance(payload, dict):
608
+ return payload.get("payload", payload)
609
+ return dict(payload)
610
+
611
+ def _extract_requirements(
612
+ self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
613
+ ) -> Dict[str, Any]:
614
+ """Extract requirements data as a dict.
615
+
616
+ Args:
617
+ requirements: Payment requirements (model or dict)
618
+
619
+ Returns:
620
+ Dict containing requirement fields
621
+ """
622
+ if hasattr(requirements, "model_dump"):
623
+ return requirements.model_dump(by_alias=True)
624
+ return dict(requirements)
625
+
626
+ def _parse_tron_payload(
627
+ self, payload_data: Dict[str, Any]
628
+ ) -> Optional[TronPaymentPayload]:
629
+ """Parse raw payload data into a TronPaymentPayload model.
630
+
631
+ Args:
632
+ payload_data: Dict with signedTransaction and authorization fields
633
+
634
+ Returns:
635
+ TronPaymentPayload if parsing succeeds, None otherwise
636
+ """
637
+ try:
638
+ # Validate required fields exist
639
+ if "signedTransaction" not in payload_data:
640
+ logger.debug("Missing signedTransaction in payload")
641
+ return None
642
+
643
+ if "authorization" not in payload_data:
644
+ logger.debug("Missing authorization in payload")
645
+ return None
646
+
647
+ auth_data = payload_data["authorization"]
648
+ if not auth_data.get("from"):
649
+ logger.debug("Missing authorization.from in payload")
650
+ return None
651
+
652
+ return TronPaymentPayload.model_validate(payload_data)
653
+ except Exception as e:
654
+ logger.debug(f"Failed to parse TRON payload: {e}")
655
+ return None
656
+
657
+
658
+ class ExactTronFacilitatorConfig:
659
+ """Configuration for the TRON exact facilitator scheme.
660
+
661
+ Attributes:
662
+ can_sponsor_gas: Whether this facilitator can sponsor gas costs
663
+ for transactions. When True, the facilitator's address will
664
+ be advertised as a gas sponsor in the /supported response.
665
+ """
666
+
667
+ def __init__(self, can_sponsor_gas: bool = False):
668
+ """Initialize facilitator configuration.
669
+
670
+ Args:
671
+ can_sponsor_gas: Whether to advertise gas sponsorship capability
672
+ """
673
+ self.can_sponsor_gas = can_sponsor_gas
@@ -98,7 +98,7 @@ class ExactTronServerScheme:
98
98
  amount_decimal = Decimal(str(price))
99
99
 
100
100
  # Convert to atomic units
101
- atomic_amount = int(amount_decimal * Decimal(10 ** decimals))
101
+ atomic_amount = int(amount_decimal * Decimal(10**decimals))
102
102
 
103
103
  # Build extra metadata
104
104
  extra = {