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,235 @@
1
+ """Polkadot Exact-Direct Scheme - Client Implementation.
2
+
3
+ This module provides the client-side implementation of the exact-direct
4
+ payment scheme for Polkadot Asset Hub networks.
5
+
6
+ The client:
7
+ 1. Builds an assets.transfer_keep_alive extrinsic
8
+ 2. Signs and submits it on-chain via the signer
9
+ 3. Returns the extrinsic hash, block hash, and index as payment proof
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from typing import Any, Dict, Optional, Union
16
+
17
+ from t402.types import (
18
+ PaymentRequirementsV2,
19
+ T402_VERSION_V1,
20
+ T402_VERSION_V2,
21
+ )
22
+ from t402.schemes.polkadot.constants import (
23
+ SCHEME_EXACT_DIRECT,
24
+ get_network_config,
25
+ is_polkadot_network,
26
+ )
27
+ from t402.schemes.polkadot.types import (
28
+ ClientPolkadotSigner,
29
+ ExactDirectPayload,
30
+ is_valid_ss58_address,
31
+ parse_asset_identifier,
32
+ )
33
+
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class ExactDirectPolkadotClientScheme:
39
+ """Client scheme for Polkadot exact-direct payments.
40
+
41
+ Executes on-chain asset transfers and returns the transaction proof
42
+ as a payment payload.
43
+
44
+ Example:
45
+ ```python
46
+ scheme = ExactDirectPolkadotClientScheme(signer=my_polkadot_signer)
47
+
48
+ payload = await scheme.create_payment_payload(
49
+ t402_version=2,
50
+ requirements={
51
+ "scheme": "exact-direct",
52
+ "network": "polkadot:68d56f15f85d3136970ec16946040bc1",
53
+ "asset": "polkadot:68d56f15f85d3136970ec16946040bc1/asset:1984",
54
+ "amount": "1000000",
55
+ "payTo": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
56
+ "maxTimeoutSeconds": 300,
57
+ },
58
+ )
59
+ ```
60
+ """
61
+
62
+ scheme = SCHEME_EXACT_DIRECT
63
+ caip_family = "polkadot:*"
64
+
65
+ def __init__(
66
+ self,
67
+ signer: ClientPolkadotSigner,
68
+ rpc_url: Optional[str] = None,
69
+ ):
70
+ """Initialize the Polkadot client scheme.
71
+
72
+ Args:
73
+ signer: Polkadot signer for signing and submitting extrinsics
74
+ rpc_url: Optional RPC endpoint override for the network
75
+ """
76
+ self._signer = signer
77
+ self._rpc_url = rpc_url
78
+
79
+ @property
80
+ def address(self) -> str:
81
+ """Return the signer's SS58 address."""
82
+ return self._signer.address()
83
+
84
+ async def create_payment_payload(
85
+ self,
86
+ t402_version: int,
87
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
88
+ ) -> Dict[str, Any]:
89
+ """Create a payment payload by executing an on-chain transfer.
90
+
91
+ Validates the requirements, builds an assets.transfer_keep_alive
92
+ extrinsic, signs and submits it, then returns the proof.
93
+
94
+ Args:
95
+ t402_version: Protocol version (1 or 2)
96
+ requirements: Payment requirements specifying the transfer details
97
+
98
+ Returns:
99
+ Payment payload dictionary with extrinsic proof
100
+
101
+ Raises:
102
+ ValueError: If requirements are invalid (bad network, address, amount, etc.)
103
+ Exception: If signing or submission fails
104
+ """
105
+ # Convert to dict for easier access
106
+ if hasattr(requirements, "model_dump"):
107
+ req = requirements.model_dump(by_alias=True)
108
+ else:
109
+ req = dict(requirements)
110
+
111
+ # Extract fields
112
+ network = req.get("network", "")
113
+ asset = req.get("asset", "")
114
+ amount = req.get("amount", "0")
115
+ pay_to = req.get("payTo", "")
116
+ extra = req.get("extra", {})
117
+
118
+ # Validate network
119
+ if not is_polkadot_network(network):
120
+ raise ValueError(f"Unsupported network: {network}")
121
+
122
+ network_config = get_network_config(network)
123
+
124
+ # Validate payTo address
125
+ if not pay_to:
126
+ raise ValueError("payTo address is required")
127
+ if not is_valid_ss58_address(pay_to):
128
+ raise ValueError(f"Invalid payTo address: {pay_to}")
129
+
130
+ # Validate amount
131
+ if not amount:
132
+ raise ValueError("Amount is required")
133
+ try:
134
+ amount_int = int(amount)
135
+ except (ValueError, TypeError):
136
+ raise ValueError(f"Invalid amount format: {amount}")
137
+ if amount_int <= 0:
138
+ raise ValueError(f"Amount must be positive: {amount}")
139
+
140
+ # Resolve asset ID
141
+ asset_id = self._resolve_asset_id(asset, extra, network_config)
142
+
143
+ # Get sender address
144
+ from_address = self._signer.address()
145
+ if not from_address:
146
+ raise ValueError("Signer address is empty")
147
+
148
+ # Build the extrinsic call (assets.transfer_keep_alive)
149
+ call = {
150
+ "assetId": asset_id,
151
+ "target": pay_to,
152
+ "amount": amount,
153
+ }
154
+
155
+ # Sign and submit the extrinsic
156
+ result = await self._signer.sign_and_submit(call, network)
157
+
158
+ # Validate result
159
+ extrinsic_hash = result.get("extrinsicHash", "")
160
+ block_hash = result.get("blockHash", "")
161
+ extrinsic_index = result.get("extrinsicIndex", 0)
162
+
163
+ if not extrinsic_hash and not block_hash:
164
+ raise ValueError(
165
+ "Extrinsic result missing both extrinsic hash and block hash"
166
+ )
167
+
168
+ # Build the payload
169
+ payload = ExactDirectPayload(
170
+ extrinsic_hash=extrinsic_hash,
171
+ block_hash=block_hash,
172
+ extrinsic_index=extrinsic_index,
173
+ from_address=from_address,
174
+ to_address=pay_to,
175
+ amount=amount,
176
+ asset_id=asset_id,
177
+ )
178
+
179
+ if t402_version == T402_VERSION_V1:
180
+ return {
181
+ "t402Version": T402_VERSION_V1,
182
+ "scheme": self.scheme,
183
+ "network": network,
184
+ "payload": payload.to_dict(),
185
+ }
186
+
187
+ # V2 format
188
+ return {
189
+ "t402Version": T402_VERSION_V2,
190
+ "payload": payload.to_dict(),
191
+ }
192
+
193
+ def _resolve_asset_id(
194
+ self,
195
+ asset: str,
196
+ extra: Dict[str, Any],
197
+ network_config: Any,
198
+ ) -> int:
199
+ """Resolve the asset ID from requirements fields.
200
+
201
+ Tries to determine the asset ID from:
202
+ 1. The extra.assetId field
203
+ 2. The CAIP-19 asset identifier
204
+ 3. The network's default token
205
+
206
+ Args:
207
+ asset: CAIP-19 asset identifier string
208
+ extra: Extra metadata from requirements
209
+ network_config: Network configuration
210
+
211
+ Returns:
212
+ Resolved asset ID
213
+
214
+ Raises:
215
+ ValueError: If asset ID cannot be determined
216
+ """
217
+ # Try extra.assetId first
218
+ if extra and "assetId" in extra:
219
+ asset_id_val = extra["assetId"]
220
+ if isinstance(asset_id_val, (int, float)):
221
+ return int(asset_id_val)
222
+ if isinstance(asset_id_val, str):
223
+ try:
224
+ return int(asset_id_val)
225
+ except ValueError:
226
+ pass
227
+
228
+ # Try parsing CAIP-19 asset identifier
229
+ if asset:
230
+ parsed_id = parse_asset_identifier(asset)
231
+ if parsed_id is not None:
232
+ return parsed_id
233
+
234
+ # Fall back to network default
235
+ return network_config.default_token.asset_id
@@ -0,0 +1,428 @@
1
+ """Polkadot Exact-Direct Scheme - Facilitator Implementation.
2
+
3
+ This module provides the facilitator-side implementation of the exact-direct
4
+ payment scheme for Polkadot Asset Hub networks.
5
+
6
+ The facilitator:
7
+ 1. Verifies payment payloads by querying the extrinsic on-chain
8
+ 2. Validates that the extrinsic is a successful asset transfer matching
9
+ the payment requirements (sender, recipient, amount, asset ID)
10
+ 3. For settle(), confirms the transfer has already occurred on-chain
11
+ (since exact-direct payments are pre-paid by the client)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from typing import Any, Dict, List, Optional, Union
18
+
19
+ from t402.types import (
20
+ PaymentRequirementsV2,
21
+ PaymentPayloadV2,
22
+ VerifyResponse,
23
+ SettleResponse,
24
+ Network,
25
+ )
26
+ from t402.schemes.polkadot.constants import (
27
+ SCHEME_EXACT_DIRECT,
28
+ get_network_config,
29
+ is_polkadot_network,
30
+ )
31
+ from t402.schemes.polkadot.types import (
32
+ FacilitatorPolkadotSigner,
33
+ ExactDirectPayload,
34
+ ExtrinsicResult,
35
+ is_valid_hash,
36
+ extract_asset_transfer,
37
+ parse_asset_identifier,
38
+ )
39
+
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class ExactDirectPolkadotFacilitatorScheme:
45
+ """Facilitator scheme for Polkadot exact-direct payments.
46
+
47
+ Verifies on-chain asset transfers by querying the extrinsic
48
+ via an indexer or RPC, and confirms the transfer matches the
49
+ payment requirements.
50
+
51
+ Example:
52
+ ```python
53
+ facilitator = ExactDirectPolkadotFacilitatorScheme(
54
+ signer=my_polkadot_facilitator_signer,
55
+ addresses={
56
+ "polkadot:68d56f15f85d3136970ec16946040bc1": [
57
+ "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
58
+ ],
59
+ },
60
+ )
61
+
62
+ # Verify a payment
63
+ result = await facilitator.verify(payload, requirements)
64
+ if result.is_valid:
65
+ # Payment is confirmed on-chain
66
+ settlement = await facilitator.settle(payload, requirements)
67
+ ```
68
+ """
69
+
70
+ scheme = SCHEME_EXACT_DIRECT
71
+ caip_family = "polkadot:*"
72
+
73
+ def __init__(
74
+ self,
75
+ signer: FacilitatorPolkadotSigner,
76
+ addresses: Optional[Dict[str, List[str]]] = None,
77
+ ):
78
+ """Initialize the facilitator.
79
+
80
+ Args:
81
+ signer: Polkadot facilitator signer for querying extrinsics
82
+ addresses: Mapping of network -> list of facilitator addresses.
83
+ Used in the /supported response.
84
+ """
85
+ self._signer = signer
86
+ self._addresses = addresses or {}
87
+
88
+ def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
89
+ """Get mechanism-specific extra data for supported kinds.
90
+
91
+ Returns asset metadata for the network's default token.
92
+
93
+ Args:
94
+ network: The network identifier
95
+
96
+ Returns:
97
+ Dict with asset metadata, or None if network is unsupported
98
+ """
99
+ try:
100
+ config = get_network_config(network)
101
+ except ValueError:
102
+ return None
103
+
104
+ return {
105
+ "assetId": config.default_token.asset_id,
106
+ "assetSymbol": config.default_token.symbol,
107
+ "assetDecimals": config.default_token.decimals,
108
+ "networkName": config.name,
109
+ }
110
+
111
+ def get_signers(self, network: Network) -> List[str]:
112
+ """Get signer addresses for this facilitator on a given network.
113
+
114
+ Args:
115
+ network: The network identifier
116
+
117
+ Returns:
118
+ List of facilitator SS58 addresses for the network
119
+ """
120
+ return self._addresses.get(network, [])
121
+
122
+ async def verify(
123
+ self,
124
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
125
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
126
+ ) -> VerifyResponse:
127
+ """Verify a Polkadot exact-direct payment payload.
128
+
129
+ Queries the extrinsic on-chain and validates:
130
+ 1. The extrinsic exists and was successful
131
+ 2. It is an assets.transfer or assets.transfer_keep_alive call
132
+ 3. The sender, recipient, amount, and asset ID match the requirements
133
+
134
+ Args:
135
+ payload: Payment payload containing extrinsic proof
136
+ requirements: Payment requirements to verify against
137
+
138
+ Returns:
139
+ VerifyResponse indicating validity and payer address
140
+ """
141
+ try:
142
+ # Extract data
143
+ payload_data = self._extract_payload(payload)
144
+ req_data = self._extract_requirements(requirements)
145
+
146
+ # Parse the payload
147
+ exact_payload = ExactDirectPayload.from_dict(payload_data)
148
+
149
+ # Extract requirements
150
+ network = req_data.get("network", "")
151
+ required_amount = req_data.get("amount", "0")
152
+ pay_to = req_data.get("payTo", req_data.get("pay_to", ""))
153
+ asset = req_data.get("asset", "")
154
+
155
+ # Validate network
156
+ if not is_polkadot_network(network):
157
+ return VerifyResponse(
158
+ is_valid=False,
159
+ invalid_reason=f"Unsupported network: {network}",
160
+ payer=exact_payload.from_address or None,
161
+ )
162
+
163
+ # Validate extrinsic hash
164
+ if not exact_payload.extrinsic_hash:
165
+ return VerifyResponse(
166
+ is_valid=False,
167
+ invalid_reason="Missing extrinsic hash in payload",
168
+ payer=exact_payload.from_address or None,
169
+ )
170
+
171
+ if not is_valid_hash(exact_payload.extrinsic_hash):
172
+ return VerifyResponse(
173
+ is_valid=False,
174
+ invalid_reason=f"Invalid extrinsic hash format: {exact_payload.extrinsic_hash}",
175
+ payer=exact_payload.from_address or None,
176
+ )
177
+
178
+ # Query the extrinsic on-chain
179
+ extrinsic_data = await self._signer.get_extrinsic(
180
+ exact_payload.extrinsic_hash, network
181
+ )
182
+
183
+ if not extrinsic_data:
184
+ return VerifyResponse(
185
+ is_valid=False,
186
+ invalid_reason="Extrinsic not found on-chain",
187
+ payer=exact_payload.from_address or None,
188
+ )
189
+
190
+ # Parse the extrinsic result
191
+ extrinsic_result = self._parse_extrinsic_data(extrinsic_data)
192
+
193
+ # Check success
194
+ if not extrinsic_result.success:
195
+ return VerifyResponse(
196
+ is_valid=False,
197
+ invalid_reason="Extrinsic failed on-chain",
198
+ payer=extrinsic_result.signer or None,
199
+ )
200
+
201
+ # Extract transfer details
202
+ transfer = extract_asset_transfer(extrinsic_result)
203
+ if transfer is None:
204
+ return VerifyResponse(
205
+ is_valid=False,
206
+ invalid_reason="Extrinsic is not a valid asset transfer",
207
+ payer=extrinsic_result.signer 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 asset ID if specified in requirements
243
+ expected_asset_id = self._resolve_expected_asset_id(asset, req_data)
244
+ if expected_asset_id is not None and transfer.asset_id != expected_asset_id:
245
+ return VerifyResponse(
246
+ is_valid=False,
247
+ invalid_reason=(
248
+ f"Transfer asset ID {transfer.asset_id} does not match "
249
+ f"expected asset ID {expected_asset_id}"
250
+ ),
251
+ payer=transfer.from_address or None,
252
+ )
253
+
254
+ # All checks passed
255
+ return VerifyResponse(
256
+ is_valid=True,
257
+ invalid_reason=None,
258
+ payer=transfer.from_address,
259
+ )
260
+
261
+ except Exception as e:
262
+ logger.error(f"Polkadot verification failed: {e}")
263
+ return VerifyResponse(
264
+ is_valid=False,
265
+ invalid_reason=f"Verification error: {str(e)}",
266
+ payer=None,
267
+ )
268
+
269
+ async def settle(
270
+ self,
271
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
272
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
273
+ ) -> SettleResponse:
274
+ """Settle a Polkadot exact-direct payment.
275
+
276
+ For exact-direct payments, the transfer has already been executed
277
+ on-chain by the client. Settlement simply confirms the transfer
278
+ and returns the extrinsic hash as the transaction identifier.
279
+
280
+ This method first verifies the payment, then returns the
281
+ extrinsic hash as the settlement proof.
282
+
283
+ Args:
284
+ payload: The verified payment payload
285
+ requirements: The payment requirements
286
+
287
+ Returns:
288
+ SettleResponse with the extrinsic hash and status
289
+ """
290
+ try:
291
+ # Extract data
292
+ payload_data = self._extract_payload(payload)
293
+ req_data = self._extract_requirements(requirements)
294
+
295
+ network = req_data.get("network", "")
296
+
297
+ # First verify the payment
298
+ verify_result = await self.verify(payload, requirements)
299
+
300
+ if not verify_result.is_valid:
301
+ return SettleResponse(
302
+ success=False,
303
+ error_reason=verify_result.invalid_reason,
304
+ transaction=None,
305
+ network=network,
306
+ payer=verify_result.payer,
307
+ )
308
+
309
+ # Payment already settled on-chain, return the extrinsic hash
310
+ exact_payload = ExactDirectPayload.from_dict(payload_data)
311
+
312
+ return SettleResponse(
313
+ success=True,
314
+ error_reason=None,
315
+ transaction=exact_payload.extrinsic_hash,
316
+ network=network,
317
+ payer=verify_result.payer,
318
+ )
319
+
320
+ except Exception as e:
321
+ logger.error(f"Polkadot settlement failed: {e}")
322
+ return SettleResponse(
323
+ success=False,
324
+ error_reason=f"Settlement error: {str(e)}",
325
+ transaction=None,
326
+ network=req_data.get("network") if "req_data" in dir() else None,
327
+ payer=None,
328
+ )
329
+
330
+ def _extract_payload(
331
+ self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
332
+ ) -> Dict[str, Any]:
333
+ """Extract payload data as a dict.
334
+
335
+ Handles both PaymentPayloadV2 models and plain dicts.
336
+
337
+ Args:
338
+ payload: Payment payload (model or dict)
339
+
340
+ Returns:
341
+ Dict containing the inner payload data
342
+ """
343
+ if hasattr(payload, "model_dump"):
344
+ data = payload.model_dump(by_alias=True)
345
+ return data.get("payload", data)
346
+ elif isinstance(payload, dict):
347
+ return payload.get("payload", payload)
348
+ return dict(payload)
349
+
350
+ def _extract_requirements(
351
+ self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
352
+ ) -> Dict[str, Any]:
353
+ """Extract requirements data as a dict.
354
+
355
+ Args:
356
+ requirements: Payment requirements (model or dict)
357
+
358
+ Returns:
359
+ Dict containing requirement fields
360
+ """
361
+ if hasattr(requirements, "model_dump"):
362
+ return requirements.model_dump(by_alias=True)
363
+ return dict(requirements)
364
+
365
+ def _parse_extrinsic_data(self, data: Dict[str, Any]) -> ExtrinsicResult:
366
+ """Parse raw extrinsic query data into an ExtrinsicResult.
367
+
368
+ Args:
369
+ data: Raw dictionary from the indexer/RPC query
370
+
371
+ Returns:
372
+ ExtrinsicResult instance
373
+ """
374
+ return ExtrinsicResult(
375
+ extrinsic_hash=data.get("extrinsic_hash", data.get("extrinsicHash", "")),
376
+ block_hash=data.get("block_hash", data.get("blockHash", "")),
377
+ block_number=int(data.get("block_num", data.get("blockNumber", 0))),
378
+ extrinsic_index=int(
379
+ data.get("extrinsic_index", data.get("extrinsicIndex", 0))
380
+ ),
381
+ success=bool(data.get("success", False)),
382
+ signer=data.get("account_id", data.get("signer", "")),
383
+ module=data.get("call_module", data.get("module", "")),
384
+ call=data.get("call_module_function", data.get("call", "")),
385
+ params=data.get("params", []),
386
+ )
387
+
388
+ def _resolve_expected_asset_id(
389
+ self, asset: str, req_data: Dict[str, Any]
390
+ ) -> Optional[int]:
391
+ """Resolve the expected asset ID from requirements.
392
+
393
+ Tries to determine the asset ID from:
394
+ 1. The CAIP-19 asset identifier
395
+ 2. The extra.assetId field
396
+ 3. The network's default token
397
+
398
+ Args:
399
+ asset: CAIP-19 asset identifier string
400
+ req_data: Requirements dictionary
401
+
402
+ Returns:
403
+ Expected asset ID, or None if it cannot be determined
404
+ """
405
+ # Try CAIP-19 identifier
406
+ if asset:
407
+ parsed = parse_asset_identifier(asset)
408
+ if parsed is not None:
409
+ return parsed
410
+
411
+ # Try extra.assetId
412
+ extra = req_data.get("extra", {})
413
+ if extra and "assetId" in extra:
414
+ try:
415
+ return int(extra["assetId"])
416
+ except (ValueError, TypeError):
417
+ pass
418
+
419
+ # Try network default
420
+ network = req_data.get("network", "")
421
+ if network:
422
+ try:
423
+ config = get_network_config(network)
424
+ return config.default_token.asset_id
425
+ except ValueError:
426
+ pass
427
+
428
+ return None