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
t402/mcp/server.py
CHANGED
|
@@ -9,13 +9,21 @@ from typing import Any, Optional, TextIO
|
|
|
9
9
|
|
|
10
10
|
from .constants import (
|
|
11
11
|
ALL_NETWORKS,
|
|
12
|
+
CHAIN_IDS,
|
|
13
|
+
LAYERZERO_ENDPOINT_IDS,
|
|
12
14
|
LAYERZERO_SCAN_URL,
|
|
15
|
+
NATIVE_DECIMALS,
|
|
13
16
|
NATIVE_SYMBOLS,
|
|
17
|
+
TOKEN_DECIMALS,
|
|
18
|
+
USDT0_ADDRESSES,
|
|
19
|
+
format_token_amount,
|
|
14
20
|
get_explorer_tx_url,
|
|
21
|
+
get_rpc_url,
|
|
15
22
|
get_token_address,
|
|
16
23
|
is_bridgeable_chain,
|
|
17
24
|
is_gasless_network,
|
|
18
25
|
is_valid_network,
|
|
26
|
+
parse_token_amount,
|
|
19
27
|
)
|
|
20
28
|
from .tools import get_tool_definitions
|
|
21
29
|
from .types import (
|
|
@@ -30,6 +38,27 @@ from .types import (
|
|
|
30
38
|
ServerConfig,
|
|
31
39
|
ToolResult,
|
|
32
40
|
)
|
|
41
|
+
from .web3_utils import (
|
|
42
|
+
execute_bridge_send,
|
|
43
|
+
extract_message_guid_from_receipt,
|
|
44
|
+
format_wei_to_ether,
|
|
45
|
+
get_erc20_balance,
|
|
46
|
+
get_native_balance,
|
|
47
|
+
get_web3_provider,
|
|
48
|
+
quote_bridge_fee,
|
|
49
|
+
run_sync_in_executor,
|
|
50
|
+
transfer_erc20,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Estimated bridge times in seconds per destination chain
|
|
55
|
+
ESTIMATED_BRIDGE_TIMES: dict[str, int] = {
|
|
56
|
+
"ethereum": 900, # 15 minutes
|
|
57
|
+
"arbitrum": 300, # 5 minutes
|
|
58
|
+
"ink": 300,
|
|
59
|
+
"berachain": 300,
|
|
60
|
+
"unichain": 300,
|
|
61
|
+
}
|
|
33
62
|
|
|
34
63
|
|
|
35
64
|
class T402McpServer:
|
|
@@ -170,59 +199,167 @@ class T402McpServer:
|
|
|
170
199
|
else:
|
|
171
200
|
result = self._error_result(f"Unknown tool: {tool_name}")
|
|
172
201
|
|
|
173
|
-
return {
|
|
202
|
+
return {
|
|
203
|
+
"content": [asdict(c) for c in result.content],
|
|
204
|
+
"isError": result.isError,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
def _get_web3(self, network: str) -> Any:
|
|
208
|
+
"""Get a Web3 provider for the given network.
|
|
174
209
|
|
|
175
|
-
|
|
176
|
-
|
|
210
|
+
Args:
|
|
211
|
+
network: Network name
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Web3 instance
|
|
215
|
+
"""
|
|
216
|
+
rpc_url = get_rpc_url(self.config, network)
|
|
217
|
+
if not rpc_url:
|
|
218
|
+
raise ValueError(f"No RPC URL configured for {network}")
|
|
219
|
+
return get_web3_provider(rpc_url)
|
|
220
|
+
|
|
221
|
+
async def _fetch_single_balance(
|
|
222
|
+
self, address: str, network: str
|
|
223
|
+
) -> NetworkBalance:
|
|
224
|
+
"""Fetch balance for a single network, returning a NetworkBalance.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
address: Wallet address to check
|
|
228
|
+
network: Network name
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
NetworkBalance with native and token balances
|
|
232
|
+
"""
|
|
177
233
|
try:
|
|
178
|
-
|
|
179
|
-
network = args.get("network", "")
|
|
234
|
+
w3 = self._get_web3(network)
|
|
180
235
|
|
|
181
|
-
|
|
182
|
-
|
|
236
|
+
# Get native balance
|
|
237
|
+
native_raw = await run_sync_in_executor(
|
|
238
|
+
get_native_balance, w3, address
|
|
239
|
+
)
|
|
240
|
+
native_formatted = format_token_amount(native_raw, NATIVE_DECIMALS)
|
|
183
241
|
|
|
184
|
-
# In demo mode or without web3, return placeholder data
|
|
185
242
|
result = NetworkBalance(
|
|
186
243
|
network=network,
|
|
187
244
|
native=BalanceInfo(
|
|
188
245
|
token=NATIVE_SYMBOLS.get(network, "ETH"),
|
|
189
|
-
balance=
|
|
190
|
-
raw=
|
|
246
|
+
balance=native_formatted,
|
|
247
|
+
raw=str(native_raw),
|
|
191
248
|
),
|
|
192
249
|
tokens=[],
|
|
193
250
|
)
|
|
194
251
|
|
|
252
|
+
# Get token balances for USDC, USDT, USDT0
|
|
253
|
+
tokens_to_check = ["USDC", "USDT", "USDT0"]
|
|
254
|
+
for token_name in tokens_to_check:
|
|
255
|
+
token_addr = get_token_address(network, token_name)
|
|
256
|
+
if not token_addr:
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
balance = await run_sync_in_executor(
|
|
261
|
+
get_erc20_balance, w3, token_addr, address
|
|
262
|
+
)
|
|
263
|
+
if balance > 0:
|
|
264
|
+
result.tokens.append(
|
|
265
|
+
BalanceInfo(
|
|
266
|
+
token=token_name,
|
|
267
|
+
balance=format_token_amount(balance, TOKEN_DECIMALS),
|
|
268
|
+
raw=str(balance),
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
except Exception:
|
|
272
|
+
# Skip token if query fails
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
return result
|
|
276
|
+
|
|
277
|
+
except Exception as e:
|
|
278
|
+
return NetworkBalance(
|
|
279
|
+
network=network,
|
|
280
|
+
error=str(e),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def _handle_get_balance(self, args: dict[str, Any]) -> ToolResult:
|
|
284
|
+
"""Handle t402/getBalance tool.
|
|
285
|
+
|
|
286
|
+
Connects to the network via RPC, queries native and ERC-20 token
|
|
287
|
+
balances, and returns formatted results. Falls back to demo mode
|
|
288
|
+
when no RPC is available.
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
address = args.get("address", "")
|
|
292
|
+
network = args.get("network", "")
|
|
293
|
+
|
|
294
|
+
if not is_valid_network(network):
|
|
295
|
+
return self._error_result(f"Invalid network: {network}")
|
|
296
|
+
|
|
297
|
+
# Demo mode returns placeholder data
|
|
298
|
+
if self.config.demo_mode:
|
|
299
|
+
result = NetworkBalance(
|
|
300
|
+
network=network,
|
|
301
|
+
native=BalanceInfo(
|
|
302
|
+
token=NATIVE_SYMBOLS.get(network, "ETH"),
|
|
303
|
+
balance="0.0",
|
|
304
|
+
raw="0",
|
|
305
|
+
),
|
|
306
|
+
tokens=[],
|
|
307
|
+
)
|
|
308
|
+
return self._text_result(self._format_balance_result(result))
|
|
309
|
+
|
|
310
|
+
# Real mode: query blockchain
|
|
311
|
+
result = await self._fetch_single_balance(address, network)
|
|
195
312
|
return self._text_result(self._format_balance_result(result))
|
|
196
313
|
|
|
197
314
|
except Exception as e:
|
|
198
315
|
return self._error_result(str(e))
|
|
199
316
|
|
|
200
317
|
async def _handle_get_all_balances(self, args: dict[str, Any]) -> ToolResult:
|
|
201
|
-
"""Handle t402/getAllBalances tool.
|
|
318
|
+
"""Handle t402/getAllBalances tool.
|
|
319
|
+
|
|
320
|
+
Queries all supported networks in parallel using asyncio.gather,
|
|
321
|
+
reusing _fetch_single_balance per network. Handles per-network
|
|
322
|
+
errors gracefully.
|
|
323
|
+
"""
|
|
202
324
|
try:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
results
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
325
|
+
address = args.get("address", "")
|
|
326
|
+
|
|
327
|
+
# Demo mode returns placeholder data
|
|
328
|
+
if self.config.demo_mode:
|
|
329
|
+
results = []
|
|
330
|
+
for network in ALL_NETWORKS:
|
|
331
|
+
results.append(
|
|
332
|
+
NetworkBalance(
|
|
333
|
+
network=network,
|
|
334
|
+
native=BalanceInfo(
|
|
335
|
+
token=NATIVE_SYMBOLS.get(network, "ETH"),
|
|
336
|
+
balance="0.0",
|
|
337
|
+
raw="0",
|
|
338
|
+
),
|
|
339
|
+
tokens=[],
|
|
340
|
+
)
|
|
216
341
|
)
|
|
217
|
-
)
|
|
342
|
+
return self._text_result(self._format_all_balances_result(results))
|
|
218
343
|
|
|
219
|
-
|
|
344
|
+
# Real mode: query all networks in parallel
|
|
345
|
+
tasks = [
|
|
346
|
+
self._fetch_single_balance(address, network)
|
|
347
|
+
for network in ALL_NETWORKS
|
|
348
|
+
]
|
|
349
|
+
results = await asyncio.gather(*tasks)
|
|
350
|
+
return self._text_result(self._format_all_balances_result(list(results)))
|
|
220
351
|
|
|
221
352
|
except Exception as e:
|
|
222
353
|
return self._error_result(str(e))
|
|
223
354
|
|
|
224
355
|
async def _handle_pay(self, args: dict[str, Any]) -> ToolResult:
|
|
225
|
-
"""Handle t402/pay tool.
|
|
356
|
+
"""Handle t402/pay tool.
|
|
357
|
+
|
|
358
|
+
Validates token support on network, parses amount with correct
|
|
359
|
+
decimals, builds and signs an ERC-20 transfer transaction via web3,
|
|
360
|
+
sends and waits for receipt, and returns the tx hash and explorer URL.
|
|
361
|
+
Falls back to demo mode when no private key is configured.
|
|
362
|
+
"""
|
|
226
363
|
try:
|
|
227
364
|
to = args.get("to", "")
|
|
228
365
|
amount = args.get("amount", "")
|
|
@@ -255,18 +392,60 @@ class T402McpServer:
|
|
|
255
392
|
)
|
|
256
393
|
return self._text_result(self._format_payment_result(result))
|
|
257
394
|
|
|
258
|
-
|
|
395
|
+
# Real mode: execute ERC-20 transfer
|
|
396
|
+
raw_amount = parse_token_amount(amount, TOKEN_DECIMALS)
|
|
397
|
+
w3 = self._get_web3(network)
|
|
398
|
+
|
|
399
|
+
receipt = await run_sync_in_executor(
|
|
400
|
+
transfer_erc20,
|
|
401
|
+
w3,
|
|
402
|
+
self.config.private_key,
|
|
403
|
+
token_addr,
|
|
404
|
+
to,
|
|
405
|
+
raw_amount,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
tx_hash = receipt["transactionHash"].hex()
|
|
409
|
+
if not tx_hash.startswith("0x"):
|
|
410
|
+
tx_hash = "0x" + tx_hash
|
|
411
|
+
|
|
412
|
+
# Derive from_address from private key
|
|
413
|
+
from_address = w3.eth.account.from_key(
|
|
414
|
+
self.config.private_key
|
|
415
|
+
).address
|
|
416
|
+
|
|
417
|
+
result = PaymentResult(
|
|
418
|
+
tx_hash=tx_hash,
|
|
419
|
+
from_address=from_address,
|
|
420
|
+
to=to,
|
|
421
|
+
amount=amount,
|
|
422
|
+
token=token,
|
|
423
|
+
network=network,
|
|
424
|
+
explorer_url=get_explorer_tx_url(network, tx_hash),
|
|
425
|
+
)
|
|
426
|
+
return self._text_result(self._format_payment_result(result))
|
|
259
427
|
|
|
260
428
|
except Exception as e:
|
|
261
429
|
return self._error_result(str(e))
|
|
262
430
|
|
|
263
431
|
async def _handle_pay_gasless(self, args: dict[str, Any]) -> ToolResult:
|
|
264
|
-
"""Handle t402/payGasless tool.
|
|
432
|
+
"""Handle t402/payGasless tool.
|
|
433
|
+
|
|
434
|
+
Builds an ERC-4337 UserOperation using the existing t402.erc4337
|
|
435
|
+
module, submits it to the bundler, polls for receipt, and returns
|
|
436
|
+
the transaction hash. Falls back to demo mode when bundler is
|
|
437
|
+
not configured.
|
|
438
|
+
"""
|
|
265
439
|
try:
|
|
266
440
|
network = args.get("network", "")
|
|
441
|
+
to = args.get("to", "")
|
|
442
|
+
amount = args.get("amount", "")
|
|
443
|
+
token = args.get("token", "")
|
|
267
444
|
|
|
268
445
|
if not is_gasless_network(network):
|
|
269
|
-
return self._error_result(
|
|
446
|
+
return self._error_result(
|
|
447
|
+
f"Network {network} does not support gasless payments"
|
|
448
|
+
)
|
|
270
449
|
|
|
271
450
|
if not self.config.bundler_url and not self.config.demo_mode:
|
|
272
451
|
return self._error_result(
|
|
@@ -278,41 +457,174 @@ class T402McpServer:
|
|
|
278
457
|
result = PaymentResult(
|
|
279
458
|
tx_hash="0x" + "0" * 64 + "_gasless_demo",
|
|
280
459
|
from_address="0x" + "0" * 40,
|
|
281
|
-
to=
|
|
282
|
-
amount=
|
|
283
|
-
token=
|
|
460
|
+
to=to,
|
|
461
|
+
amount=amount,
|
|
462
|
+
token=token,
|
|
284
463
|
network=network,
|
|
285
464
|
explorer_url=get_explorer_tx_url(network, "0x_demo"),
|
|
286
465
|
demo_mode=True,
|
|
287
466
|
)
|
|
288
467
|
return self._text_result(self._format_payment_result(result))
|
|
289
468
|
|
|
290
|
-
|
|
469
|
+
# Real mode: build and submit ERC-4337 UserOperation
|
|
470
|
+
token_addr = get_token_address(network, token)
|
|
471
|
+
if not token_addr:
|
|
472
|
+
return self._error_result(f"Token {token} not supported on {network}")
|
|
473
|
+
|
|
474
|
+
if not self.config.private_key:
|
|
475
|
+
return self._error_result(
|
|
476
|
+
"Private key not configured for gasless payments"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
raw_amount = parse_token_amount(amount, TOKEN_DECIMALS)
|
|
480
|
+
chain_id = CHAIN_IDS.get(network)
|
|
481
|
+
if not chain_id:
|
|
482
|
+
return self._error_result(f"Chain ID not found for {network}")
|
|
483
|
+
|
|
484
|
+
# Use ERC-4337 module to build and submit UserOperation
|
|
485
|
+
from t402.erc4337 import (
|
|
486
|
+
GenericBundlerClient,
|
|
487
|
+
BundlerConfig,
|
|
488
|
+
ENTRYPOINT_V07_ADDRESS,
|
|
489
|
+
UserOperation,
|
|
490
|
+
)
|
|
491
|
+
from web3 import Web3
|
|
492
|
+
|
|
493
|
+
w3 = self._get_web3(network)
|
|
494
|
+
|
|
495
|
+
# Encode the ERC-20 transfer call data
|
|
496
|
+
transfer_selector = Web3.keccak(text="transfer(address,uint256)")[:4]
|
|
497
|
+
to_padded = bytes.fromhex(to[2:]).rjust(32, b"\x00")
|
|
498
|
+
amount_padded = raw_amount.to_bytes(32, "big")
|
|
499
|
+
call_data = transfer_selector + to_padded + amount_padded
|
|
500
|
+
|
|
501
|
+
account = w3.eth.account.from_key(self.config.private_key)
|
|
502
|
+
from_address = account.address
|
|
503
|
+
|
|
504
|
+
nonce = await run_sync_in_executor(
|
|
505
|
+
w3.eth.get_transaction_count, from_address
|
|
506
|
+
)
|
|
507
|
+
gas_price = await run_sync_in_executor(
|
|
508
|
+
lambda: w3.eth.gas_price
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
user_op = UserOperation(
|
|
512
|
+
sender=from_address,
|
|
513
|
+
nonce=nonce,
|
|
514
|
+
call_data=call_data,
|
|
515
|
+
verification_gas_limit=150000,
|
|
516
|
+
call_gas_limit=100000,
|
|
517
|
+
pre_verification_gas=50000,
|
|
518
|
+
max_fee_per_gas=gas_price,
|
|
519
|
+
max_priority_fee_per_gas=gas_price // 10,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
bundler = GenericBundlerClient(
|
|
523
|
+
BundlerConfig(
|
|
524
|
+
bundler_url=self.config.bundler_url,
|
|
525
|
+
chain_id=chain_id,
|
|
526
|
+
entry_point=ENTRYPOINT_V07_ADDRESS,
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# Submit UserOperation
|
|
531
|
+
user_op_hash = await run_sync_in_executor(
|
|
532
|
+
bundler.send_user_operation, user_op
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Poll for receipt
|
|
536
|
+
receipt = await run_sync_in_executor(
|
|
537
|
+
bundler.wait_for_receipt, user_op_hash, 60.0, 2.0
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
tx_hash = receipt.transaction_hash or user_op_hash
|
|
541
|
+
|
|
542
|
+
pay_result = PaymentResult(
|
|
543
|
+
tx_hash=tx_hash,
|
|
544
|
+
from_address=from_address,
|
|
545
|
+
to=to,
|
|
546
|
+
amount=amount,
|
|
547
|
+
token=token,
|
|
548
|
+
network=network,
|
|
549
|
+
explorer_url=get_explorer_tx_url(network, tx_hash),
|
|
550
|
+
)
|
|
551
|
+
return self._text_result(self._format_payment_result(pay_result))
|
|
291
552
|
|
|
292
553
|
except Exception as e:
|
|
293
554
|
return self._error_result(str(e))
|
|
294
555
|
|
|
295
556
|
async def _handle_get_bridge_fee(self, args: dict[str, Any]) -> ToolResult:
|
|
296
|
-
"""Handle t402/getBridgeFee tool.
|
|
557
|
+
"""Handle t402/getBridgeFee tool.
|
|
558
|
+
|
|
559
|
+
Queries the LayerZero OFT contract's quoteSend function to get
|
|
560
|
+
the actual bridge fee estimate. Falls back to demo mode when
|
|
561
|
+
no RPC is available.
|
|
562
|
+
"""
|
|
297
563
|
try:
|
|
298
564
|
from_chain = args.get("fromChain", "")
|
|
299
565
|
to_chain = args.get("toChain", "")
|
|
300
566
|
amount = args.get("amount", "")
|
|
567
|
+
recipient = args.get("recipient", "")
|
|
301
568
|
|
|
302
569
|
if not is_bridgeable_chain(from_chain):
|
|
303
|
-
return self._error_result(
|
|
570
|
+
return self._error_result(
|
|
571
|
+
f"Chain {from_chain} does not support USDT0 bridging"
|
|
572
|
+
)
|
|
304
573
|
if not is_bridgeable_chain(to_chain):
|
|
305
|
-
return self._error_result(
|
|
574
|
+
return self._error_result(
|
|
575
|
+
f"Chain {to_chain} does not support USDT0 bridging"
|
|
576
|
+
)
|
|
306
577
|
if from_chain == to_chain:
|
|
307
|
-
return self._error_result(
|
|
578
|
+
return self._error_result(
|
|
579
|
+
"Source and destination chains must be different"
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# Demo mode returns estimated fee
|
|
583
|
+
if self.config.demo_mode:
|
|
584
|
+
result = BridgeFeeResult(
|
|
585
|
+
native_fee="0.001",
|
|
586
|
+
native_symbol=NATIVE_SYMBOLS.get(from_chain, "ETH"),
|
|
587
|
+
from_chain=from_chain,
|
|
588
|
+
to_chain=to_chain,
|
|
589
|
+
amount=amount,
|
|
590
|
+
estimated_time=ESTIMATED_BRIDGE_TIMES.get(to_chain, 300),
|
|
591
|
+
)
|
|
592
|
+
return self._text_result(self._format_bridge_fee_result(result))
|
|
593
|
+
|
|
594
|
+
# Real mode: query OFT contract
|
|
595
|
+
oft_address = USDT0_ADDRESSES.get(from_chain)
|
|
596
|
+
if not oft_address:
|
|
597
|
+
return self._error_result(f"USDT0 not found on {from_chain}")
|
|
598
|
+
|
|
599
|
+
dst_eid = LAYERZERO_ENDPOINT_IDS.get(to_chain)
|
|
600
|
+
if not dst_eid:
|
|
601
|
+
return self._error_result(
|
|
602
|
+
f"LayerZero endpoint ID not found for {to_chain}"
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
raw_amount = parse_token_amount(amount, TOKEN_DECIMALS)
|
|
606
|
+
w3 = self._get_web3(from_chain)
|
|
607
|
+
|
|
608
|
+
native_fee, _lz_fee = await run_sync_in_executor(
|
|
609
|
+
quote_bridge_fee,
|
|
610
|
+
w3,
|
|
611
|
+
oft_address,
|
|
612
|
+
dst_eid,
|
|
613
|
+
recipient,
|
|
614
|
+
raw_amount,
|
|
615
|
+
raw_amount, # minAmount = amount for quote (no slippage)
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
native_symbol = NATIVE_SYMBOLS.get(from_chain, "ETH")
|
|
619
|
+
fee_formatted = format_wei_to_ether(native_fee)
|
|
308
620
|
|
|
309
621
|
result = BridgeFeeResult(
|
|
310
|
-
native_fee="
|
|
311
|
-
native_symbol=
|
|
622
|
+
native_fee=f"{fee_formatted} {native_symbol}",
|
|
623
|
+
native_symbol=native_symbol,
|
|
312
624
|
from_chain=from_chain,
|
|
313
625
|
to_chain=to_chain,
|
|
314
626
|
amount=amount,
|
|
315
|
-
estimated_time=300,
|
|
627
|
+
estimated_time=ESTIMATED_BRIDGE_TIMES.get(to_chain, 300),
|
|
316
628
|
)
|
|
317
629
|
return self._text_result(self._format_bridge_fee_result(result))
|
|
318
630
|
|
|
@@ -320,18 +632,32 @@ class T402McpServer:
|
|
|
320
632
|
return self._error_result(str(e))
|
|
321
633
|
|
|
322
634
|
async def _handle_bridge(self, args: dict[str, Any]) -> ToolResult:
|
|
323
|
-
"""Handle t402/bridge tool.
|
|
635
|
+
"""Handle t402/bridge tool.
|
|
636
|
+
|
|
637
|
+
Executes a LayerZero OFT send transaction to bridge USDT0
|
|
638
|
+
between chains. Gets a fee quote, executes the send with
|
|
639
|
+
a 10% fee buffer, and extracts the message GUID from the
|
|
640
|
+
OFTSent event logs. Falls back to demo mode when no private
|
|
641
|
+
key is configured.
|
|
642
|
+
"""
|
|
324
643
|
try:
|
|
325
644
|
from_chain = args.get("fromChain", "")
|
|
326
645
|
to_chain = args.get("toChain", "")
|
|
327
646
|
amount = args.get("amount", "")
|
|
647
|
+
recipient = args.get("recipient", "")
|
|
328
648
|
|
|
329
649
|
if not is_bridgeable_chain(from_chain):
|
|
330
|
-
return self._error_result(
|
|
650
|
+
return self._error_result(
|
|
651
|
+
f"Chain {from_chain} does not support USDT0 bridging"
|
|
652
|
+
)
|
|
331
653
|
if not is_bridgeable_chain(to_chain):
|
|
332
|
-
return self._error_result(
|
|
654
|
+
return self._error_result(
|
|
655
|
+
f"Chain {to_chain} does not support USDT0 bridging"
|
|
656
|
+
)
|
|
333
657
|
if from_chain == to_chain:
|
|
334
|
-
return self._error_result(
|
|
658
|
+
return self._error_result(
|
|
659
|
+
"Source and destination chains must be different"
|
|
660
|
+
)
|
|
335
661
|
|
|
336
662
|
if not self.config.private_key and not self.config.demo_mode:
|
|
337
663
|
return self._error_result(
|
|
@@ -349,12 +675,91 @@ class T402McpServer:
|
|
|
349
675
|
amount=amount,
|
|
350
676
|
explorer_url=get_explorer_tx_url(from_chain, "0x_demo"),
|
|
351
677
|
tracking_url=LAYERZERO_SCAN_URL + demo_guid,
|
|
352
|
-
estimated_time=300,
|
|
678
|
+
estimated_time=ESTIMATED_BRIDGE_TIMES.get(to_chain, 300),
|
|
353
679
|
demo_mode=True,
|
|
354
680
|
)
|
|
355
681
|
return self._text_result(self._format_bridge_result(result))
|
|
356
682
|
|
|
357
|
-
|
|
683
|
+
# Real mode: execute LayerZero OFT bridge
|
|
684
|
+
oft_address = USDT0_ADDRESSES.get(from_chain)
|
|
685
|
+
if not oft_address:
|
|
686
|
+
return self._error_result(f"USDT0 not found on {from_chain}")
|
|
687
|
+
|
|
688
|
+
dst_eid = LAYERZERO_ENDPOINT_IDS.get(to_chain)
|
|
689
|
+
if not dst_eid:
|
|
690
|
+
return self._error_result(
|
|
691
|
+
f"LayerZero endpoint ID not found for {to_chain}"
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
raw_amount = parse_token_amount(amount, TOKEN_DECIMALS)
|
|
695
|
+
|
|
696
|
+
# Calculate min amount with 0.5% slippage
|
|
697
|
+
min_amount = raw_amount - (raw_amount * 50) // 10000
|
|
698
|
+
|
|
699
|
+
w3 = self._get_web3(from_chain)
|
|
700
|
+
|
|
701
|
+
# Get fee quote
|
|
702
|
+
native_fee, _lz_fee = await run_sync_in_executor(
|
|
703
|
+
quote_bridge_fee,
|
|
704
|
+
w3,
|
|
705
|
+
oft_address,
|
|
706
|
+
dst_eid,
|
|
707
|
+
recipient,
|
|
708
|
+
raw_amount,
|
|
709
|
+
min_amount,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Add 10% buffer to fee
|
|
713
|
+
native_fee_with_buffer = (native_fee * 110) // 100
|
|
714
|
+
|
|
715
|
+
# Check USDT0 balance
|
|
716
|
+
balance = await run_sync_in_executor(
|
|
717
|
+
get_erc20_balance, w3, oft_address,
|
|
718
|
+
w3.eth.account.from_key(self.config.private_key).address,
|
|
719
|
+
)
|
|
720
|
+
if balance < raw_amount:
|
|
721
|
+
return self._error_result(
|
|
722
|
+
f"Insufficient USDT0 balance: have {format_token_amount(balance, TOKEN_DECIMALS)}, "
|
|
723
|
+
f"need {amount}"
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
# Execute bridge send
|
|
727
|
+
receipt = await run_sync_in_executor(
|
|
728
|
+
execute_bridge_send,
|
|
729
|
+
w3,
|
|
730
|
+
self.config.private_key,
|
|
731
|
+
oft_address,
|
|
732
|
+
dst_eid,
|
|
733
|
+
recipient,
|
|
734
|
+
raw_amount,
|
|
735
|
+
min_amount,
|
|
736
|
+
native_fee_with_buffer,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
tx_hash = receipt["transactionHash"].hex()
|
|
740
|
+
if not tx_hash.startswith("0x"):
|
|
741
|
+
tx_hash = "0x" + tx_hash
|
|
742
|
+
|
|
743
|
+
# Extract message GUID from logs
|
|
744
|
+
message_guid = extract_message_guid_from_receipt(receipt)
|
|
745
|
+
if not message_guid:
|
|
746
|
+
return self._error_result(
|
|
747
|
+
"Bridge transaction succeeded but failed to extract message GUID from logs"
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
estimated_time = ESTIMATED_BRIDGE_TIMES.get(to_chain, 300)
|
|
751
|
+
|
|
752
|
+
bridge_result = BridgeResultData(
|
|
753
|
+
tx_hash=tx_hash,
|
|
754
|
+
message_guid=message_guid,
|
|
755
|
+
from_chain=from_chain,
|
|
756
|
+
to_chain=to_chain,
|
|
757
|
+
amount=amount,
|
|
758
|
+
explorer_url=get_explorer_tx_url(from_chain, tx_hash),
|
|
759
|
+
tracking_url=LAYERZERO_SCAN_URL + message_guid,
|
|
760
|
+
estimated_time=estimated_time,
|
|
761
|
+
)
|
|
762
|
+
return self._text_result(self._format_bridge_result(bridge_result))
|
|
358
763
|
|
|
359
764
|
except Exception as e:
|
|
360
765
|
return self._error_result(str(e))
|
|
@@ -402,13 +807,15 @@ class T402McpServer:
|
|
|
402
807
|
for result in results:
|
|
403
808
|
if result.error:
|
|
404
809
|
lines.append(f"### {result.network}")
|
|
405
|
-
lines.append(f"
|
|
810
|
+
lines.append(f"Error: {result.error}")
|
|
406
811
|
lines.append("")
|
|
407
812
|
continue
|
|
408
813
|
|
|
409
814
|
lines.append(f"### {result.network}")
|
|
410
815
|
if result.native:
|
|
411
|
-
lines.append(
|
|
816
|
+
lines.append(
|
|
817
|
+
f"- Native ({result.native.token}): {result.native.balance}"
|
|
818
|
+
)
|
|
412
819
|
for token in result.tokens:
|
|
413
820
|
lines.append(f"- {token.token}: {token.balance}")
|
|
414
821
|
lines.append("")
|
|
@@ -420,58 +827,68 @@ class T402McpServer:
|
|
|
420
827
|
lines = []
|
|
421
828
|
|
|
422
829
|
if result.demo_mode:
|
|
423
|
-
lines.extend(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
830
|
+
lines.extend(
|
|
831
|
+
[
|
|
832
|
+
"## Payment (Demo Mode)",
|
|
833
|
+
"",
|
|
834
|
+
"This is a simulated transaction. No actual tokens were transferred.",
|
|
835
|
+
"",
|
|
836
|
+
]
|
|
837
|
+
)
|
|
429
838
|
else:
|
|
430
839
|
lines.extend(["## Payment Successful", ""])
|
|
431
840
|
|
|
432
|
-
lines.extend(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
841
|
+
lines.extend(
|
|
842
|
+
[
|
|
843
|
+
f"- **Amount:** {result.amount} {result.token}",
|
|
844
|
+
f"- **To:** {result.to}",
|
|
845
|
+
f"- **Network:** {result.network}",
|
|
846
|
+
f"- **Transaction:** [{self._truncate_hash(result.tx_hash)}]({result.explorer_url})",
|
|
847
|
+
]
|
|
848
|
+
)
|
|
438
849
|
|
|
439
850
|
return "\n".join(lines)
|
|
440
851
|
|
|
441
852
|
def _format_bridge_fee_result(self, result: BridgeFeeResult) -> str:
|
|
442
853
|
"""Format bridge fee result as markdown."""
|
|
443
|
-
return "\n".join(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
854
|
+
return "\n".join(
|
|
855
|
+
[
|
|
856
|
+
"## Bridge Fee Quote",
|
|
857
|
+
"",
|
|
858
|
+
f"- **From:** {result.from_chain}",
|
|
859
|
+
f"- **To:** {result.to_chain}",
|
|
860
|
+
f"- **Amount:** {result.amount} USDT0",
|
|
861
|
+
f"- **Fee:** {result.native_fee} {result.native_symbol}",
|
|
862
|
+
f"- **Estimated Time:** ~{result.estimated_time} seconds",
|
|
863
|
+
]
|
|
864
|
+
)
|
|
452
865
|
|
|
453
866
|
def _format_bridge_result(self, result: BridgeResultData) -> str:
|
|
454
867
|
"""Format bridge result as markdown."""
|
|
455
868
|
lines = []
|
|
456
869
|
|
|
457
870
|
if result.demo_mode:
|
|
458
|
-
lines.extend(
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
871
|
+
lines.extend(
|
|
872
|
+
[
|
|
873
|
+
"## Bridge (Demo Mode)",
|
|
874
|
+
"",
|
|
875
|
+
"This is a simulated bridge. No actual tokens were transferred.",
|
|
876
|
+
"",
|
|
877
|
+
]
|
|
878
|
+
)
|
|
464
879
|
else:
|
|
465
880
|
lines.extend(["## Bridge Initiated", ""])
|
|
466
881
|
|
|
467
|
-
lines.extend(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
882
|
+
lines.extend(
|
|
883
|
+
[
|
|
884
|
+
f"- **Amount:** {result.amount} USDT0",
|
|
885
|
+
f"- **From:** {result.from_chain}",
|
|
886
|
+
f"- **To:** {result.to_chain}",
|
|
887
|
+
f"- **Transaction:** [{self._truncate_hash(result.tx_hash)}]({result.explorer_url})",
|
|
888
|
+
f"- **Track:** [LayerZero Scan]({result.tracking_url})",
|
|
889
|
+
f"- **Estimated Delivery:** ~{result.estimated_time} seconds",
|
|
890
|
+
]
|
|
891
|
+
)
|
|
475
892
|
|
|
476
893
|
return "\n".join(lines)
|
|
477
894
|
|