t402 1.9.0__py3-none-any.whl → 1.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- t402/__init__.py +2 -1
- t402/bridge/client.py +13 -5
- t402/bridge/constants.py +3 -1
- t402/bridge/router.py +1 -1
- t402/bridge/scan.py +3 -1
- t402/chains.py +268 -1
- t402/cli.py +31 -9
- t402/common.py +2 -0
- t402/cosmos_paywall_template.py +2 -0
- t402/encoding.py +9 -3
- t402/erc4337/accounts.py +56 -51
- t402/erc4337/bundlers.py +105 -99
- t402/erc4337/paymasters.py +100 -109
- t402/erc4337/types.py +39 -26
- t402/evm_paywall_template.py +1 -1
- t402/fastapi/middleware.py +1 -3
- t402/mcp/server.py +79 -46
- t402/near_paywall_template.py +2 -0
- t402/networks.py +34 -1
- t402/paywall.py +1 -3
- t402/schemes/__init__.py +124 -0
- t402/schemes/aptos/__init__.py +70 -0
- t402/schemes/aptos/constants.py +349 -0
- t402/schemes/aptos/exact_direct/__init__.py +44 -0
- t402/schemes/aptos/exact_direct/client.py +202 -0
- t402/schemes/aptos/exact_direct/facilitator.py +426 -0
- t402/schemes/aptos/exact_direct/server.py +272 -0
- t402/schemes/aptos/types.py +237 -0
- t402/schemes/evm/__init__.py +46 -1
- t402/schemes/evm/exact/__init__.py +11 -0
- t402/schemes/evm/exact/client.py +3 -1
- t402/schemes/evm/exact/facilitator.py +894 -0
- t402/schemes/evm/exact/server.py +1 -1
- t402/schemes/evm/exact_legacy/__init__.py +38 -0
- t402/schemes/evm/exact_legacy/client.py +291 -0
- t402/schemes/evm/exact_legacy/facilitator.py +777 -0
- t402/schemes/evm/exact_legacy/server.py +231 -0
- t402/schemes/evm/upto/__init__.py +12 -0
- t402/schemes/evm/upto/client.py +6 -2
- t402/schemes/evm/upto/facilitator.py +625 -0
- t402/schemes/evm/upto/server.py +243 -0
- t402/schemes/evm/upto/types.py +3 -1
- t402/schemes/interfaces.py +6 -2
- t402/schemes/near/__init__.py +112 -0
- t402/schemes/near/constants.py +189 -0
- t402/schemes/near/exact_direct/__init__.py +21 -0
- t402/schemes/near/exact_direct/client.py +204 -0
- t402/schemes/near/exact_direct/facilitator.py +455 -0
- t402/schemes/near/exact_direct/server.py +303 -0
- t402/schemes/near/types.py +419 -0
- t402/schemes/polkadot/__init__.py +72 -0
- t402/schemes/polkadot/constants.py +155 -0
- t402/schemes/polkadot/exact_direct/__init__.py +43 -0
- t402/schemes/polkadot/exact_direct/client.py +235 -0
- t402/schemes/polkadot/exact_direct/facilitator.py +428 -0
- t402/schemes/polkadot/exact_direct/server.py +292 -0
- t402/schemes/polkadot/types.py +385 -0
- t402/schemes/registry.py +6 -2
- t402/schemes/stacks/__init__.py +68 -0
- t402/schemes/stacks/constants.py +122 -0
- t402/schemes/stacks/exact_direct/__init__.py +43 -0
- t402/schemes/stacks/exact_direct/client.py +222 -0
- t402/schemes/stacks/exact_direct/facilitator.py +424 -0
- t402/schemes/stacks/exact_direct/server.py +292 -0
- t402/schemes/stacks/types.py +380 -0
- t402/schemes/svm/__init__.py +29 -0
- t402/schemes/svm/exact/__init__.py +35 -0
- t402/schemes/svm/exact/client.py +23 -0
- t402/schemes/svm/exact/facilitator.py +24 -0
- t402/schemes/svm/exact/server.py +20 -0
- t402/schemes/tezos/__init__.py +84 -0
- t402/schemes/tezos/constants.py +372 -0
- t402/schemes/tezos/exact_direct/__init__.py +22 -0
- t402/schemes/tezos/exact_direct/client.py +226 -0
- t402/schemes/tezos/exact_direct/facilitator.py +491 -0
- t402/schemes/tezos/exact_direct/server.py +277 -0
- t402/schemes/tezos/types.py +220 -0
- t402/schemes/ton/__init__.py +9 -2
- t402/schemes/ton/exact/__init__.py +7 -0
- t402/schemes/ton/exact/facilitator.py +730 -0
- t402/schemes/ton/exact/server.py +1 -1
- t402/schemes/tron/__init__.py +11 -2
- t402/schemes/tron/exact/__init__.py +9 -0
- t402/schemes/tron/exact/facilitator.py +673 -0
- t402/schemes/tron/exact/server.py +1 -1
- t402/stacks_paywall_template.py +2 -0
- t402/svm.py +45 -11
- t402/svm_paywall_template.py +1 -1
- t402/ton.py +5 -1
- t402/ton_paywall_template.py +1 -192
- t402/tron.py +2 -0
- t402/tron_paywall_template.py +2 -0
- t402/types.py +3 -1
- t402/wdk/errors.py +15 -5
- t402/wdk/signer.py +11 -2
- {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/METADATA +42 -1
- t402-1.9.1.dist-info/RECORD +125 -0
- t402-1.9.0.dist-info/RECORD +0 -72
- {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/WHEEL +0 -0
- {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""Stacks Exact-Direct Scheme - Facilitator Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the facilitator-side implementation of the exact-direct
|
|
4
|
+
payment scheme for Stacks (Bitcoin L2) networks.
|
|
5
|
+
|
|
6
|
+
The facilitator:
|
|
7
|
+
1. Verifies payment payloads by querying the transaction on-chain
|
|
8
|
+
2. Validates that the transaction is a successful SIP-010 token transfer
|
|
9
|
+
matching the payment requirements (sender, recipient, amount, contract)
|
|
10
|
+
3. For settle(), confirms the transfer has already occurred on-chain
|
|
11
|
+
(since exact-direct payments are pre-paid by the client)
|
|
12
|
+
4. Maintains a txId cache for replay protection
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
|
19
|
+
|
|
20
|
+
from t402.types import (
|
|
21
|
+
PaymentRequirementsV2,
|
|
22
|
+
PaymentPayloadV2,
|
|
23
|
+
VerifyResponse,
|
|
24
|
+
SettleResponse,
|
|
25
|
+
Network,
|
|
26
|
+
)
|
|
27
|
+
from t402.schemes.stacks.constants import (
|
|
28
|
+
SCHEME_EXACT_DIRECT,
|
|
29
|
+
get_network_config,
|
|
30
|
+
is_stacks_network,
|
|
31
|
+
)
|
|
32
|
+
from t402.schemes.stacks.types import (
|
|
33
|
+
FacilitatorStacksSigner,
|
|
34
|
+
ExactDirectPayload,
|
|
35
|
+
TransactionResult,
|
|
36
|
+
is_valid_tx_id,
|
|
37
|
+
extract_token_transfer,
|
|
38
|
+
parse_contract_identifier,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ExactDirectStacksFacilitatorScheme:
|
|
46
|
+
"""Facilitator scheme for Stacks exact-direct payments.
|
|
47
|
+
|
|
48
|
+
Verifies on-chain SIP-010 token transfers by querying the transaction
|
|
49
|
+
via the Hiro API, and confirms the transfer matches the payment
|
|
50
|
+
requirements. Includes replay protection via txId caching.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
```python
|
|
54
|
+
facilitator = ExactDirectStacksFacilitatorScheme(
|
|
55
|
+
signer=my_stacks_facilitator_signer,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Verify a payment
|
|
59
|
+
result = await facilitator.verify(payload, requirements)
|
|
60
|
+
if result.is_valid:
|
|
61
|
+
# Payment is confirmed on-chain
|
|
62
|
+
settlement = await facilitator.settle(payload, requirements)
|
|
63
|
+
```
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
scheme = SCHEME_EXACT_DIRECT
|
|
67
|
+
caip_family = "stacks:*"
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
signer: FacilitatorStacksSigner,
|
|
72
|
+
):
|
|
73
|
+
"""Initialize the facilitator.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
signer: Stacks facilitator signer for querying transactions
|
|
77
|
+
"""
|
|
78
|
+
self._signer = signer
|
|
79
|
+
self._used_tx_ids: Set[str] = set()
|
|
80
|
+
|
|
81
|
+
def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
|
|
82
|
+
"""Get mechanism-specific extra data for supported kinds.
|
|
83
|
+
|
|
84
|
+
Returns asset metadata for the network's default token.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
network: The network identifier
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dict with asset metadata, or None if network is unsupported
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
config = get_network_config(network)
|
|
94
|
+
except ValueError:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"contractAddress": config.default_token.contract_address,
|
|
99
|
+
"assetSymbol": config.default_token.symbol,
|
|
100
|
+
"assetDecimals": config.default_token.decimals,
|
|
101
|
+
"networkName": config.name,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def get_signers(self, network: Network) -> List[str]:
|
|
105
|
+
"""Get signer addresses for this facilitator on a given network.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
network: The network identifier
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of facilitator Stacks addresses for the network
|
|
112
|
+
"""
|
|
113
|
+
return self._signer.get_addresses(network)
|
|
114
|
+
|
|
115
|
+
async def verify(
|
|
116
|
+
self,
|
|
117
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
118
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
119
|
+
) -> VerifyResponse:
|
|
120
|
+
"""Verify a Stacks exact-direct payment payload.
|
|
121
|
+
|
|
122
|
+
Queries the transaction on-chain and validates:
|
|
123
|
+
1. The transaction exists and was successful
|
|
124
|
+
2. It is a SIP-010 token transfer call
|
|
125
|
+
3. The sender, recipient, amount, and contract match the requirements
|
|
126
|
+
4. The transaction has not been previously used (replay protection)
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
payload: Payment payload containing transaction proof
|
|
130
|
+
requirements: Payment requirements to verify against
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
VerifyResponse indicating validity and payer address
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
# Extract data
|
|
137
|
+
payload_data = self._extract_payload(payload)
|
|
138
|
+
req_data = self._extract_requirements(requirements)
|
|
139
|
+
|
|
140
|
+
# Parse the payload
|
|
141
|
+
exact_payload = ExactDirectPayload.from_dict(payload_data)
|
|
142
|
+
|
|
143
|
+
# Extract requirements
|
|
144
|
+
network = req_data.get("network", "")
|
|
145
|
+
required_amount = req_data.get("amount", "0")
|
|
146
|
+
pay_to = req_data.get("payTo", req_data.get("pay_to", ""))
|
|
147
|
+
asset = req_data.get("asset", "")
|
|
148
|
+
|
|
149
|
+
# Validate network
|
|
150
|
+
if not is_stacks_network(network):
|
|
151
|
+
return VerifyResponse(
|
|
152
|
+
is_valid=False,
|
|
153
|
+
invalid_reason=f"Unsupported network: {network}",
|
|
154
|
+
payer=exact_payload.from_address or None,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Validate transaction ID
|
|
158
|
+
if not exact_payload.tx_id:
|
|
159
|
+
return VerifyResponse(
|
|
160
|
+
is_valid=False,
|
|
161
|
+
invalid_reason="Missing transaction ID in payload",
|
|
162
|
+
payer=exact_payload.from_address or None,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if not is_valid_tx_id(exact_payload.tx_id):
|
|
166
|
+
return VerifyResponse(
|
|
167
|
+
is_valid=False,
|
|
168
|
+
invalid_reason=f"Invalid transaction ID format: {exact_payload.tx_id}",
|
|
169
|
+
payer=exact_payload.from_address or None,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Replay protection: check if txId was already used
|
|
173
|
+
if exact_payload.tx_id in self._used_tx_ids:
|
|
174
|
+
return VerifyResponse(
|
|
175
|
+
is_valid=False,
|
|
176
|
+
invalid_reason=f"Transaction ID already used: {exact_payload.tx_id}",
|
|
177
|
+
payer=exact_payload.from_address or None,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Query the transaction on-chain
|
|
181
|
+
tx_data = await self._signer.query_transaction(exact_payload.tx_id)
|
|
182
|
+
|
|
183
|
+
if not tx_data:
|
|
184
|
+
return VerifyResponse(
|
|
185
|
+
is_valid=False,
|
|
186
|
+
invalid_reason="Transaction not found on-chain",
|
|
187
|
+
payer=exact_payload.from_address or None,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Parse the transaction result
|
|
191
|
+
tx_result = self._parse_transaction_data(tx_data)
|
|
192
|
+
|
|
193
|
+
# Check success
|
|
194
|
+
if tx_result.tx_status != "success":
|
|
195
|
+
return VerifyResponse(
|
|
196
|
+
is_valid=False,
|
|
197
|
+
invalid_reason=f"Transaction not successful, status: {tx_result.tx_status}",
|
|
198
|
+
payer=tx_result.sender_address or None,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Extract transfer details
|
|
202
|
+
transfer = extract_token_transfer(tx_result)
|
|
203
|
+
if transfer is None:
|
|
204
|
+
return VerifyResponse(
|
|
205
|
+
is_valid=False,
|
|
206
|
+
invalid_reason="Transaction is not a valid SIP-010 token transfer",
|
|
207
|
+
payer=tx_result.sender_address or None,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Validate recipient matches payTo
|
|
211
|
+
if pay_to and transfer.to_address != pay_to:
|
|
212
|
+
return VerifyResponse(
|
|
213
|
+
is_valid=False,
|
|
214
|
+
invalid_reason=(
|
|
215
|
+
f"Transfer recipient {transfer.to_address} does not match "
|
|
216
|
+
f"required payTo {pay_to}"
|
|
217
|
+
),
|
|
218
|
+
payer=transfer.from_address or None,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Validate amount >= required
|
|
222
|
+
try:
|
|
223
|
+
transfer_amount = int(transfer.amount)
|
|
224
|
+
req_amount = int(required_amount)
|
|
225
|
+
except (ValueError, TypeError):
|
|
226
|
+
return VerifyResponse(
|
|
227
|
+
is_valid=False,
|
|
228
|
+
invalid_reason="Invalid amount format in transfer or requirements",
|
|
229
|
+
payer=transfer.from_address or None,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if transfer_amount < req_amount:
|
|
233
|
+
return VerifyResponse(
|
|
234
|
+
is_valid=False,
|
|
235
|
+
invalid_reason=(
|
|
236
|
+
f"Transfer amount {transfer_amount} is less than "
|
|
237
|
+
f"required amount {req_amount}"
|
|
238
|
+
),
|
|
239
|
+
payer=transfer.from_address or None,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Validate contract address if specified in requirements
|
|
243
|
+
expected_contract = self._resolve_expected_contract(asset, req_data)
|
|
244
|
+
if expected_contract and transfer.contract_address != expected_contract:
|
|
245
|
+
return VerifyResponse(
|
|
246
|
+
is_valid=False,
|
|
247
|
+
invalid_reason=(
|
|
248
|
+
f"Transfer contract {transfer.contract_address} does not match "
|
|
249
|
+
f"expected contract {expected_contract}"
|
|
250
|
+
),
|
|
251
|
+
payer=transfer.from_address or None,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# All checks passed - mark txId as used for replay protection
|
|
255
|
+
self._used_tx_ids.add(exact_payload.tx_id)
|
|
256
|
+
|
|
257
|
+
return VerifyResponse(
|
|
258
|
+
is_valid=True,
|
|
259
|
+
invalid_reason=None,
|
|
260
|
+
payer=transfer.from_address,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.error(f"Stacks verification failed: {e}")
|
|
265
|
+
return VerifyResponse(
|
|
266
|
+
is_valid=False,
|
|
267
|
+
invalid_reason=f"Verification error: {str(e)}",
|
|
268
|
+
payer=None,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
async def settle(
|
|
272
|
+
self,
|
|
273
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
274
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
275
|
+
) -> SettleResponse:
|
|
276
|
+
"""Settle a Stacks exact-direct payment.
|
|
277
|
+
|
|
278
|
+
For exact-direct payments, the transfer has already been executed
|
|
279
|
+
on-chain by the client. Settlement simply confirms the transfer
|
|
280
|
+
and returns the transaction ID as the settlement proof.
|
|
281
|
+
|
|
282
|
+
This method first verifies the payment, then returns the
|
|
283
|
+
transaction ID as the settlement proof.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
payload: The verified payment payload
|
|
287
|
+
requirements: The payment requirements
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
SettleResponse with the transaction ID and status
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
# Extract data
|
|
294
|
+
payload_data = self._extract_payload(payload)
|
|
295
|
+
req_data = self._extract_requirements(requirements)
|
|
296
|
+
|
|
297
|
+
network = req_data.get("network", "")
|
|
298
|
+
|
|
299
|
+
# First verify the payment
|
|
300
|
+
verify_result = await self.verify(payload, requirements)
|
|
301
|
+
|
|
302
|
+
if not verify_result.is_valid:
|
|
303
|
+
return SettleResponse(
|
|
304
|
+
success=False,
|
|
305
|
+
error_reason=verify_result.invalid_reason,
|
|
306
|
+
transaction=None,
|
|
307
|
+
network=network,
|
|
308
|
+
payer=verify_result.payer,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Payment already settled on-chain, return the transaction ID
|
|
312
|
+
exact_payload = ExactDirectPayload.from_dict(payload_data)
|
|
313
|
+
|
|
314
|
+
return SettleResponse(
|
|
315
|
+
success=True,
|
|
316
|
+
error_reason=None,
|
|
317
|
+
transaction=exact_payload.tx_id,
|
|
318
|
+
network=network,
|
|
319
|
+
payer=verify_result.payer,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
logger.error(f"Stacks settlement failed: {e}")
|
|
324
|
+
return SettleResponse(
|
|
325
|
+
success=False,
|
|
326
|
+
error_reason=f"Settlement error: {str(e)}",
|
|
327
|
+
transaction=None,
|
|
328
|
+
network=req_data.get("network") if "req_data" in dir() else None,
|
|
329
|
+
payer=None,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def _extract_payload(
|
|
333
|
+
self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
|
|
334
|
+
) -> Dict[str, Any]:
|
|
335
|
+
"""Extract payload data as a dict.
|
|
336
|
+
|
|
337
|
+
Handles both PaymentPayloadV2 models and plain dicts.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
payload: Payment payload (model or dict)
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Dict containing the inner payload data
|
|
344
|
+
"""
|
|
345
|
+
if hasattr(payload, "model_dump"):
|
|
346
|
+
data = payload.model_dump(by_alias=True)
|
|
347
|
+
return data.get("payload", data)
|
|
348
|
+
elif isinstance(payload, dict):
|
|
349
|
+
return payload.get("payload", payload)
|
|
350
|
+
return dict(payload)
|
|
351
|
+
|
|
352
|
+
def _extract_requirements(
|
|
353
|
+
self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
|
|
354
|
+
) -> Dict[str, Any]:
|
|
355
|
+
"""Extract requirements data as a dict.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
requirements: Payment requirements (model or dict)
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Dict containing requirement fields
|
|
362
|
+
"""
|
|
363
|
+
if hasattr(requirements, "model_dump"):
|
|
364
|
+
return requirements.model_dump(by_alias=True)
|
|
365
|
+
return dict(requirements)
|
|
366
|
+
|
|
367
|
+
def _parse_transaction_data(self, data: Dict[str, Any]) -> TransactionResult:
|
|
368
|
+
"""Parse raw transaction query data into a TransactionResult.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
data: Raw dictionary from the Hiro API query
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
TransactionResult instance
|
|
375
|
+
"""
|
|
376
|
+
return TransactionResult(
|
|
377
|
+
tx_id=data.get("tx_id", ""),
|
|
378
|
+
tx_status=data.get("tx_status", ""),
|
|
379
|
+
sender_address=data.get("sender_address", ""),
|
|
380
|
+
contract_call=data.get("contract_call"),
|
|
381
|
+
block_height=int(data.get("block_height", 0)),
|
|
382
|
+
block_hash=data.get("block_hash", ""),
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def _resolve_expected_contract(
|
|
386
|
+
self, asset: str, req_data: Dict[str, Any]
|
|
387
|
+
) -> Optional[str]:
|
|
388
|
+
"""Resolve the expected contract address from requirements.
|
|
389
|
+
|
|
390
|
+
Tries to determine the contract address from:
|
|
391
|
+
1. The CAIP-19 asset identifier
|
|
392
|
+
2. The extra.contractAddress field
|
|
393
|
+
3. The network's default token
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
asset: CAIP-19 asset identifier string
|
|
397
|
+
req_data: Requirements dictionary
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Expected contract address, or None if it cannot be determined
|
|
401
|
+
"""
|
|
402
|
+
# Try CAIP-19 identifier
|
|
403
|
+
if asset:
|
|
404
|
+
parsed = parse_contract_identifier(asset)
|
|
405
|
+
if parsed is not None:
|
|
406
|
+
return parsed
|
|
407
|
+
|
|
408
|
+
# Try extra.contractAddress
|
|
409
|
+
extra = req_data.get("extra", {})
|
|
410
|
+
if extra and "contractAddress" in extra:
|
|
411
|
+
contract_val = extra["contractAddress"]
|
|
412
|
+
if isinstance(contract_val, str) and contract_val:
|
|
413
|
+
return contract_val
|
|
414
|
+
|
|
415
|
+
# Try network default
|
|
416
|
+
network = req_data.get("network", "")
|
|
417
|
+
if network:
|
|
418
|
+
try:
|
|
419
|
+
config = get_network_config(network)
|
|
420
|
+
return config.default_token.contract_address
|
|
421
|
+
except ValueError:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
return None
|