t402 1.9.0__py3-none-any.whl → 1.10.0__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/a2a/__init__.py +73 -0
- t402/a2a/helpers.py +158 -0
- t402/a2a/types.py +145 -0
- t402/bridge/client.py +13 -5
- t402/bridge/constants.py +4 -2
- 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/django/__init__.py +42 -0
- t402/django/middleware.py +596 -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/errors.py +213 -0
- t402/evm_paywall_template.py +1 -1
- t402/facilitator.py +125 -0
- t402/fastapi/middleware.py +1 -3
- t402/mcp/constants.py +3 -6
- t402/mcp/server.py +501 -84
- t402/mcp/web3_utils.py +493 -0
- t402/multisig/__init__.py +120 -0
- t402/multisig/constants.py +54 -0
- t402/multisig/safe.py +441 -0
- t402/multisig/signature.py +228 -0
- t402/multisig/transaction.py +238 -0
- t402/multisig/types.py +108 -0
- t402/multisig/utils.py +77 -0
- t402/near_paywall_template.py +2 -0
- t402/networks.py +34 -1
- t402/paywall.py +1 -3
- t402/schemes/__init__.py +143 -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/cosmos/__init__.py +114 -0
- t402/schemes/cosmos/constants.py +211 -0
- t402/schemes/cosmos/exact_direct/__init__.py +21 -0
- t402/schemes/cosmos/exact_direct/client.py +198 -0
- t402/schemes/cosmos/exact_direct/facilitator.py +493 -0
- t402/schemes/cosmos/exact_direct/server.py +315 -0
- t402/schemes/cosmos/types.py +501 -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 +137 -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/near/upto/__init__.py +54 -0
- t402/schemes/near/upto/types.py +272 -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 +44 -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/svm/upto/__init__.py +23 -0
- t402/schemes/svm/upto/types.py +193 -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 +24 -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/ton/upto/__init__.py +31 -0
- t402/schemes/ton/upto/types.py +215 -0
- t402/schemes/tron/__init__.py +28 -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/schemes/tron/upto/__init__.py +30 -0
- t402/schemes/tron/upto/types.py +213 -0
- t402/stacks_paywall_template.py +2 -0
- t402/starlette/__init__.py +38 -0
- t402/starlette/middleware.py +522 -0
- t402/svm.py +45 -11
- t402/svm_paywall_template.py +1 -1
- t402/ton.py +6 -2
- t402/ton_paywall_template.py +1 -192
- t402/tron.py +2 -0
- t402/tron_paywall_template.py +2 -0
- t402/types.py +103 -3
- t402/wdk/chains.py +1 -1
- t402/wdk/errors.py +15 -5
- t402/wdk/signer.py +11 -2
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/METADATA +42 -1
- t402-1.10.0.dist-info/RECORD +156 -0
- t402-1.9.0.dist-info/RECORD +0 -72
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/WHEEL +0 -0
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
"""EVM Exact Scheme - Facilitator Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the facilitator-side implementation of the exact payment
|
|
4
|
+
scheme for EVM networks using EIP-3009 TransferWithAuthorization.
|
|
5
|
+
|
|
6
|
+
The facilitator:
|
|
7
|
+
1. Verifies EIP-3009 signatures off-chain by checking authorization metadata,
|
|
8
|
+
EIP-712 typed data signature recovery, balance, and nonce usage
|
|
9
|
+
2. Settles payments by calling transferWithAuthorization on the token contract
|
|
10
|
+
3. Waits for transaction confirmation via receipt polling
|
|
11
|
+
|
|
12
|
+
EIP-3009 TransferWithAuthorization allows gasless token transfers where:
|
|
13
|
+
- The token holder signs an off-chain authorization (EIP-712 typed data)
|
|
14
|
+
- A facilitator submits the authorization on-chain
|
|
15
|
+
- The token contract verifies the signature and executes the transfer
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import time
|
|
22
|
+
from typing import Any, Dict, List, Optional, Protocol, Union, runtime_checkable
|
|
23
|
+
|
|
24
|
+
from t402.types import (
|
|
25
|
+
PaymentRequirementsV2,
|
|
26
|
+
PaymentPayloadV2,
|
|
27
|
+
VerifyResponse,
|
|
28
|
+
SettleResponse,
|
|
29
|
+
Network,
|
|
30
|
+
)
|
|
31
|
+
from t402.chains import KNOWN_TOKENS
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# Constants
|
|
37
|
+
SCHEME_EXACT = "exact"
|
|
38
|
+
CAIP_FAMILY = "eip155:*"
|
|
39
|
+
|
|
40
|
+
# Minimum time buffer (seconds) before validBefore deadline
|
|
41
|
+
MIN_VALIDITY_BUFFER = 30
|
|
42
|
+
|
|
43
|
+
# Default timeout for transaction confirmation (milliseconds)
|
|
44
|
+
DEFAULT_CONFIRMATION_TIMEOUT = 60000
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@runtime_checkable
|
|
48
|
+
class FacilitatorEvmSigner(Protocol):
|
|
49
|
+
"""Protocol for EVM facilitator signer operations.
|
|
50
|
+
|
|
51
|
+
Implementations should provide address retrieval, EIP-3009 signature
|
|
52
|
+
verification, token transfer execution, transaction confirmation,
|
|
53
|
+
and balance checking capabilities.
|
|
54
|
+
|
|
55
|
+
The signer abstracts all blockchain interactions so the facilitator
|
|
56
|
+
scheme logic remains chain-agnostic within EVM.
|
|
57
|
+
|
|
58
|
+
Example implementation:
|
|
59
|
+
```python
|
|
60
|
+
from web3 import Web3
|
|
61
|
+
|
|
62
|
+
class MyEvmFacilitatorSigner:
|
|
63
|
+
def __init__(self, web3: Web3, private_key: str, addresses: dict):
|
|
64
|
+
self._web3 = web3
|
|
65
|
+
self._account = web3.eth.account.from_key(private_key)
|
|
66
|
+
self._addresses = addresses
|
|
67
|
+
|
|
68
|
+
def get_addresses(self, network: str) -> List[str]:
|
|
69
|
+
return self._addresses.get(network, [self._account.address])
|
|
70
|
+
|
|
71
|
+
async def verify_eip3009_signature(
|
|
72
|
+
self,
|
|
73
|
+
from_address: str,
|
|
74
|
+
to_address: str,
|
|
75
|
+
value: str,
|
|
76
|
+
valid_after: str,
|
|
77
|
+
valid_before: str,
|
|
78
|
+
nonce: str,
|
|
79
|
+
signature: str,
|
|
80
|
+
token_address: str,
|
|
81
|
+
chain_id: int,
|
|
82
|
+
token_name: str,
|
|
83
|
+
token_version: str,
|
|
84
|
+
) -> EvmVerifyResult:
|
|
85
|
+
# Recover signer from EIP-712 typed data signature
|
|
86
|
+
# and compare with from_address
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
async def execute_transfer(
|
|
90
|
+
self,
|
|
91
|
+
from_address: str,
|
|
92
|
+
to_address: str,
|
|
93
|
+
value: str,
|
|
94
|
+
valid_after: str,
|
|
95
|
+
valid_before: str,
|
|
96
|
+
nonce: str,
|
|
97
|
+
signature: str,
|
|
98
|
+
token_address: str,
|
|
99
|
+
network: str,
|
|
100
|
+
) -> str:
|
|
101
|
+
# Call transferWithAuthorization on token contract
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
async def wait_for_confirmation(
|
|
105
|
+
self,
|
|
106
|
+
tx_hash: str,
|
|
107
|
+
network: str,
|
|
108
|
+
timeout_ms: int = 60000,
|
|
109
|
+
) -> EvmTransactionConfirmation:
|
|
110
|
+
# Wait for transaction receipt
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
async def get_balance(
|
|
114
|
+
self,
|
|
115
|
+
owner_address: str,
|
|
116
|
+
token_address: str,
|
|
117
|
+
network: str,
|
|
118
|
+
) -> str:
|
|
119
|
+
# Get ERC-20 token balance
|
|
120
|
+
...
|
|
121
|
+
```
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def get_addresses(self, network: str) -> List[str]:
|
|
125
|
+
"""Return all facilitator addresses for the given network.
|
|
126
|
+
|
|
127
|
+
Enables multi-address support for load balancing and key rotation.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
network: Network identifier (CAIP-2 format, e.g., "eip155:8453")
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of Ethereum addresses (checksummed or lowercase hex)
|
|
134
|
+
"""
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
async def verify_eip3009_signature(
|
|
138
|
+
self,
|
|
139
|
+
from_address: str,
|
|
140
|
+
to_address: str,
|
|
141
|
+
value: str,
|
|
142
|
+
valid_after: str,
|
|
143
|
+
valid_before: str,
|
|
144
|
+
nonce: str,
|
|
145
|
+
signature: str,
|
|
146
|
+
token_address: str,
|
|
147
|
+
chain_id: int,
|
|
148
|
+
token_name: str,
|
|
149
|
+
token_version: str,
|
|
150
|
+
) -> "EvmVerifyResult":
|
|
151
|
+
"""Verify an EIP-3009 TransferWithAuthorization signature.
|
|
152
|
+
|
|
153
|
+
Reconstructs the EIP-712 typed data hash and recovers the signer
|
|
154
|
+
address from the signature, comparing it with the expected from_address.
|
|
155
|
+
|
|
156
|
+
Supports both EOA (ecrecover) and smart wallet (EIP-1271) signatures.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
from_address: Expected signer/payer address
|
|
160
|
+
to_address: Recipient address
|
|
161
|
+
value: Transfer amount in token's smallest unit
|
|
162
|
+
valid_after: Unix timestamp after which authorization is valid
|
|
163
|
+
valid_before: Unix timestamp before which authorization is valid
|
|
164
|
+
nonce: 32-byte nonce as hex string (0x-prefixed)
|
|
165
|
+
signature: ECDSA signature as hex string (0x-prefixed, 65 bytes)
|
|
166
|
+
token_address: ERC-20 token contract address
|
|
167
|
+
chain_id: EVM chain ID
|
|
168
|
+
token_name: Token name for EIP-712 domain (e.g., "TetherToken")
|
|
169
|
+
token_version: Token version for EIP-712 domain (e.g., "1")
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
EvmVerifyResult indicating validity and recovered address
|
|
173
|
+
"""
|
|
174
|
+
...
|
|
175
|
+
|
|
176
|
+
async def execute_transfer(
|
|
177
|
+
self,
|
|
178
|
+
from_address: str,
|
|
179
|
+
to_address: str,
|
|
180
|
+
value: str,
|
|
181
|
+
valid_after: str,
|
|
182
|
+
valid_before: str,
|
|
183
|
+
nonce: str,
|
|
184
|
+
signature: str,
|
|
185
|
+
token_address: str,
|
|
186
|
+
network: str,
|
|
187
|
+
) -> str:
|
|
188
|
+
"""Execute transferWithAuthorization on the token contract.
|
|
189
|
+
|
|
190
|
+
Calls the EIP-3009 transferWithAuthorization function with the
|
|
191
|
+
provided authorization parameters and signature.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
from_address: Payer address (token holder)
|
|
195
|
+
to_address: Recipient address
|
|
196
|
+
value: Transfer amount in token's smallest unit
|
|
197
|
+
valid_after: Unix timestamp (as string)
|
|
198
|
+
valid_before: Unix timestamp (as string)
|
|
199
|
+
nonce: 32-byte nonce as hex string (0x-prefixed)
|
|
200
|
+
signature: ECDSA signature as hex string (0x-prefixed)
|
|
201
|
+
token_address: ERC-20 token contract address
|
|
202
|
+
network: Network identifier (CAIP-2 format)
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Transaction hash as hex string (0x-prefixed)
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
Exception: If transaction submission fails
|
|
209
|
+
"""
|
|
210
|
+
...
|
|
211
|
+
|
|
212
|
+
async def wait_for_confirmation(
|
|
213
|
+
self,
|
|
214
|
+
tx_hash: str,
|
|
215
|
+
network: str,
|
|
216
|
+
timeout_ms: int = 60000,
|
|
217
|
+
) -> "EvmTransactionConfirmation":
|
|
218
|
+
"""Wait for a transaction to be confirmed (mined and successful).
|
|
219
|
+
|
|
220
|
+
Polls for the transaction receipt until confirmed or timeout.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
tx_hash: Transaction hash to monitor
|
|
224
|
+
network: Network identifier
|
|
225
|
+
timeout_ms: Maximum wait time in milliseconds
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
EvmTransactionConfirmation with status, block number, and hash
|
|
229
|
+
"""
|
|
230
|
+
...
|
|
231
|
+
|
|
232
|
+
async def get_balance(
|
|
233
|
+
self,
|
|
234
|
+
owner_address: str,
|
|
235
|
+
token_address: str,
|
|
236
|
+
network: str,
|
|
237
|
+
) -> str:
|
|
238
|
+
"""Get the ERC-20 token balance for an address.
|
|
239
|
+
|
|
240
|
+
Calls balanceOf on the token contract.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
owner_address: Address to check balance for
|
|
244
|
+
token_address: ERC-20 token contract address
|
|
245
|
+
network: Network identifier
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Balance in token's smallest unit as string
|
|
249
|
+
"""
|
|
250
|
+
...
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class EvmVerifyResult:
|
|
254
|
+
"""Result of EIP-3009 signature verification.
|
|
255
|
+
|
|
256
|
+
Attributes:
|
|
257
|
+
valid: Whether the signature is valid
|
|
258
|
+
recovered_address: Address recovered from the signature (if successful)
|
|
259
|
+
reason: Reason for failure (if invalid)
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
valid: bool,
|
|
265
|
+
recovered_address: Optional[str] = None,
|
|
266
|
+
reason: Optional[str] = None,
|
|
267
|
+
):
|
|
268
|
+
self.valid = valid
|
|
269
|
+
self.recovered_address = recovered_address
|
|
270
|
+
self.reason = reason
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class EvmTransactionConfirmation:
|
|
274
|
+
"""Result of waiting for transaction confirmation.
|
|
275
|
+
|
|
276
|
+
Attributes:
|
|
277
|
+
success: Whether the transaction was successfully confirmed
|
|
278
|
+
tx_hash: The confirmed transaction hash
|
|
279
|
+
block_number: Block number where the transaction was mined
|
|
280
|
+
error: Error message if confirmation failed
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def __init__(
|
|
284
|
+
self,
|
|
285
|
+
success: bool,
|
|
286
|
+
tx_hash: Optional[str] = None,
|
|
287
|
+
block_number: Optional[int] = None,
|
|
288
|
+
error: Optional[str] = None,
|
|
289
|
+
):
|
|
290
|
+
self.success = success
|
|
291
|
+
self.tx_hash = tx_hash
|
|
292
|
+
self.block_number = block_number
|
|
293
|
+
self.error = error
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class ExactEvmFacilitatorScheme:
|
|
297
|
+
"""Facilitator scheme for EVM exact payments using EIP-3009.
|
|
298
|
+
|
|
299
|
+
Verifies EIP-3009 TransferWithAuthorization signatures and settles
|
|
300
|
+
payments by calling transferWithAuthorization on the token contract.
|
|
301
|
+
|
|
302
|
+
The verification process checks:
|
|
303
|
+
1. Scheme and network validity
|
|
304
|
+
2. Payload structure (signature + authorization fields)
|
|
305
|
+
3. EIP-3009 signature recovery against from_address
|
|
306
|
+
4. Deadline validity (validBefore with 30-second buffer)
|
|
307
|
+
5. Valid-after constraint (validAfter <= current time)
|
|
308
|
+
6. Token balance sufficiency
|
|
309
|
+
7. Amount >= required amount
|
|
310
|
+
8. Recipient matches payTo
|
|
311
|
+
9. Token address matches required asset
|
|
312
|
+
|
|
313
|
+
The settlement process:
|
|
314
|
+
1. Re-verifies the payment
|
|
315
|
+
2. Calls transferWithAuthorization on the token contract
|
|
316
|
+
3. Waits for transaction confirmation
|
|
317
|
+
|
|
318
|
+
Example:
|
|
319
|
+
```python
|
|
320
|
+
facilitator = ExactEvmFacilitatorScheme(signer=my_evm_signer)
|
|
321
|
+
|
|
322
|
+
# Verify a payment
|
|
323
|
+
result = await facilitator.verify(payload, requirements)
|
|
324
|
+
if result.is_valid:
|
|
325
|
+
# Settle the payment on-chain
|
|
326
|
+
settlement = await facilitator.settle(payload, requirements)
|
|
327
|
+
if settlement.success:
|
|
328
|
+
print(f"Settled: {settlement.transaction}")
|
|
329
|
+
```
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
scheme = SCHEME_EXACT
|
|
333
|
+
caip_family = CAIP_FAMILY
|
|
334
|
+
|
|
335
|
+
def __init__(self, signer: FacilitatorEvmSigner):
|
|
336
|
+
"""Initialize the EVM facilitator scheme.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
signer: EVM facilitator signer for signature verification,
|
|
340
|
+
balance checking, and transaction execution.
|
|
341
|
+
"""
|
|
342
|
+
self._signer = signer
|
|
343
|
+
|
|
344
|
+
def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
|
|
345
|
+
"""Get mechanism-specific extra data for supported kinds.
|
|
346
|
+
|
|
347
|
+
Returns asset metadata (default asset address, name, version, decimals)
|
|
348
|
+
for the specified EVM network.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
network: The network identifier (e.g., "eip155:8453")
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Dict with asset metadata if network is supported, else None
|
|
355
|
+
"""
|
|
356
|
+
chain_id_str = self._get_chain_id_str(network)
|
|
357
|
+
if chain_id_str is None:
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
tokens = KNOWN_TOKENS.get(chain_id_str)
|
|
361
|
+
if not tokens or len(tokens) == 0:
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
token = tokens[0]
|
|
365
|
+
return {
|
|
366
|
+
"defaultAsset": token["address"],
|
|
367
|
+
"name": token["name"],
|
|
368
|
+
"version": token["version"],
|
|
369
|
+
"decimals": token["decimals"],
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
def get_signers(self, network: Network) -> List[str]:
|
|
373
|
+
"""Get signer addresses for this facilitator on the given network.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
network: The network identifier
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
List of facilitator Ethereum addresses
|
|
380
|
+
"""
|
|
381
|
+
return self._signer.get_addresses(network)
|
|
382
|
+
|
|
383
|
+
async def verify(
|
|
384
|
+
self,
|
|
385
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
386
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
387
|
+
) -> VerifyResponse:
|
|
388
|
+
"""Verify an EVM EIP-3009 payment payload.
|
|
389
|
+
|
|
390
|
+
Performs comprehensive validation of the EIP-3009 authorization
|
|
391
|
+
including signature recovery, balance checks, and constraint validation.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
payload: The payment payload containing signature and authorization
|
|
395
|
+
requirements: The payment requirements to verify against
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
VerifyResponse indicating validity and payer address
|
|
399
|
+
"""
|
|
400
|
+
try:
|
|
401
|
+
# Extract data from payload and requirements
|
|
402
|
+
payload_data = self._extract_payload(payload)
|
|
403
|
+
req_data = self._extract_requirements(requirements)
|
|
404
|
+
|
|
405
|
+
network = req_data.get("network", "")
|
|
406
|
+
scheme = req_data.get("scheme", "")
|
|
407
|
+
|
|
408
|
+
# Step 1: Validate scheme
|
|
409
|
+
if scheme != SCHEME_EXACT:
|
|
410
|
+
return VerifyResponse(
|
|
411
|
+
is_valid=False,
|
|
412
|
+
invalid_reason="unsupported_scheme",
|
|
413
|
+
payer=None,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Step 2: Validate network (must be eip155:*)
|
|
417
|
+
if not self._is_valid_network(network):
|
|
418
|
+
return VerifyResponse(
|
|
419
|
+
is_valid=False,
|
|
420
|
+
invalid_reason="unsupported_network",
|
|
421
|
+
payer=None,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Step 3: Parse EIP-3009 payload
|
|
425
|
+
eip3009_payload = self._parse_eip3009_payload(payload_data)
|
|
426
|
+
if eip3009_payload is None:
|
|
427
|
+
return VerifyResponse(
|
|
428
|
+
is_valid=False,
|
|
429
|
+
invalid_reason="invalid_payload",
|
|
430
|
+
payer=None,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
authorization = eip3009_payload["authorization"]
|
|
434
|
+
signature = eip3009_payload["signature"]
|
|
435
|
+
payer = authorization["from"]
|
|
436
|
+
|
|
437
|
+
# Step 4: Get chain ID and token info
|
|
438
|
+
chain_id = self._get_chain_id(network)
|
|
439
|
+
if chain_id is None:
|
|
440
|
+
return VerifyResponse(
|
|
441
|
+
is_valid=False,
|
|
442
|
+
invalid_reason="unsupported_network",
|
|
443
|
+
payer=payer,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
asset = req_data.get("asset", "")
|
|
447
|
+
token_name, token_version = self._get_token_info(network, asset)
|
|
448
|
+
|
|
449
|
+
# Step 5: Verify EIP-3009 signature
|
|
450
|
+
try:
|
|
451
|
+
verify_result = await self._signer.verify_eip3009_signature(
|
|
452
|
+
from_address=payer,
|
|
453
|
+
to_address=authorization["to"],
|
|
454
|
+
value=authorization["value"],
|
|
455
|
+
valid_after=authorization["validAfter"],
|
|
456
|
+
valid_before=authorization["validBefore"],
|
|
457
|
+
nonce=authorization["nonce"],
|
|
458
|
+
signature=signature,
|
|
459
|
+
token_address=asset,
|
|
460
|
+
chain_id=chain_id,
|
|
461
|
+
token_name=token_name,
|
|
462
|
+
token_version=token_version,
|
|
463
|
+
)
|
|
464
|
+
except Exception as e:
|
|
465
|
+
logger.error(f"Signature verification error: {e}")
|
|
466
|
+
return VerifyResponse(
|
|
467
|
+
is_valid=False,
|
|
468
|
+
invalid_reason=f"signature_verification_error: {str(e)}",
|
|
469
|
+
payer=payer,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if not verify_result.valid:
|
|
473
|
+
reason = verify_result.reason or "invalid_signature"
|
|
474
|
+
return VerifyResponse(
|
|
475
|
+
is_valid=False,
|
|
476
|
+
invalid_reason=f"invalid_signature: {reason}",
|
|
477
|
+
payer=payer,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Step 6: Check validBefore deadline (with buffer)
|
|
481
|
+
now = int(time.time())
|
|
482
|
+
try:
|
|
483
|
+
valid_before = int(authorization["validBefore"])
|
|
484
|
+
except (ValueError, TypeError):
|
|
485
|
+
return VerifyResponse(
|
|
486
|
+
is_valid=False,
|
|
487
|
+
invalid_reason="invalid_valid_before",
|
|
488
|
+
payer=payer,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
if valid_before < now + MIN_VALIDITY_BUFFER:
|
|
492
|
+
return VerifyResponse(
|
|
493
|
+
is_valid=False,
|
|
494
|
+
invalid_reason="authorization_expired",
|
|
495
|
+
payer=payer,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Step 7: Check validAfter constraint
|
|
499
|
+
try:
|
|
500
|
+
valid_after = int(authorization["validAfter"])
|
|
501
|
+
except (ValueError, TypeError):
|
|
502
|
+
return VerifyResponse(
|
|
503
|
+
is_valid=False,
|
|
504
|
+
invalid_reason="invalid_valid_after",
|
|
505
|
+
payer=payer,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if valid_after > now:
|
|
509
|
+
return VerifyResponse(
|
|
510
|
+
is_valid=False,
|
|
511
|
+
invalid_reason="authorization_not_yet_valid",
|
|
512
|
+
payer=payer,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Step 8: Verify token balance
|
|
516
|
+
try:
|
|
517
|
+
balance_str = await self._signer.get_balance(
|
|
518
|
+
owner_address=payer,
|
|
519
|
+
token_address=asset,
|
|
520
|
+
network=network,
|
|
521
|
+
)
|
|
522
|
+
balance = int(balance_str)
|
|
523
|
+
except (ValueError, TypeError) as e:
|
|
524
|
+
logger.error(f"Balance check failed: {e}")
|
|
525
|
+
return VerifyResponse(
|
|
526
|
+
is_valid=False,
|
|
527
|
+
invalid_reason="balance_check_failed",
|
|
528
|
+
payer=payer,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
required_amount_str = req_data.get("amount", "0")
|
|
532
|
+
try:
|
|
533
|
+
required_amount = int(required_amount_str)
|
|
534
|
+
except (ValueError, TypeError):
|
|
535
|
+
return VerifyResponse(
|
|
536
|
+
is_valid=False,
|
|
537
|
+
invalid_reason="invalid_required_amount",
|
|
538
|
+
payer=payer,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if balance < required_amount:
|
|
542
|
+
return VerifyResponse(
|
|
543
|
+
is_valid=False,
|
|
544
|
+
invalid_reason="insufficient_balance",
|
|
545
|
+
payer=payer,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Step 9: Verify amount sufficiency
|
|
549
|
+
try:
|
|
550
|
+
payload_value = int(authorization["value"])
|
|
551
|
+
except (ValueError, TypeError):
|
|
552
|
+
return VerifyResponse(
|
|
553
|
+
is_valid=False,
|
|
554
|
+
invalid_reason="invalid_payload_amount",
|
|
555
|
+
payer=payer,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if payload_value < required_amount:
|
|
559
|
+
return VerifyResponse(
|
|
560
|
+
is_valid=False,
|
|
561
|
+
invalid_reason="insufficient_amount",
|
|
562
|
+
payer=payer,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# Step 10: Verify recipient matches payTo
|
|
566
|
+
pay_to = req_data.get("payTo", "")
|
|
567
|
+
auth_to = authorization.get("to", "")
|
|
568
|
+
if not self._addresses_equal(auth_to, pay_to):
|
|
569
|
+
return VerifyResponse(
|
|
570
|
+
is_valid=False,
|
|
571
|
+
invalid_reason="recipient_mismatch",
|
|
572
|
+
payer=payer,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# All checks passed
|
|
576
|
+
return VerifyResponse(
|
|
577
|
+
is_valid=True,
|
|
578
|
+
invalid_reason=None,
|
|
579
|
+
payer=payer,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
except Exception as e:
|
|
583
|
+
logger.error(f"EVM verification failed: {e}")
|
|
584
|
+
return VerifyResponse(
|
|
585
|
+
is_valid=False,
|
|
586
|
+
invalid_reason=f"verification_error: {str(e)}",
|
|
587
|
+
payer=None,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
async def settle(
|
|
591
|
+
self,
|
|
592
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
593
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
594
|
+
) -> SettleResponse:
|
|
595
|
+
"""Settle an EVM EIP-3009 payment on-chain.
|
|
596
|
+
|
|
597
|
+
Verifies the payment first, then calls transferWithAuthorization
|
|
598
|
+
on the token contract and waits for transaction confirmation.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
payload: The verified payment payload with signature and authorization
|
|
602
|
+
requirements: The payment requirements
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
SettleResponse with transaction hash and status
|
|
606
|
+
"""
|
|
607
|
+
req_data = self._extract_requirements(requirements)
|
|
608
|
+
network = req_data.get("network", "")
|
|
609
|
+
|
|
610
|
+
# Step 1: Verify the payment first
|
|
611
|
+
verify_result = await self.verify(payload, requirements)
|
|
612
|
+
|
|
613
|
+
if not verify_result.is_valid:
|
|
614
|
+
return SettleResponse(
|
|
615
|
+
success=False,
|
|
616
|
+
error_reason=verify_result.invalid_reason,
|
|
617
|
+
transaction=None,
|
|
618
|
+
network=network,
|
|
619
|
+
payer=verify_result.payer,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# Step 2: Extract payload data for on-chain execution
|
|
623
|
+
try:
|
|
624
|
+
payload_data = self._extract_payload(payload)
|
|
625
|
+
eip3009_payload = self._parse_eip3009_payload(payload_data)
|
|
626
|
+
|
|
627
|
+
if eip3009_payload is None:
|
|
628
|
+
return SettleResponse(
|
|
629
|
+
success=False,
|
|
630
|
+
error_reason="invalid_payload",
|
|
631
|
+
transaction=None,
|
|
632
|
+
network=network,
|
|
633
|
+
payer=verify_result.payer,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
authorization = eip3009_payload["authorization"]
|
|
637
|
+
signature = eip3009_payload["signature"]
|
|
638
|
+
payer = authorization["from"]
|
|
639
|
+
|
|
640
|
+
except Exception as e:
|
|
641
|
+
logger.error(f"Payload extraction failed: {e}")
|
|
642
|
+
return SettleResponse(
|
|
643
|
+
success=False,
|
|
644
|
+
error_reason=f"invalid_payload: {str(e)}",
|
|
645
|
+
transaction=None,
|
|
646
|
+
network=network,
|
|
647
|
+
payer=verify_result.payer,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
# Step 3: Execute transferWithAuthorization on-chain
|
|
651
|
+
asset = req_data.get("asset", "")
|
|
652
|
+
try:
|
|
653
|
+
tx_hash = await self._signer.execute_transfer(
|
|
654
|
+
from_address=payer,
|
|
655
|
+
to_address=authorization["to"],
|
|
656
|
+
value=authorization["value"],
|
|
657
|
+
valid_after=authorization["validAfter"],
|
|
658
|
+
valid_before=authorization["validBefore"],
|
|
659
|
+
nonce=authorization["nonce"],
|
|
660
|
+
signature=signature,
|
|
661
|
+
token_address=asset,
|
|
662
|
+
network=network,
|
|
663
|
+
)
|
|
664
|
+
except Exception as e:
|
|
665
|
+
logger.error(f"Transaction execution failed: {e}")
|
|
666
|
+
return SettleResponse(
|
|
667
|
+
success=False,
|
|
668
|
+
error_reason=f"transaction_failed: {str(e)}",
|
|
669
|
+
transaction=None,
|
|
670
|
+
network=network,
|
|
671
|
+
payer=payer,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# Step 4: Wait for transaction confirmation
|
|
675
|
+
try:
|
|
676
|
+
confirmation = await self._signer.wait_for_confirmation(
|
|
677
|
+
tx_hash=tx_hash,
|
|
678
|
+
network=network,
|
|
679
|
+
timeout_ms=DEFAULT_CONFIRMATION_TIMEOUT,
|
|
680
|
+
)
|
|
681
|
+
except Exception as e:
|
|
682
|
+
logger.error(f"Transaction confirmation failed: {e}")
|
|
683
|
+
return SettleResponse(
|
|
684
|
+
success=False,
|
|
685
|
+
error_reason=f"confirmation_failed: {str(e)}",
|
|
686
|
+
transaction=tx_hash,
|
|
687
|
+
network=network,
|
|
688
|
+
payer=payer,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
if not confirmation.success:
|
|
692
|
+
return SettleResponse(
|
|
693
|
+
success=False,
|
|
694
|
+
error_reason=confirmation.error or "transaction_reverted",
|
|
695
|
+
transaction=tx_hash,
|
|
696
|
+
network=network,
|
|
697
|
+
payer=payer,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
# Use confirmed tx hash if available (should match)
|
|
701
|
+
final_tx_hash = confirmation.tx_hash if confirmation.tx_hash else tx_hash
|
|
702
|
+
|
|
703
|
+
return SettleResponse(
|
|
704
|
+
success=True,
|
|
705
|
+
error_reason=None,
|
|
706
|
+
transaction=final_tx_hash,
|
|
707
|
+
network=network,
|
|
708
|
+
payer=payer,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
def _extract_payload(
|
|
712
|
+
self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
|
|
713
|
+
) -> Dict[str, Any]:
|
|
714
|
+
"""Extract payload data as a dict.
|
|
715
|
+
|
|
716
|
+
Handles both PaymentPayloadV2 models (where the inner payload is
|
|
717
|
+
in the 'payload' field) and plain dicts.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
payload: Payment payload (model or dict)
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
Dict containing signature and authorization data
|
|
724
|
+
"""
|
|
725
|
+
if hasattr(payload, "model_dump"):
|
|
726
|
+
data = payload.model_dump(by_alias=True)
|
|
727
|
+
return data.get("payload", data)
|
|
728
|
+
elif isinstance(payload, dict):
|
|
729
|
+
return payload.get("payload", payload)
|
|
730
|
+
return dict(payload)
|
|
731
|
+
|
|
732
|
+
def _extract_requirements(
|
|
733
|
+
self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
|
|
734
|
+
) -> Dict[str, Any]:
|
|
735
|
+
"""Extract requirements data as a dict.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
requirements: Payment requirements (model or dict)
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
Dict containing requirement fields
|
|
742
|
+
"""
|
|
743
|
+
if hasattr(requirements, "model_dump"):
|
|
744
|
+
return requirements.model_dump(by_alias=True)
|
|
745
|
+
return dict(requirements)
|
|
746
|
+
|
|
747
|
+
def _parse_eip3009_payload(
|
|
748
|
+
self, payload_data: Dict[str, Any]
|
|
749
|
+
) -> Optional[Dict[str, Any]]:
|
|
750
|
+
"""Parse and validate EIP-3009 payload fields.
|
|
751
|
+
|
|
752
|
+
Extracts signature and authorization from the payload data,
|
|
753
|
+
normalizing field names for internal use.
|
|
754
|
+
|
|
755
|
+
The EIP-3009 authorization contains:
|
|
756
|
+
- from: payer address
|
|
757
|
+
- to: recipient address
|
|
758
|
+
- value: amount in token units
|
|
759
|
+
- validAfter: earliest validity timestamp
|
|
760
|
+
- validBefore: latest validity timestamp
|
|
761
|
+
- nonce: random 32-byte nonce (hex)
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
payload_data: Raw payload dict
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
Normalized dict with signature and authorization fields,
|
|
768
|
+
or None if required fields are missing.
|
|
769
|
+
"""
|
|
770
|
+
signature = payload_data.get("signature", "")
|
|
771
|
+
if not signature:
|
|
772
|
+
return None
|
|
773
|
+
|
|
774
|
+
auth_data = payload_data.get("authorization")
|
|
775
|
+
if not auth_data:
|
|
776
|
+
return None
|
|
777
|
+
|
|
778
|
+
# Extract and normalize authorization fields
|
|
779
|
+
from_addr = auth_data.get("from", "")
|
|
780
|
+
to_addr = auth_data.get("to", "")
|
|
781
|
+
value = auth_data.get("value", "0")
|
|
782
|
+
valid_after = auth_data.get("validAfter", auth_data.get("valid_after", "0"))
|
|
783
|
+
valid_before = auth_data.get("validBefore", auth_data.get("valid_before", "0"))
|
|
784
|
+
nonce = auth_data.get("nonce", "")
|
|
785
|
+
|
|
786
|
+
if not from_addr:
|
|
787
|
+
return None
|
|
788
|
+
|
|
789
|
+
if not signature:
|
|
790
|
+
return None
|
|
791
|
+
|
|
792
|
+
return {
|
|
793
|
+
"signature": signature,
|
|
794
|
+
"authorization": {
|
|
795
|
+
"from": from_addr,
|
|
796
|
+
"to": to_addr,
|
|
797
|
+
"value": str(value),
|
|
798
|
+
"validAfter": str(valid_after),
|
|
799
|
+
"validBefore": str(valid_before),
|
|
800
|
+
"nonce": str(nonce),
|
|
801
|
+
},
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
def _is_valid_network(self, network: str) -> bool:
|
|
805
|
+
"""Check if the network is a valid EVM network.
|
|
806
|
+
|
|
807
|
+
Validates that the network follows the eip155:* CAIP-2 format
|
|
808
|
+
and has a valid numeric chain ID.
|
|
809
|
+
|
|
810
|
+
Args:
|
|
811
|
+
network: Network identifier
|
|
812
|
+
|
|
813
|
+
Returns:
|
|
814
|
+
True if the network is a valid EVM network
|
|
815
|
+
"""
|
|
816
|
+
if not network.startswith("eip155:"):
|
|
817
|
+
return False
|
|
818
|
+
|
|
819
|
+
try:
|
|
820
|
+
chain_id_str = network.split(":")[1]
|
|
821
|
+
chain_id = int(chain_id_str)
|
|
822
|
+
return chain_id > 0
|
|
823
|
+
except (IndexError, ValueError):
|
|
824
|
+
return False
|
|
825
|
+
|
|
826
|
+
def _get_chain_id(self, network: str) -> Optional[int]:
|
|
827
|
+
"""Get the chain ID from a network identifier.
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
network: Network identifier (CAIP-2 format, e.g., "eip155:8453")
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
Chain ID as integer, or None if invalid
|
|
834
|
+
"""
|
|
835
|
+
if not network.startswith("eip155:"):
|
|
836
|
+
return None
|
|
837
|
+
|
|
838
|
+
try:
|
|
839
|
+
return int(network.split(":")[1])
|
|
840
|
+
except (IndexError, ValueError):
|
|
841
|
+
return None
|
|
842
|
+
|
|
843
|
+
def _get_chain_id_str(self, network: str) -> Optional[str]:
|
|
844
|
+
"""Get the chain ID as string for KNOWN_TOKENS lookup.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
network: Network identifier (CAIP-2 format)
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
Chain ID as string, or None if invalid
|
|
851
|
+
"""
|
|
852
|
+
chain_id = self._get_chain_id(network)
|
|
853
|
+
if chain_id is None:
|
|
854
|
+
return None
|
|
855
|
+
return str(chain_id)
|
|
856
|
+
|
|
857
|
+
def _get_token_info(self, network: str, asset: str) -> tuple:
|
|
858
|
+
"""Get token name and version for EIP-712 domain.
|
|
859
|
+
|
|
860
|
+
Looks up the token in KNOWN_TOKENS by chain ID and address.
|
|
861
|
+
Falls back to defaults if not found.
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
network: Network identifier
|
|
865
|
+
asset: Token contract address
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
Tuple of (token_name, token_version)
|
|
869
|
+
"""
|
|
870
|
+
chain_id_str = self._get_chain_id_str(network)
|
|
871
|
+
if chain_id_str and chain_id_str in KNOWN_TOKENS:
|
|
872
|
+
for token in KNOWN_TOKENS[chain_id_str]:
|
|
873
|
+
if self._addresses_equal(token["address"], asset):
|
|
874
|
+
return token["name"], token["version"]
|
|
875
|
+
|
|
876
|
+
# Default fallback
|
|
877
|
+
return "TetherToken", "1"
|
|
878
|
+
|
|
879
|
+
def _addresses_equal(self, addr1: str, addr2: str) -> bool:
|
|
880
|
+
"""Compare two Ethereum addresses case-insensitively.
|
|
881
|
+
|
|
882
|
+
Ethereum addresses are hex-encoded and should be compared
|
|
883
|
+
case-insensitively (checksummed vs. lowercase).
|
|
884
|
+
|
|
885
|
+
Args:
|
|
886
|
+
addr1: First address
|
|
887
|
+
addr2: Second address
|
|
888
|
+
|
|
889
|
+
Returns:
|
|
890
|
+
True if addresses are equal (case-insensitive)
|
|
891
|
+
"""
|
|
892
|
+
if not addr1 or not addr2:
|
|
893
|
+
return False
|
|
894
|
+
return addr1.lower() == addr2.lower()
|