t402 1.7.1__py3-none-any.whl → 1.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) 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 +164 -1
  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 +67 -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 +70 -0
  39. t402/schemes/evm/upto/client.py +244 -0
  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 +307 -0
  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/schemes/upto/__init__.py +80 -0
  87. t402/schemes/upto/types.py +376 -0
  88. t402/stacks_paywall_template.py +2 -0
  89. t402/svm.py +45 -11
  90. t402/svm_paywall_template.py +1 -1
  91. t402/ton.py +5 -1
  92. t402/ton_paywall_template.py +1 -192
  93. t402/tron.py +2 -0
  94. t402/tron_paywall_template.py +2 -0
  95. t402/types.py +4 -2
  96. t402/wdk/errors.py +15 -5
  97. t402/wdk/signer.py +11 -2
  98. {t402-1.7.1.dist-info → t402-1.9.1.dist-info}/METADATA +42 -1
  99. t402-1.9.1.dist-info/RECORD +125 -0
  100. t402-1.7.1.dist-info/RECORD +0 -67
  101. {t402-1.7.1.dist-info → t402-1.9.1.dist-info}/WHEEL +0 -0
  102. {t402-1.7.1.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 self._schemes[v].get(pattern, {}).items():
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(scheme, "get_signers"):
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