t402 1.9.1__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.
Files changed (51) hide show
  1. t402/__init__.py +1 -1
  2. t402/a2a/__init__.py +73 -0
  3. t402/a2a/helpers.py +158 -0
  4. t402/a2a/types.py +145 -0
  5. t402/bridge/constants.py +1 -1
  6. t402/django/__init__.py +42 -0
  7. t402/django/middleware.py +596 -0
  8. t402/errors.py +213 -0
  9. t402/facilitator.py +125 -0
  10. t402/mcp/constants.py +3 -6
  11. t402/mcp/server.py +428 -44
  12. t402/mcp/web3_utils.py +493 -0
  13. t402/multisig/__init__.py +120 -0
  14. t402/multisig/constants.py +54 -0
  15. t402/multisig/safe.py +441 -0
  16. t402/multisig/signature.py +228 -0
  17. t402/multisig/transaction.py +238 -0
  18. t402/multisig/types.py +108 -0
  19. t402/multisig/utils.py +77 -0
  20. t402/schemes/__init__.py +19 -0
  21. t402/schemes/cosmos/__init__.py +114 -0
  22. t402/schemes/cosmos/constants.py +211 -0
  23. t402/schemes/cosmos/exact_direct/__init__.py +21 -0
  24. t402/schemes/cosmos/exact_direct/client.py +198 -0
  25. t402/schemes/cosmos/exact_direct/facilitator.py +493 -0
  26. t402/schemes/cosmos/exact_direct/server.py +315 -0
  27. t402/schemes/cosmos/types.py +501 -0
  28. t402/schemes/evm/__init__.py +1 -1
  29. t402/schemes/evm/exact_legacy/server.py +1 -1
  30. t402/schemes/near/__init__.py +25 -0
  31. t402/schemes/near/upto/__init__.py +54 -0
  32. t402/schemes/near/upto/types.py +272 -0
  33. t402/schemes/svm/__init__.py +15 -0
  34. t402/schemes/svm/upto/__init__.py +23 -0
  35. t402/schemes/svm/upto/types.py +193 -0
  36. t402/schemes/ton/__init__.py +15 -0
  37. t402/schemes/ton/upto/__init__.py +31 -0
  38. t402/schemes/ton/upto/types.py +215 -0
  39. t402/schemes/tron/__init__.py +21 -4
  40. t402/schemes/tron/upto/__init__.py +30 -0
  41. t402/schemes/tron/upto/types.py +213 -0
  42. t402/starlette/__init__.py +38 -0
  43. t402/starlette/middleware.py +522 -0
  44. t402/ton.py +1 -1
  45. t402/ton_paywall_template.py +1 -1
  46. t402/types.py +100 -2
  47. t402/wdk/chains.py +1 -1
  48. {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/METADATA +3 -3
  49. {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/RECORD +51 -20
  50. {t402-1.9.1.dist-info → t402-1.10.0.dist-info}/WHEEL +0 -0
  51. {t402-1.9.1.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:
@@ -175,57 +204,162 @@ class T402McpServer:
175
204
  "isError": result.isError,
176
205
  }
177
206
 
178
- async def _handle_get_balance(self, args: dict[str, Any]) -> ToolResult:
179
- """Handle t402/getBalance tool."""
207
+ def _get_web3(self, network: str) -> Any:
208
+ """Get a Web3 provider for the given network.
209
+
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
+ """
180
233
  try:
181
- _address = args.get("address", "") # noqa: F841 - reserved for future use
182
- network = args.get("network", "")
234
+ w3 = self._get_web3(network)
183
235
 
184
- if not is_valid_network(network):
185
- return self._error_result(f"Invalid network: {network}")
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)
186
241
 
187
- # In demo mode or without web3, return placeholder data
188
242
  result = NetworkBalance(
189
243
  network=network,
190
244
  native=BalanceInfo(
191
245
  token=NATIVE_SYMBOLS.get(network, "ETH"),
192
- balance="0.0",
193
- raw="0",
246
+ balance=native_formatted,
247
+ raw=str(native_raw),
194
248
  ),
195
249
  tokens=[],
196
250
  )
197
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)
198
312
  return self._text_result(self._format_balance_result(result))
199
313
 
200
314
  except Exception as e:
201
315
  return self._error_result(str(e))
202
316
 
203
317
  async def _handle_get_all_balances(self, args: dict[str, Any]) -> ToolResult:
204
- """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
+ """
205
324
  try:
206
- _address = args.get("address", "") # noqa: F841 - reserved for future use
207
-
208
- results = []
209
- for network in ALL_NETWORKS:
210
- results.append(
211
- NetworkBalance(
212
- network=network,
213
- native=BalanceInfo(
214
- token=NATIVE_SYMBOLS.get(network, "ETH"),
215
- balance="0.0",
216
- raw="0",
217
- ),
218
- tokens=[],
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
+ )
219
341
  )
220
- )
342
+ return self._text_result(self._format_all_balances_result(results))
221
343
 
222
- return self._text_result(self._format_all_balances_result(results))
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)))
223
351
 
224
352
  except Exception as e:
225
353
  return self._error_result(str(e))
226
354
 
227
355
  async def _handle_pay(self, args: dict[str, Any]) -> ToolResult:
228
- """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
+ """
229
363
  try:
230
364
  to = args.get("to", "")
