t402 1.7.1__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 +164 -1
- 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 +67 -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 +70 -0
- t402/schemes/evm/upto/client.py +244 -0
- t402/schemes/evm/upto/facilitator.py +625 -0
- t402/schemes/evm/upto/server.py +243 -0
- t402/schemes/evm/upto/types.py +307 -0
- 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/schemes/upto/__init__.py +80 -0
- t402/schemes/upto/types.py +376 -0
- 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 +4 -2
- t402/wdk/errors.py +15 -5
- t402/wdk/signer.py +11 -2
- {t402-1.7.1.dist-info → t402-1.9.1.dist-info}/METADATA +42 -1
- t402-1.9.1.dist-info/RECORD +125 -0
- t402-1.7.1.dist-info/RECORD +0 -67
- {t402-1.7.1.dist-info → t402-1.9.1.dist-info}/WHEEL +0 -0
- {t402-1.7.1.dist-info → t402-1.9.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""Tezos Exact-Direct Scheme - Facilitator Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the facilitator-side implementation of the exact-direct
|
|
4
|
+
payment scheme for Tezos.
|
|
5
|
+
|
|
6
|
+
The facilitator:
|
|
7
|
+
1. Receives a payment payload containing an operation hash
|
|
8
|
+
2. Queries the Tezos blockchain (via TzKT indexer) for operation details
|
|
9
|
+
3. Verifies: status="applied", correct sender/recipient/amount/contract
|
|
10
|
+
4. For settle: the operation is already executed, so settle confirms verification
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any, Dict, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
from t402.types import (
|
|
19
|
+
PaymentRequirementsV2,
|
|
20
|
+
PaymentPayloadV2,
|
|
21
|
+
VerifyResponse,
|
|
22
|
+
SettleResponse,
|
|
23
|
+
Network,
|
|
24
|
+
)
|
|
25
|
+
from t402.schemes.tezos.constants import (
|
|
26
|
+
SCHEME_EXACT_DIRECT,
|
|
27
|
+
FA2_TRANSFER_ENTRYPOINT,
|
|
28
|
+
is_valid_address,
|
|
29
|
+
is_valid_operation_hash,
|
|
30
|
+
parse_asset_identifier,
|
|
31
|
+
)
|
|
32
|
+
from t402.schemes.tezos.types import FacilitatorTezosSigner
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ExactDirectTezosFacilitator:
|
|
39
|
+
"""Facilitator scheme for Tezos exact-direct payments.
|
|
40
|
+
|
|
41
|
+
Verifies on-chain FA2 transfer operations by querying the Tezos blockchain
|
|
42
|
+
and checking that the operation matches the payment requirements.
|
|
43
|
+
|
|
44
|
+
In the exact-direct scheme, the client has already executed the transfer,
|
|
45
|
+
so the facilitator's role is purely verification. Settlement confirms that
|
|
46
|
+
the operation was successfully applied on-chain.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
```python
|
|
50
|
+
from t402.schemes.tezos import ExactDirectTezosFacilitator
|
|
51
|
+
|
|
52
|
+
class MyTezosQuerier:
|
|
53
|
+
async def get_operation(self, op_hash, network):
|
|
54
|
+
# Query TzKT indexer
|
|
55
|
+
return {...}
|
|
56
|
+
|
|
57
|
+
facilitator = ExactDirectTezosFacilitator(
|
|
58
|
+
signer=MyTezosQuerier(),
|
|
59
|
+
addresses={"tezos:NetXdQprcVkpaWU": "tz1..."},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
result = await facilitator.verify(payload, requirements)
|
|
63
|
+
if result.is_valid:
|
|
64
|
+
settlement = await facilitator.settle(payload, requirements)
|
|
65
|
+
```
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
scheme = SCHEME_EXACT_DIRECT
|
|
69
|
+
caip_family = "tezos:*"
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
signer: FacilitatorTezosSigner,
|
|
74
|
+
addresses: Optional[Dict[str, str]] = None,
|
|
75
|
+
):
|
|
76
|
+
"""Initialize the Tezos exact-direct facilitator.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
signer: A Tezos operation querier implementing FacilitatorTezosSigner.
|
|
80
|
+
Must provide get_operation() method.
|
|
81
|
+
addresses: Mapping of network -> facilitator Tezos address.
|
|
82
|
+
Used for get_signers() responses. Keys are CAIP-2 network IDs.
|
|
83
|
+
"""
|
|
84
|
+
self._signer = signer
|
|
85
|
+
self._addresses = addresses or {}
|
|
86
|
+
|
|
87
|
+
def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
|
|
88
|
+
"""Get mechanism-specific extra data for supported kinds.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
network: The network identifier
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
None (no extra data needed for exact-direct scheme)
|
|
95
|
+
"""
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def get_signers(self, network: Network) -> List[str]:
|
|
99
|
+
"""Get signer addresses for this facilitator on a given network.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
network: The network identifier (CAIP-2 format)
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of facilitator addresses for the given network
|
|
106
|
+
"""
|
|
107
|
+
address = self._addresses.get(network)
|
|
108
|
+
if address:
|
|
109
|
+
return [address]
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
async def verify(
|
|
113
|
+
self,
|
|
114
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
115
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
116
|
+
) -> VerifyResponse:
|
|
117
|
+
"""Verify a Tezos exact-direct payment payload.
|
|
118
|
+
|
|
119
|
+
Queries the Tezos blockchain for the operation hash and verifies:
|
|
120
|
+
1. Operation exists and has status "applied"
|
|
121
|
+
2. Target contract matches the expected FA2 contract
|
|
122
|
+
3. Entrypoint is "transfer"
|
|
123
|
+
4. Sender matches the payload's "from" field
|
|
124
|
+
5. Recipient matches the requirements' "payTo" address
|
|
125
|
+
6. Amount is >= the required amount
|
|
126
|
+
7. Token ID matches
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
payload: The payment payload containing the operation hash
|
|
130
|
+
requirements: The payment requirements to verify against
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
VerifyResponse indicating validity and payer address
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
# Extract payload and requirements data
|
|
137
|
+
payload_data = self._extract_payload(payload)
|
|
138
|
+
req_data = self._extract_requirements(requirements)
|
|
139
|
+
|
|
140
|
+
# Get the inner payload fields
|
|
141
|
+
op_hash = payload_data.get("opHash", "")
|
|
142
|
+
from_address = payload_data.get("from", "")
|
|
143
|
+
_to_address = payload_data.get("to", "") # noqa: F841
|
|
144
|
+
_amount_str = payload_data.get("amount", "0") # noqa: F841
|
|
145
|
+
contract_address = payload_data.get("contractAddress", "")
|
|
146
|
+
token_id = payload_data.get("tokenId", 0)
|
|
147
|
+
|
|
148
|
+
# Validate operation hash format
|
|
149
|
+
if not is_valid_operation_hash(op_hash):
|
|
150
|
+
return VerifyResponse(
|
|
151
|
+
is_valid=False,
|
|
152
|
+
invalid_reason=f"Invalid operation hash format: {op_hash}",
|
|
153
|
+
payer=from_address or None,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Validate from address
|
|
157
|
+
if not is_valid_address(from_address):
|
|
158
|
+
return VerifyResponse(
|
|
159
|
+
is_valid=False,
|
|
160
|
+
invalid_reason=f"Invalid sender address: {from_address}",
|
|
161
|
+
payer=None,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Get required fields from requirements
|
|
165
|
+
req_network = req_data.get("network", "")
|
|
166
|
+
req_pay_to = req_data.get("payTo") or req_data.get("pay_to", "")
|
|
167
|
+
req_amount = req_data.get("amount", "0")
|
|
168
|
+
req_asset = req_data.get("asset", "")
|
|
169
|
+
|
|
170
|
+
# Parse expected asset info from requirements
|
|
171
|
+
if req_asset:
|
|
172
|
+
try:
|
|
173
|
+
expected_asset = parse_asset_identifier(req_asset)
|
|
174
|
+
expected_contract = expected_asset["contract_address"]
|
|
175
|
+
expected_token_id = expected_asset["token_id"]
|
|
176
|
+
except ValueError as e:
|
|
177
|
+
return VerifyResponse(
|
|
178
|
+
is_valid=False,
|
|
179
|
+
invalid_reason=f"Invalid asset in requirements: {e}",
|
|
180
|
+
payer=from_address,
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
expected_contract = contract_address
|
|
184
|
+
expected_token_id = token_id
|
|
185
|
+
|
|
186
|
+
# Query the operation on-chain
|
|
187
|
+
try:
|
|
188
|
+
operation = await self._signer.get_operation(op_hash, req_network)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.error("Failed to query operation %s: %s", op_hash, e)
|
|
191
|
+
return VerifyResponse(
|
|
192
|
+
is_valid=False,
|
|
193
|
+
invalid_reason=f"Failed to query operation: {str(e)}",
|
|
194
|
+
payer=from_address,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if not operation:
|
|
198
|
+
return VerifyResponse(
|
|
199
|
+
is_valid=False,
|
|
200
|
+
invalid_reason=f"Operation not found: {op_hash}",
|
|
201
|
+
payer=from_address,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Check operation status
|
|
205
|
+
status = operation.get("status", "")
|
|
206
|
+
if status != "applied":
|
|
207
|
+
return VerifyResponse(
|
|
208
|
+
is_valid=False,
|
|
209
|
+
invalid_reason=(
|
|
210
|
+
f"Operation status is '{status}', expected 'applied'"
|
|
211
|
+
),
|
|
212
|
+
payer=from_address,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Check entrypoint
|
|
216
|
+
entrypoint = operation.get("entrypoint", "")
|
|
217
|
+
if entrypoint != FA2_TRANSFER_ENTRYPOINT:
|
|
218
|
+
return VerifyResponse(
|
|
219
|
+
is_valid=False,
|
|
220
|
+
invalid_reason=(
|
|
221
|
+
f"Operation entrypoint is '{entrypoint}', "
|
|
222
|
+
f"expected '{FA2_TRANSFER_ENTRYPOINT}'"
|
|
223
|
+
),
|
|
224
|
+
payer=from_address,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Check target contract
|
|
228
|
+
target = operation.get("target", {})
|
|
229
|
+
target_address = target.get("address", "") if isinstance(target, dict) else ""
|
|
230
|
+
if target_address != expected_contract:
|
|
231
|
+
return VerifyResponse(
|
|
232
|
+
is_valid=False,
|
|
233
|
+
invalid_reason=(
|
|
234
|
+
f"Operation target contract '{target_address}' does not match "
|
|
235
|
+
f"expected '{expected_contract}'"
|
|
236
|
+
),
|
|
237
|
+
payer=from_address,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Check sender
|
|
241
|
+
sender = operation.get("sender", {})
|
|
242
|
+
sender_address = sender.get("address", "") if isinstance(sender, dict) else ""
|
|
243
|
+
if sender_address != from_address:
|
|
244
|
+
return VerifyResponse(
|
|
245
|
+
is_valid=False,
|
|
246
|
+
invalid_reason=(
|
|
247
|
+
f"Operation sender '{sender_address}' does not match "
|
|
248
|
+
f"payload sender '{from_address}'"
|
|
249
|
+
),
|
|
250
|
+
payer=from_address,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Extract and verify transfer parameters
|
|
254
|
+
transfer_details = self._extract_transfer_details(operation)
|
|
255
|
+
if transfer_details is None:
|
|
256
|
+
return VerifyResponse(
|
|
257
|
+
is_valid=False,
|
|
258
|
+
invalid_reason="Failed to parse FA2 transfer parameters",
|
|
259
|
+
payer=from_address,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Verify recipient
|
|
263
|
+
if req_pay_to and transfer_details["to"] != req_pay_to:
|
|
264
|
+
return VerifyResponse(
|
|
265
|
+
is_valid=False,
|
|
266
|
+
invalid_reason=(
|
|
267
|
+
f"Transfer recipient '{transfer_details['to']}' does not match "
|
|
268
|
+
f"required payTo '{req_pay_to}'"
|
|
269
|
+
),
|
|
270
|
+
payer=from_address,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Verify amount (must be >= required)
|
|
274
|
+
try:
|
|
275
|
+
transfer_amount = int(transfer_details["amount"])
|
|
276
|
+
required_amount = int(req_amount)
|
|
277
|
+
if transfer_amount < required_amount:
|
|
278
|
+
return VerifyResponse(
|
|
279
|
+
is_valid=False,
|
|
280
|
+
invalid_reason=(
|
|
281
|
+
f"Transfer amount {transfer_amount} is less than "
|
|
282
|
+
f"required amount {required_amount}"
|
|
283
|
+
),
|
|
284
|
+
payer=from_address,
|
|
285
|
+
)
|
|
286
|
+
except (ValueError, TypeError) as e:
|
|
287
|
+
return VerifyResponse(
|
|
288
|
+
is_valid=False,
|
|
289
|
+
invalid_reason=f"Invalid amount in transfer: {e}",
|
|
290
|
+
payer=from_address,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Verify token ID
|
|
294
|
+
if transfer_details.get("token_id") != expected_token_id:
|
|
295
|
+
return VerifyResponse(
|
|
296
|
+
is_valid=False,
|
|
297
|
+
invalid_reason=(
|
|
298
|
+
f"Token ID {transfer_details.get('token_id')} does not match "
|
|
299
|
+
f"expected {expected_token_id}"
|
|
300
|
+
),
|
|
301
|
+
payer=from_address,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# All checks passed
|
|
305
|
+
return VerifyResponse(
|
|
306
|
+
is_valid=True,
|
|
307
|
+
invalid_reason=None,
|
|
308
|
+
payer=from_address,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error("Tezos verification failed: %s", e)
|
|
313
|
+
return VerifyResponse(
|
|
314
|
+
is_valid=False,
|
|
315
|
+
invalid_reason=f"Verification error: {str(e)}",
|
|
316
|
+
payer=None,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
async def settle(
|
|
320
|
+
self,
|
|
321
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
322
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
323
|
+
) -> SettleResponse:
|
|
324
|
+
"""Settle a verified Tezos exact-direct payment.
|
|
325
|
+
|
|
326
|
+
In the exact-direct scheme, the transfer has already been executed by
|
|
327
|
+
the client. Settlement simply confirms the verification was successful
|
|
328
|
+
and returns the operation hash as the transaction reference.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
payload: The verified payment payload with operation hash
|
|
332
|
+
requirements: The payment requirements
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
SettleResponse with operation hash and status
|
|
336
|
+
"""
|
|
337
|
+
try:
|
|
338
|
+
# First verify the payment
|
|
339
|
+
verify_result = await self.verify(payload, requirements)
|
|
340
|
+
|
|
341
|
+
if not verify_result.is_valid:
|
|
342
|
+
return SettleResponse(
|
|
343
|
+
success=False,
|
|
344
|
+
error_reason=verify_result.invalid_reason,
|
|
345
|
+
transaction=None,
|
|
346
|
+
network=self._get_network(requirements),
|
|
347
|
+
payer=verify_result.payer,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Extract operation hash from payload
|
|
351
|
+
payload_data = self._extract_payload(payload)
|
|
352
|
+
op_hash = payload_data.get("opHash", "")
|
|
353
|
+
network = self._get_network(requirements)
|
|
354
|
+
|
|
355
|
+
return SettleResponse(
|
|
356
|
+
success=True,
|
|
357
|
+
error_reason=None,
|
|
358
|
+
transaction=op_hash,
|
|
359
|
+
network=network,
|
|
360
|
+
payer=verify_result.payer,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error("Tezos settlement failed: %s", e)
|
|
365
|
+
return SettleResponse(
|
|
366
|
+
success=False,
|
|
367
|
+
error_reason=f"Settlement error: {str(e)}",
|
|
368
|
+
transaction=None,
|
|
369
|
+
network=self._get_network(requirements),
|
|
370
|
+
payer=None,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def _extract_payload(
|
|
374
|
+
self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
|
|
375
|
+
) -> Dict[str, Any]:
|
|
376
|
+
"""Extract payload data as a dict.
|
|
377
|
+
|
|
378
|
+
Handles both PaymentPayloadV2 models (where the inner payload is
|
|
379
|
+
in the 'payload' field) and plain dicts.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
payload: Payment payload (model or dict)
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Dict containing the inner payload fields
|
|
386
|
+
"""
|
|
387
|
+
if hasattr(payload, "model_dump"):
|
|
388
|
+
data = payload.model_dump(by_alias=True)
|
|
389
|
+
return data.get("payload", data)
|
|
390
|
+
elif isinstance(payload, dict):
|
|
391
|
+
return payload.get("payload", payload)
|
|
392
|
+
return dict(payload)
|
|
393
|
+
|
|
394
|
+
def _extract_requirements(
|
|
395
|
+
self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
|
|
396
|
+
) -> Dict[str, Any]:
|
|
397
|
+
"""Extract requirements data as a dict.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
requirements: Payment requirements (model or dict)
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Dict containing requirement fields
|
|
404
|
+
"""
|
|
405
|
+
if hasattr(requirements, "model_dump"):
|
|
406
|
+
return requirements.model_dump(by_alias=True)
|
|
407
|
+
return dict(requirements)
|
|
408
|
+
|
|
409
|
+
def _get_network(
|
|
410
|
+
self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
|
|
411
|
+
) -> Optional[str]:
|
|
412
|
+
"""Extract network from requirements.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
requirements: Payment requirements
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Network string or None
|
|
419
|
+
"""
|
|
420
|
+
if hasattr(requirements, "model_dump"):
|
|
421
|
+
data = requirements.model_dump(by_alias=True)
|
|
422
|
+
return data.get("network")
|
|
423
|
+
elif isinstance(requirements, dict):
|
|
424
|
+
return requirements.get("network")
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
def _extract_transfer_details(
|
|
428
|
+
self, operation: Dict[str, Any]
|
|
429
|
+
) -> Optional[Dict[str, Any]]:
|
|
430
|
+
"""Extract FA2 transfer details from an operation.
|
|
431
|
+
|
|
432
|
+
Parses the FA2 transfer parameter to extract sender, recipient,
|
|
433
|
+
amount, and token ID from the first transfer in the batch.
|
|
434
|
+
|
|
435
|
+
The parameter structure follows the FA2 standard:
|
|
436
|
+
[{"from_": "tz1...", "txs": [{"to_": "tz1...", "token_id": 0, "amount": "1000000"}]}]
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
operation: Operation dict from the indexer
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Dict with "from", "to", "amount", "token_id" if parsing succeeds,
|
|
443
|
+
None if the parameter cannot be parsed
|
|
444
|
+
"""
|
|
445
|
+
parameter = operation.get("parameter")
|
|
446
|
+
if parameter is None:
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
# Parameter can be a list of transfer batches or a single batch
|
|
451
|
+
if isinstance(parameter, list):
|
|
452
|
+
params = parameter
|
|
453
|
+
elif isinstance(parameter, dict):
|
|
454
|
+
# Some indexers wrap in a value field
|
|
455
|
+
value = parameter.get("value", parameter)
|
|
456
|
+
if isinstance(value, list):
|
|
457
|
+
params = value
|
|
458
|
+
else:
|
|
459
|
+
params = [value]
|
|
460
|
+
else:
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
if not params:
|
|
464
|
+
return None
|
|
465
|
+
|
|
466
|
+
first_param = params[0]
|
|
467
|
+
from_address = first_param.get("from_") or first_param.get("from", "")
|
|
468
|
+
|
|
469
|
+
txs = first_param.get("txs", [])
|
|
470
|
+
if not txs:
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
first_tx = txs[0]
|
|
474
|
+
to_address = first_tx.get("to_") or first_tx.get("to", "")
|
|
475
|
+
amount = str(first_tx.get("amount", "0"))
|
|
476
|
+
token_id = first_tx.get("token_id", 0)
|
|
477
|
+
|
|
478
|
+
# Handle token_id as string or int
|
|
479
|
+
if isinstance(token_id, str):
|
|
480
|
+
token_id = int(token_id)
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
"from": from_address,
|
|
484
|
+
"to": to_address,
|
|
485
|
+
"amount": amount,
|
|
486
|
+
"token_id": token_id,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
except (KeyError, IndexError, TypeError, ValueError) as e:
|
|
490
|
+
logger.debug("Failed to parse FA2 transfer parameters: %s", e)
|
|
491
|
+
return None
|