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,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
|