231
365
  amount = args.get("amount", "")
@@ -258,17 +392,55 @@ class T402McpServer:
258
392
  )
259
393
  return self._text_result(self._format_payment_result(result))
260
394
 
261
- return self._error_result(
262
- "Real transactions require private key configuration"
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,
263
406
  )
264
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))
427
+
265
428
  except Exception as e:
266
429
  return self._error_result(str(e))
267
430
 
268
431
  async def _handle_pay_gasless(self, args: dict[str, Any]) -> ToolResult:
269
- """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
+ """
270
439
  try:
271
440
  network = args.get("network", "")
441
+ to = args.get("to", "")
442
+ amount = args.get("amount", "")
443
+ token = args.get("token", "")
272
444
 
273
445
  if not is_gasless_network(network):
274
446
  return self._error_result(
@@ -285,26 +457,114 @@ class T402McpServer:
285
457
  result = PaymentResult(
286
458
  tx_hash="0x" + "0" * 64 + "_gasless_demo",
287
459
  from_address="0x" + "0" * 40,
288
- to=args.get("to", ""),
289
- amount=args.get("amount", ""),
290
- token=args.get("token", ""),
460
+ to=to,
461
+ amount=amount,
462
+ token=token,
291
463
  network=network,
292
464
  explorer_url=get_explorer_tx_url(network, "0x_demo"),
293
465
  demo_mode=True,
294
466
  )
295
467
  return self._text_result(self._format_payment_result(result))
296
468
 
297
- return self._error_result("Gasless payments require bundler configuration")
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))
298
552
 
299
553
  except Exception as e:
300
554
  return self._error_result(str(e))
301
555
 
302
556
  async def _handle_get_bridge_fee(self, args: dict[str, Any]) -> ToolResult:
303
- """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
+ """
304
563
  try:
305
564
  from_chain = args.get("fromChain", "")
306
565
  to_chain = args.get("toChain", "")
307
566
  amount = args.get("amount", "")
567
+ recipient = args.get("recipient", "")
308
568
 
309
569
  if not is_bridgeable_chain(from_chain):
310
570
  return self._error_result(
@@ -319,13 +579,52 @@ class T402McpServer:
319
579
  "Source and destination chains must be different"
320
580
  )
321
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)
620
+
322
621
  result = BridgeFeeResult(
323
- native_fee="0.001",
324
- native_symbol=NATIVE_SYMBOLS.get(from_chain, "ETH"),
622
+ native_fee=f"{fee_formatted} {native_symbol}",
623
+ native_symbol=native_symbol,
325
624
  from_chain=from_chain,
326
625
  to_chain=to_chain,
327
626
  amount=amount,
328
- estimated_time=300,
627
+ estimated_time=ESTIMATED_BRIDGE_TIMES.get(to_chain, 300),
329
628
  )
330
629
  return self._text_result(self._format_bridge_fee_result(result))
331
630
 
@@ -333,11 +632,19 @@ class T402McpServer:
333
632
  return self._error_result(str(e))
334
633
 
335
634
  async def _handle_bridge(self, args: dict[str, Any]) -> ToolResult:
336
- """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
+ """
337
643
  try:
338
644
  from_chain = args.get("fromChain", "")
339
645
  to_chain = args.get("toChain", "")
340
646
  amount = args.get("amount", "")
647
+ recipient = args.get("recipient", "")
341
648
 
342
649
  if not is_bridgeable_chain(from_chain):
343
650
  return self._error_result(
@@ -368,14 +675,91 @@ class T402McpServer:
368
675
  amount=amount,
369
676
  explorer_url=get_explorer_tx_url(from_chain, "0x_demo"),
370
677
  tracking_url=LAYERZERO_SCAN_URL + demo_guid,
371
- estimated_time=300,
678
+ estimated_time=ESTIMATED_BRIDGE_TIMES.get(to_chain, 300),
372
679
  demo_mode=True,
373
680
  )
374
681
  return self._text_result(self._format_bridge_result(result))
375
682
 
376
- return self._error_result(
377
- "Bridge functionality requires private key configuration"
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,
378
761
  )
762
+ return self._text_result(self._format_bridge_result(bridge_result))
379
763
 
380
764
  except Exception as e:
381
765
  return self._error_result(str(e))
@@ -423,7 +807,7 @@ class T402McpServer:
423
807
  for result in results:
424
808
  if result.error:
425
809
  lines.append(f"### {result.network}")
426
- lines.append(f" {result.error}")
810
+ lines.append(f"Error: {result.error}")
427
811
  lines.append("")
428
812
  continue
429
813
 
@@ -447,7 +831,7 @@ class T402McpServer:
447
831
  [
448
832
  "## Payment (Demo Mode)",
449
833
  "",
450
- "⚠️ This is a simulated transaction. No actual tokens were transferred.",
834
+ "This is a simulated transaction. No actual tokens were transferred.",
451
835
  "",
452
836
  ]
453
837
  )
@@ -488,7 +872,7 @@ class T402McpServer:
488
872
  [
489
873
  "## Bridge (Demo Mode)",
490
874
  "",
491
- "⚠️ This is a simulated bridge. No actual tokens were transferred.",
875
+ "This is a simulated bridge. No actual tokens were transferred.",
492
876
  "",
493
877
  ]
494
878
  )