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,292 @@
|
|
|
1
|
+
"""Polkadot Exact-Direct Scheme - Server Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the server-side implementation of the exact-direct
|
|
4
|
+
payment scheme for Polkadot Asset Hub networks.
|
|
5
|
+
|
|
6
|
+
The server:
|
|
7
|
+
1. Parses user-friendly prices (e.g., "$0.10") into atomic amounts
|
|
8
|
+
2. Enhances payment requirements with Polkadot-specific metadata
|
|
9
|
+
(asset ID, decimals, network name, CAIP-19 asset identifier)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any, Dict, List, Optional, Union
|
|
15
|
+
|
|
16
|
+
from t402.types import (
|
|
17
|
+
PaymentRequirementsV2,
|
|
18
|
+
Network,
|
|
19
|
+
)
|
|
20
|
+
from t402.schemes.interfaces import AssetAmount, SupportedKindDict
|
|
21
|
+
from t402.schemes.polkadot.constants import (
|
|
22
|
+
SCHEME_EXACT_DIRECT,
|
|
23
|
+
NetworkConfig,
|
|
24
|
+
TokenInfo,
|
|
25
|
+
get_network_config,
|
|
26
|
+
is_polkadot_network,
|
|
27
|
+
)
|
|
28
|
+
from t402.schemes.polkadot.types import create_asset_identifier
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ExactDirectPolkadotServerScheme:
|
|
32
|
+
"""Server scheme for Polkadot exact-direct payments.
|
|
33
|
+
|
|
34
|
+
Handles parsing user-friendly prices into atomic amounts and
|
|
35
|
+
enhancing payment requirements with Polkadot-specific metadata.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
```python
|
|
39
|
+
scheme = ExactDirectPolkadotServerScheme()
|
|
40
|
+
|
|
41
|
+
# Parse price to atomic units
|
|
42
|
+
asset_amount = await scheme.parse_price("$0.10", "polkadot:68d56f15f85d3136970ec16946040bc1")
|
|
43
|
+
# Returns: {"amount": "100000", "asset": "polkadot:.../asset:1984", "extra": {...}}
|
|
44
|
+
|
|
45
|
+
# Enhance requirements with metadata
|
|
46
|
+
enhanced = await scheme.enhance_requirements(
|
|
47
|
+
requirements,
|
|
48
|
+
supported_kind,
|
|
49
|
+
facilitator_extensions,
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
scheme = SCHEME_EXACT_DIRECT
|
|
55
|
+
caip_family = "polkadot:*"
|
|
56
|
+
|
|
57
|
+
def __init__(self, preferred_token: Optional[str] = None):
|
|
58
|
+
"""Initialize the Polkadot server scheme.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
preferred_token: Override the default token symbol (e.g., "USDT")
|
|
62
|
+
"""
|
|
63
|
+
self._preferred_token = preferred_token
|
|
64
|
+
|
|
65
|
+
async def parse_price(
|
|
66
|
+
self,
|
|
67
|
+
price: Union[str, int, float, Dict[str, Any]],
|
|
68
|
+
network: Network,
|
|
69
|
+
) -> AssetAmount:
|
|
70
|
+
"""Parse a user-friendly price to atomic amount and asset.
|
|
71
|
+
|
|
72
|
+
Supports:
|
|
73
|
+
- String with $ prefix: "$0.10" -> 100000 (6 decimals)
|
|
74
|
+
- String without prefix: "0.10" -> 100000
|
|
75
|
+
- Integer/float: 0.10 -> 100000
|
|
76
|
+
- Dict (pre-parsed): {"amount": "100000", "asset": "..."}
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
price: User-friendly price value
|
|
80
|
+
network: CAIP-2 network identifier
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
AssetAmount dict with amount, asset, and extra metadata
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ValueError: If the network is unsupported or price is invalid
|
|
87
|
+
"""
|
|
88
|
+
# Validate network
|
|
89
|
+
if not is_polkadot_network(network):
|
|
90
|
+
raise ValueError(f"Invalid Polkadot network: {network}")
|
|
91
|
+
|
|
92
|
+
network_config = get_network_config(network)
|
|
93
|
+
|
|
94
|
+
# Handle dict (already in pre-parsed format)
|
|
95
|
+
if isinstance(price, dict):
|
|
96
|
+
amount_str = str(price.get("amount", "0"))
|
|
97
|
+
asset = price.get("asset", "")
|
|
98
|
+
if not asset:
|
|
99
|
+
asset = create_asset_identifier(
|
|
100
|
+
network, network_config.default_token.asset_id
|
|
101
|
+
)
|
|
102
|
+
extra = price.get("extra", {})
|
|
103
|
+
return {
|
|
104
|
+
"amount": amount_str,
|
|
105
|
+
"asset": asset,
|
|
106
|
+
"extra": extra,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Get default token for this network
|
|
110
|
+
token = self._get_token(network, network_config)
|
|
111
|
+
|
|
112
|
+
# Parse price string/number to decimal
|
|
113
|
+
decimal_amount = self._parse_money_to_decimal(price)
|
|
114
|
+
|
|
115
|
+
# Convert to atomic units
|
|
116
|
+
atomic_amount = self._to_atomic_units(decimal_amount, token.decimals)
|
|
117
|
+
|
|
118
|
+
# Build asset identifier
|
|
119
|
+
asset_identifier = create_asset_identifier(network, token.asset_id)
|
|
120
|
+
|
|
121
|
+
# Build extra metadata
|
|
122
|
+
extra = {
|
|
123
|
+
"symbol": token.symbol,
|
|
124
|
+
"name": token.name,
|
|
125
|
+
"decimals": token.decimals,
|
|
126
|
+
"assetId": token.asset_id,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"amount": str(atomic_amount),
|
|
131
|
+
"asset": asset_identifier,
|
|
132
|
+
"extra": extra,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async def enhance_requirements(
|
|
136
|
+
self,
|
|
137
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
138
|
+
supported_kind: SupportedKindDict,
|
|
139
|
+
facilitator_extensions: List[str],
|
|
140
|
+
) -> Union[PaymentRequirementsV2, Dict[str, Any]]:
|
|
141
|
+
"""Enhance payment requirements with Polkadot-specific metadata.
|
|
142
|
+
|
|
143
|
+
Adds asset metadata (ID, symbol, decimals, network name) and
|
|
144
|
+
the CAIP-19 asset identifier to the requirements.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
requirements: Base payment requirements
|
|
148
|
+
supported_kind: Matched SupportedKind from facilitator
|
|
149
|
+
facilitator_extensions: Extensions supported by facilitator
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Enhanced requirements with Polkadot metadata in extra
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
ValueError: If the network is not recognized
|
|
156
|
+
"""
|
|
157
|
+
# Convert to dict for modification
|
|
158
|
+
if hasattr(requirements, "model_dump"):
|
|
159
|
+
req = requirements.model_dump(by_alias=True)
|
|
160
|
+
else:
|
|
161
|
+
req = dict(requirements)
|
|
162
|
+
|
|
163
|
+
network = req.get("network", "")
|
|
164
|
+
|
|
165
|
+
# Get network config
|
|
166
|
+
network_config = get_network_config(network)
|
|
167
|
+
|
|
168
|
+
# Get token info
|
|
169
|
+
token = self._get_token(network, network_config)
|
|
170
|
+
|
|
171
|
+
# Set asset identifier if not already set
|
|
172
|
+
if not req.get("asset"):
|
|
173
|
+
req["asset"] = create_asset_identifier(network, token.asset_id)
|
|
174
|
+
|
|
175
|
+
# Ensure amount is in atomic units (no decimals)
|
|
176
|
+
amount = req.get("amount", "")
|
|
177
|
+
if amount and "." in amount:
|
|
178
|
+
atomic = self._parse_amount_string(amount, token.decimals)
|
|
179
|
+
req["amount"] = str(atomic)
|
|
180
|
+
|
|
181
|
+
# Ensure extra exists
|
|
182
|
+
if "extra" not in req or req["extra"] is None:
|
|
183
|
+
req["extra"] = {}
|
|
184
|
+
|
|
185
|
+
# Add asset metadata
|
|
186
|
+
req["extra"]["assetId"] = token.asset_id
|
|
187
|
+
req["extra"]["assetSymbol"] = token.symbol
|
|
188
|
+
req["extra"]["assetDecimals"] = token.decimals
|
|
189
|
+
req["extra"]["networkName"] = network_config.name
|
|
190
|
+
|
|
191
|
+
# Add facilitator-provided extra fields from supportedKind
|
|
192
|
+
if supported_kind.get("extra"):
|
|
193
|
+
for key in ("assetId", "assetSymbol", "assetDecimals", "networkName"):
|
|
194
|
+
if key in supported_kind["extra"]:
|
|
195
|
+
req["extra"][key] = supported_kind["extra"][key]
|
|
196
|
+
|
|
197
|
+
# Copy extension keys from supportedKind
|
|
198
|
+
if supported_kind.get("extra"):
|
|
199
|
+
for key in facilitator_extensions:
|
|
200
|
+
if key in supported_kind["extra"]:
|
|
201
|
+
req["extra"][key] = supported_kind["extra"][key]
|
|
202
|
+
|
|
203
|
+
return req
|
|
204
|
+
|
|
205
|
+
def _get_token(self, network: str, network_config: NetworkConfig) -> TokenInfo:
|
|
206
|
+
"""Get the token to use for the given network.
|
|
207
|
+
|
|
208
|
+
Uses the preferred token if configured and available,
|
|
209
|
+
otherwise falls back to the network's default token.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
network: CAIP-2 network identifier
|
|
213
|
+
network_config: Network configuration
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
TokenInfo for the selected token
|
|
217
|
+
"""
|
|
218
|
+
if self._preferred_token:
|
|
219
|
+
if network_config.default_token.symbol == self._preferred_token:
|
|
220
|
+
return network_config.default_token
|
|
221
|
+
return network_config.default_token
|
|
222
|
+
|
|
223
|
+
def _parse_money_to_decimal(self, price: Union[str, int, float]) -> float:
|
|
224
|
+
"""Parse a money value to a decimal float.
|
|
225
|
+
|
|
226
|
+
Handles:
|
|
227
|
+
- "$0.10" -> 0.10
|
|
228
|
+
- "0.10" -> 0.10
|
|
229
|
+
- 0.10 -> 0.10
|
|
230
|
+
- "1.50 USDT" -> 1.50
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
price: Price value to parse
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Decimal amount as float
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
ValueError: If the price format is invalid
|
|
240
|
+
"""
|
|
241
|
+
if isinstance(price, (int, float)):
|
|
242
|
+
return float(price)
|
|
243
|
+
|
|
244
|
+
if isinstance(price, str):
|
|
245
|
+
clean = price.strip()
|
|
246
|
+
clean = clean.lstrip("$").strip()
|
|
247
|
+
|
|
248
|
+
# Take only the numeric part (first token)
|
|
249
|
+
parts = clean.split()
|
|
250
|
+
if parts:
|
|
251
|
+
try:
|
|
252
|
+
return float(parts[0])
|
|
253
|
+
except ValueError:
|
|
254
|
+
raise ValueError(f"Failed to parse price string: '{price}'")
|
|
255
|
+
|
|
256
|
+
raise ValueError(f"Invalid price format: {price}")
|
|
257
|
+
|
|
258
|
+
def _to_atomic_units(self, amount: float, decimals: int) -> int:
|
|
259
|
+
"""Convert a decimal amount to atomic units.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
amount: Decimal amount (e.g., 1.50)
|
|
263
|
+
decimals: Number of decimal places (e.g., 6)
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Atomic amount as integer (e.g., 1500000)
|
|
267
|
+
"""
|
|
268
|
+
multiplier = 10 ** decimals
|
|
269
|
+
return int(round(amount * multiplier))
|
|
270
|
+
|
|
271
|
+
def _parse_amount_string(self, amount_str: str, decimals: int) -> int:
|
|
272
|
+
"""Parse a decimal amount string to atomic units.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
amount_str: Amount as string (e.g., "1.50")
|
|
276
|
+
decimals: Number of decimal places
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Atomic amount as integer
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
ValueError: If the amount string is invalid
|
|
283
|
+
"""
|
|
284
|
+
try:
|
|
285
|
+
amount = float(amount_str)
|
|
286
|
+
except ValueError:
|
|
287
|
+
raise ValueError(f"Invalid amount: {amount_str}")
|
|
288
|
+
|
|
289
|
+
if amount < 0:
|
|
290
|
+
raise ValueError(f"Amount must be non-negative: {amount_str}")
|
|
291
|
+
|
|
292
|
+
return self._to_atomic_units(amount, decimals)
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Polkadot Scheme Types.
|
|
2
|
+
|
|
3
|
+
This module defines types, payload structures, and validation utilities
|
|
4
|
+
for the Polkadot exact-direct payment scheme.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Dict, Optional, Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# SS58 address regex: base58 characters, typical length 45-50
|
|
15
|
+
SS58_REGEX = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{45,50}$")
|
|
16
|
+
|
|
17
|
+
# Extrinsic/block hash regex: 0x-prefixed 64 hex characters (32 bytes)
|
|
18
|
+
HASH_REGEX = re.compile(r"^0x[a-fA-F0-9]{64}$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ExactDirectPayload:
|
|
23
|
+
"""Payment payload for the exact-direct scheme on Polkadot.
|
|
24
|
+
|
|
25
|
+
Contains the on-chain proof of a completed asset transfer.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
extrinsic_hash: The 0x-prefixed hex hash of the submitted extrinsic
|
|
29
|
+
block_hash: The 0x-prefixed hex hash of the block containing the extrinsic
|
|
30
|
+
extrinsic_index: The index of the extrinsic within the block
|
|
31
|
+
from_address: The SS58-encoded sender address
|
|
32
|
+
to_address: The SS58-encoded recipient address
|
|
33
|
+
amount: The atomic amount transferred (as string)
|
|
34
|
+
asset_id: The on-chain asset ID (e.g., 1984 for USDT)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
extrinsic_hash: str
|
|
38
|
+
block_hash: str
|
|
39
|
+
extrinsic_index: int
|
|
40
|
+
from_address: str
|
|
41
|
+
to_address: str
|
|
42
|
+
amount: str
|
|
43
|
+
asset_id: int
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
"""Convert the payload to a dictionary suitable for JSON serialization.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dictionary with camelCase keys matching the protocol format
|
|
50
|
+
"""
|
|
51
|
+
return {
|
|
52
|
+
"extrinsicHash": self.extrinsic_hash,
|
|
53
|
+
"blockHash": self.block_hash,
|
|
54
|
+
"extrinsicIndex": self.extrinsic_index,
|
|
55
|
+
"from": self.from_address,
|
|
56
|
+
"to": self.to_address,
|
|
57
|
+
"amount": self.amount,
|
|
58
|
+
"assetId": self.asset_id,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ExactDirectPayload":
|
|
63
|
+
"""Create an ExactDirectPayload from a dictionary.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
data: Dictionary with payload fields (camelCase or snake_case)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
ExactDirectPayload instance
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
KeyError: If required fields are missing
|
|
73
|
+
TypeError: If field types are incorrect
|
|
74
|
+
"""
|
|
75
|
+
extrinsic_index = data.get("extrinsicIndex", data.get("extrinsic_index", 0))
|
|
76
|
+
if isinstance(extrinsic_index, float):
|
|
77
|
+
extrinsic_index = int(extrinsic_index)
|
|
78
|
+
|
|
79
|
+
asset_id = data.get("assetId", data.get("asset_id", 0))
|
|
80
|
+
if isinstance(asset_id, float):
|
|
81
|
+
asset_id = int(asset_id)
|
|
82
|
+
|
|
83
|
+
return cls(
|
|
84
|
+
extrinsic_hash=data.get("extrinsicHash", data.get("extrinsic_hash", "")),
|
|
85
|
+
block_hash=data.get("blockHash", data.get("block_hash", "")),
|
|
86
|
+
extrinsic_index=extrinsic_index,
|
|
87
|
+
from_address=data.get("from", data.get("from_address", "")),
|
|
88
|
+
to_address=data.get("to", data.get("to_address", "")),
|
|
89
|
+
amount=str(data.get("amount", "")),
|
|
90
|
+
asset_id=asset_id,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ExtrinsicResult:
|
|
96
|
+
"""Result of querying an extrinsic from the chain.
|
|
97
|
+
|
|
98
|
+
Represents the on-chain data for a submitted extrinsic,
|
|
99
|
+
including its parameters and success status.
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
extrinsic_hash: The 0x-prefixed hash of the extrinsic
|
|
103
|
+
block_hash: The 0x-prefixed hash of the containing block
|
|
104
|
+
block_number: The block number
|
|
105
|
+
extrinsic_index: Index within the block
|
|
106
|
+
success: Whether the extrinsic executed successfully
|
|
107
|
+
signer: The SS58-encoded address of the extrinsic signer
|
|
108
|
+
module: The pallet/module name (e.g., "Assets")
|
|
109
|
+
call: The call function name (e.g., "transfer_keep_alive")
|
|
110
|
+
params: List of call parameters
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
extrinsic_hash: str
|
|
114
|
+
block_hash: str
|
|
115
|
+
block_number: int
|
|
116
|
+
extrinsic_index: int
|
|
117
|
+
success: bool
|
|
118
|
+
signer: str
|
|
119
|
+
module: str
|
|
120
|
+
call: str
|
|
121
|
+
params: list
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class ParsedAssetTransfer:
|
|
126
|
+
"""Parsed asset transfer details extracted from an extrinsic.
|
|
127
|
+
|
|
128
|
+
Attributes:
|
|
129
|
+
asset_id: The on-chain asset ID
|
|
130
|
+
from_address: Sender SS58 address
|
|
131
|
+
to_address: Recipient SS58 address
|
|
132
|
+
amount: Transfer amount in atomic units (as string)
|
|
133
|
+
success: Whether the transfer succeeded
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
asset_id: int
|
|
137
|
+
from_address: str
|
|
138
|
+
to_address: str
|
|
139
|
+
amount: str
|
|
140
|
+
success: bool
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@runtime_checkable
|
|
144
|
+
class ClientPolkadotSigner(Protocol):
|
|
145
|
+
"""Protocol for signing and submitting Polkadot extrinsics.
|
|
146
|
+
|
|
147
|
+
Implementations should provide the signer's address and the ability
|
|
148
|
+
to build, sign, and submit asset transfer extrinsics to the chain.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
```python
|
|
152
|
+
class MyPolkadotSigner:
|
|
153
|
+
def address(self) -> str:
|
|
154
|
+
return "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
|
|
155
|
+
|
|
156
|
+
async def sign_and_submit(self, call: Dict, network: str) -> Dict:
|
|
157
|
+
# Build assets.transfer_keep_alive extrinsic
|
|
158
|
+
# Sign with keypair
|
|
159
|
+
# Submit to chain
|
|
160
|
+
return {
|
|
161
|
+
"extrinsicHash": "0x...",
|
|
162
|
+
"blockHash": "0x...",
|
|
163
|
+
"extrinsicIndex": 2,
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def address(self) -> str:
|
|
169
|
+
"""Return the SS58-encoded address of the signer.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
SS58-encoded address string
|
|
173
|
+
"""
|
|
174
|
+
...
|
|
175
|
+
|
|
176
|
+
async def sign_and_submit(self, call: Dict[str, Any], network: str) -> Dict[str, Any]:
|
|
177
|
+
"""Sign and submit an asset transfer extrinsic.
|
|
178
|
+
|
|
179
|
+
The call dictionary contains:
|
|
180
|
+
- assetId: int - The on-chain asset ID
|
|
181
|
+
- target: str - The SS58-encoded recipient address
|
|
182
|
+
- amount: str - The atomic amount to transfer
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
call: Dictionary describing the assets.transfer_keep_alive call
|
|
186
|
+
network: CAIP-2 network identifier
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Dictionary with:
|
|
190
|
+
- extrinsicHash: str - 0x-prefixed hash of the extrinsic
|
|
191
|
+
- blockHash: str - 0x-prefixed hash of the block
|
|
192
|
+
- extrinsicIndex: int - Index within the block
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
Exception: If signing or submission fails
|
|
196
|
+
"""
|
|
197
|
+
...
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@runtime_checkable
|
|
201
|
+
class FacilitatorPolkadotSigner(Protocol):
|
|
202
|
+
"""Protocol for facilitator-side Polkadot operations.
|
|
203
|
+
|
|
204
|
+
Implementations should provide the ability to query extrinsics
|
|
205
|
+
from the chain (via indexer or RPC).
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
```python
|
|
209
|
+
class MyPolkadotFacilitator:
|
|
210
|
+
async def get_extrinsic(self, extrinsic_hash: str, network: str) -> Dict:
|
|
211
|
+
# Query Subscan or RPC for extrinsic details
|
|
212
|
+
return {
|
|
213
|
+
"extrinsic_hash": "0x...",
|
|
214
|
+
"block_hash": "0x...",
|
|
215
|
+
"block_num": 12345,
|
|
216
|
+
"extrinsic_index": 2,
|
|
217
|
+
"success": True,
|
|
218
|
+
"account_id": "5Grw...",
|
|
219
|
+
"call_module": "Assets",
|
|
220
|
+
"call_module_function": "transfer_keep_alive",
|
|
221
|
+
"params": [...],
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
async def get_extrinsic(self, extrinsic_hash: str, network: str) -> Dict[str, Any]:
|
|
227
|
+
"""Query an extrinsic by its hash.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
extrinsic_hash: The 0x-prefixed hex hash of the extrinsic
|
|
231
|
+
network: CAIP-2 network identifier
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Dictionary with extrinsic details including:
|
|
235
|
+
- extrinsic_hash: str
|
|
236
|
+
- block_hash: str
|
|
237
|
+
- block_num: int
|
|
238
|
+
- extrinsic_index: int
|
|
239
|
+
- success: bool
|
|
240
|
+
- account_id: str (signer address)
|
|
241
|
+
- call_module: str (e.g., "Assets")
|
|
242
|
+
- call_module_function: str (e.g., "transfer_keep_alive")
|
|
243
|
+
- params: list of parameter dicts
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
Exception: If the extrinsic cannot be found or query fails
|
|
247
|
+
"""
|
|
248
|
+
...
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def is_valid_ss58_address(address: str) -> bool:
|
|
252
|
+
"""Check if a string is a valid SS58-encoded Polkadot address.
|
|
253
|
+
|
|
254
|
+
Performs a basic format check using regex. Does not verify the checksum.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
address: String to validate
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
True if the address matches the SS58 format
|
|
261
|
+
"""
|
|
262
|
+
if not address:
|
|
263
|
+
return False
|
|
264
|
+
return bool(SS58_REGEX.match(address))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def is_valid_hash(hash_str: str) -> bool:
|
|
268
|
+
"""Check if a string is a valid 0x-prefixed 32-byte hex hash.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
hash_str: String to validate
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
True if the hash matches the expected format
|
|
275
|
+
"""
|
|
276
|
+
if not hash_str:
|
|
277
|
+
return False
|
|
278
|
+
return bool(HASH_REGEX.match(hash_str))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def parse_asset_identifier(asset: str) -> Optional[int]:
|
|
282
|
+
"""Parse a CAIP-19 asset identifier to extract the asset ID.
|
|
283
|
+
|
|
284
|
+
Format: "{network}/asset:{id}"
|
|
285
|
+
Example: "polkadot:68d56f15f85d3136970ec16946040bc1/asset:1984" -> 1984
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
asset: CAIP-19 asset identifier string
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
The asset ID as an integer, or None if parsing fails
|
|
292
|
+
"""
|
|
293
|
+
prefix = "/asset:"
|
|
294
|
+
idx = asset.find(prefix)
|
|
295
|
+
if idx == -1:
|
|
296
|
+
return None
|
|
297
|
+
try:
|
|
298
|
+
return int(asset[idx + len(prefix):])
|
|
299
|
+
except (ValueError, IndexError):
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def create_asset_identifier(network: str, asset_id: int) -> str:
|
|
304
|
+
"""Create a CAIP-19 asset identifier for a Polkadot asset.
|
|
305
|
+
|
|
306
|
+
Format: "{network}/asset:{id}"
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
network: CAIP-2 network identifier
|
|
310
|
+
asset_id: On-chain asset ID
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
CAIP-19 asset identifier string
|
|
314
|
+
"""
|
|
315
|
+
return f"{network}/asset:{asset_id}"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def extract_asset_transfer(result: ExtrinsicResult) -> Optional[ParsedAssetTransfer]:
|
|
319
|
+
"""Extract asset transfer details from an extrinsic result.
|
|
320
|
+
|
|
321
|
+
Validates that the extrinsic is a successful assets.transfer or
|
|
322
|
+
assets.transfer_keep_alive call, and extracts the transfer parameters.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
result: ExtrinsicResult from chain query
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
ParsedAssetTransfer if the extrinsic is a valid asset transfer,
|
|
329
|
+
None otherwise
|
|
330
|
+
"""
|
|
331
|
+
if not result.success:
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
# Check for assets module (case-insensitive)
|
|
335
|
+
module_lower = result.module.lower()
|
|
336
|
+
if module_lower != "assets":
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
# Check for transfer call
|
|
340
|
+
call_lower = result.call.lower()
|
|
341
|
+
if call_lower not in ("transfer", "transfer_keep_alive"):
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
asset_id: Optional[int] = None
|
|
345
|
+
to_address: Optional[str] = None
|
|
346
|
+
amount: Optional[str] = None
|
|
347
|
+
|
|
348
|
+
# Extract parameters
|
|
349
|
+
for param in result.params:
|
|
350
|
+
if not isinstance(param, dict):
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
param_name = param.get("name", "")
|
|
354
|
+
param_value = param.get("value")
|
|
355
|
+
|
|
356
|
+
if param_name in ("id", "asset_id"):
|
|
357
|
+
if isinstance(param_value, (int, float)):
|
|
358
|
+
asset_id = int(param_value)
|
|
359
|
+
elif isinstance(param_value, str):
|
|
360
|
+
try:
|
|
361
|
+
asset_id = int(param_value)
|
|
362
|
+
except ValueError:
|
|
363
|
+
pass
|
|
364
|
+
elif param_name in ("target", "dest"):
|
|
365
|
+
if isinstance(param_value, str):
|
|
366
|
+
to_address = param_value
|
|
367
|
+
elif isinstance(param_value, dict):
|
|
368
|
+
# Handle MultiAddress enum format {"Id": "address"}
|
|
369
|
+
to_address = param_value.get("Id", param_value.get("id", ""))
|
|
370
|
+
elif param_name == "amount":
|
|
371
|
+
if isinstance(param_value, str):
|
|
372
|
+
amount = param_value
|
|
373
|
+
elif isinstance(param_value, (int, float)):
|
|
374
|
+
amount = str(int(param_value))
|
|
375
|
+
|
|
376
|
+
if asset_id is None or not to_address or not amount:
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
return ParsedAssetTransfer(
|
|
380
|
+
asset_id=asset_id,
|
|
381
|
+
from_address=result.signer,
|
|
382
|
+
to_address=to_address,
|
|
383
|
+
amount=amount,
|
|
384
|
+
success=True,
|
|
385
|
+
)
|
t402/schemes/registry.py
CHANGED
|
@@ -270,7 +270,9 @@ class SchemeRegistry(Generic[T]):
|
|
|
270
270
|
for pattern in self._patterns.get(v, []):
|
|
271
271
|
if _matches_network_pattern(pattern, network):
|
|
272
272
|
# Don't override exact matches
|
|
273
|
-
for scheme_name, scheme in
|
|
273
|
+
for scheme_name, scheme in (
|
|
274
|
+
self._schemes[v].get(pattern, {}).items()
|
|
275
|
+
):
|
|
274
276
|
if scheme_name not in result:
|
|
275
277
|
result[scheme_name] = scheme
|
|
276
278
|
|
|
@@ -411,7 +413,9 @@ class FacilitatorSchemeRegistry(SchemeRegistry[SchemeNetworkFacilitator]):
|
|
|
411
413
|
with self._lock:
|
|
412
414
|
for network, schemes in self._schemes.get(version, {}).items():
|
|
413
415
|
for scheme_name, scheme in schemes.items():
|
|
414
|
-
if not hasattr(scheme, "caip_family") or not hasattr(
|
|
416
|
+
if not hasattr(scheme, "caip_family") or not hasattr(
|
|
417
|
+
scheme, "get_signers"
|
|
418
|
+
):
|
|
415
419
|
continue
|
|
416
420
|
|
|
417
421
|
family = scheme.caip_family
|