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,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
|
|
101
|
+
atomic_amount = int(amount_decimal * Decimal(10**decimals))
|
|
102
102
|
|
|
103
103
|
# Build extra metadata
|
|
104
104
|
extra = {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""TRON Up-To Payment Scheme.
|
|
2
|
+
|
|
3
|
+
This package provides the upto payment scheme types for TRON networks
|
|
4
|
+
using TRC-20 approve + transferFrom for authorized maximum-amount payments.
|
|
5
|
+
|
|
6
|
+
The upto scheme allows clients to authorize a maximum amount that can be
|
|
7
|
+
settled later based on actual usage.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from t402.schemes.tron.upto.types import (
|
|
11
|
+
# Models
|
|
12
|
+
UptoTronAuthorization,
|
|
13
|
+
UptoTronPayload,
|
|
14
|
+
UptoTronExtra,
|
|
15
|
+
# Type guards
|
|
16
|
+
is_upto_tron_payload,
|
|
17
|
+
# Helper functions
|
|
18
|
+
upto_payload_from_dict,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Types
|
|
23
|
+
"UptoTronAuthorization",
|
|
24
|
+
"UptoTronPayload",
|
|
25
|
+
"UptoTronExtra",
|
|
26
|
+
# Type guards
|
|
27
|
+
"is_upto_tron_payload",
|
|
28
|
+
# Helper functions
|
|
29
|
+
"upto_payload_from_dict",
|
|
30
|
+
]
|